[← Back to Root](../../README.md) # i18n Architecture (`@repo/core-i18n`) A highly decoupled, type-safe internationalization engine for the monorepo. It uses a **Hybrid Namespace Strategy**: 1. **Centralized Engine**: Setup, local persistence orchestration, and global words (`common`). 2. **Decentralized Dictionaries**: Feature-specific translations (`booking`, `billing`) live inside the application modules and are lazy-loaded. This architecture strictly adheres to **Inversion of Control (IoC)**. The core engine handles local state and performance, but leaves API, networking, and storage implementation decisions entirely to the consuming applications. --- ## Overview Architecture ```mermaid graph TD subgraph Apps ["apps/* (App Autonomy)"] UI[React Components] DICT[Feature Dictionaries
e.g., booking.json] end subgraph Core ["@repo/core-i18n (Engine)"] I18N((i18next Instance)) STORE[(core-storage)] COMMON[Common Vocabulary] end subgraph Backend ["Backend API (External)"] SYNC[Language Sync Endpoint] TENANT[Tenant Config Endpoint] end UI -->|uses useTranslation| I18N DICT -.->|lazy loads| I18N COMMON -->|preloads| I18N I18N <-->|reads/persists| STORE I18N -->|changeLanguage sync| SYNC SYNC -.->|fails? rollback| I18N TENANT -.->|applyTenantOverrides| I18N %% Styling Subgraphs (Backgrounds) style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057 style Backend fill:#fff4e6,stroke:#ffd8a8,stroke-width:2px,color:#d9480f %% Styling App Nodes (Blue) style UI fill:#339af0,stroke:#1864ab,color:#fff style DICT fill:#339af0,stroke:#1864ab,color:#fff %% Styling Core Nodes (Purple Engine, Green Storage/Data) style I18N fill:#845ef7,stroke:#5f3dc4,color:#fff style STORE fill:#20c997,stroke:#089981,color:#fff style COMMON fill:#20c997,stroke:#089981,color:#fff %% Styling Backend Nodes (Orange/Network) style SYNC fill:#fd7e14,stroke:#d9480f,color:#fff style TENANT fill:#fd7e14,stroke:#d9480f,color:#fff ``` --- ## 1. App-Level Setup (Bootstrap) Initialize the engine *before* your React application mounts to prevent UI flashing. Provide an `I18nStorageAdapter` using Dependency Injection so the core engine can persist the user's language without being tightly coupled to a specific storage implementation. ```tsx // apps/web/src/main.tsx import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { setupI18n } from '@repo/core-i18n'; import { secureStorage, AppStorageKey } from './core/storage'; import App from './app'; async function bootstrap() { // Synchronously reads preferred language from injected storage & inits i18next await setupI18n({ storageAdapter: { getLanguage: async () => { const stored = await secureStorage.getItem(AppStorageKey.LOCALE); return typeof stored === 'string' ? stored : null; }, setLanguage: async (lng: string) => { await secureStorage.setItem(AppStorageKey.LOCALE, lng); }, }, }); createRoot(document.getElementById('app')!).render( , ); } bootstrap(); ``` --- ## 2. Module-Level Setup (Decentralized Dictionaries) Dictionaries live right next to the UI components that use them. ### Folder Structure ```text apps/web/src/apps/modules/booking/ ├── presentation/BookingTable.tsx └── locales/ ├── id/booking.json └── en/booking.json ``` ### Lazy Loading & Type Safety Register the namespace when the component mounts. To get native TypeScript autocomplete for nested keys (e.g., `header.title`), augment the global `react-i18next` types. **1. Augment Types:** ```ts // apps/web/src/types/i18next.d.ts import 'react-i18next'; import type { resources as coreResources } from '@repo/core-i18n/src/setup'; import bookingEn from '../apps/modules/booking/locales/en/booking.json'; declare module 'react-i18next' { interface CustomTypeOptions { defaultNS: 'common'; resources: typeof coreResources['en'] & { booking: typeof bookingEn }; } } ``` **2. Use in Component:** ```tsx import { useEffect } from 'react'; import { i18n, useTranslation } from '@repo/core-i18n'; import bookingId from '../locales/id/booking.json'; import bookingEn from '../locales/en/booking.json'; export default function BookingFeature() { const { t } = useTranslation(['common', 'booking']); useEffect(() => { i18n.addResourceBundle('id', 'booking', bookingId, true, false); i18n.addResourceBundle('en', 'booking', bookingEn, true, false); }, []); return

{t('booking:header.title')}

; // Autocomplete works! } ``` **3. Dynamic Variables (Interpolation):** ```json // booking.json { "messages": { "welcome": "Welcome back, {{name}}! You have {{count}} new bookings." } } ``` ```tsx // Inside component

{t('booking:messages.welcome', { name: 'Firman', count: 5 })}

``` --- ## 3. Usage Outside React Components (Vanilla TS) For utility files, API interceptors, or vanilla functions where React hooks cannot be used, import the raw `i18n` instance directly. ```ts import { i18n } from '@repo/core-i18n'; // Must specify the namespace explicitly if it's not 'common' export const getErrorMessage = (code: string) => { return i18n.t(`booking:errors.${code}`, { defaultValue: 'Unknown Error' }); }; ``` --- ## 4. Real-World Implementation Flow The engine supports robust flows for authenticated apps, including Tenant Vocabulary Overrides and Backend Synchronization. ### A. The Tenant Override Flow (After Login) If "Company A" calls "Purchasing" -> "Procurement", they shouldn't need a custom build. The backend returns an override config, and the frontend dynamically merges it using `applyTenantOverrides`. ```tsx // Example inside an AuthProvider or Post-Login useEffect import { useEffect } from 'react'; import { applyTenantOverrides } from '@repo/core-i18n'; import { api } from '@/api'; export function AuthProvider({ children }) { useEffect(() => { async function fetchTenantConfig() { try { // 1. Fetch tenant-specific overrides from the API const response = await api.get('/v1/tenant/i18n-config'); // 2. Inject into the engine. // `deep: true` ensures only provided keys are overridden. applyTenantOverrides( response.data.namespace, response.data.overrides ); } catch (err) { console.error("Failed to fetch tenant configuration", err); } } fetchTenantConfig(); }, []); return <>{children}; } ``` ### B. User Preference Sync (With Rollback) When a logged-in user changes their language, we update the UI instantly, save it locally, and sync it to the backend. If the backend fails, the engine automatically rolls back. ```tsx import { changeLanguage } from '@repo/core-i18n'; import { api } from '@/api'; const handleSwitch = async (newLng: string) => { try { await changeLanguage(newLng, async (lng) => { // The core engine waits for this Promise. // If it throws, the UI reverts to the previous language automatically. await api.patch('/v1/user/profile', { language: lng }); }); toast.success('Language saved!'); } catch (err) { toast.error('Sync failed. Reverted to previous language.'); } }; ``` > [!NOTE] > For public pages (like `apps/landing`), simply call `changeLanguage('en')` without the callback function. It will update the UI and local storage instantly without hitting the network. --- ## 5. Backend API Contract (For Backend Engineers) To support Dynamic Tenant Overrides, the backend must expose an endpoint (e.g., `GET /v1/tenant/i18n-config`). ### Identification The backend **MUST identify the tenant via the `Authorization` (JWT) header**. The frontend will not send `tenantId` in the query payload to prevent spoofing. ### Expected JSON Response Format The response must match the structural shape of the frontend dictionary. Because the frontend uses a **Deep Merge** strategy, the backend **only needs to return the specific keys the tenant wishes to override**. If the frontend dictionary has `header.title` and `header.subtitle`, and the backend only sends `header.title`, the `subtitle` will safely remain intact. **Example Request:** `GET /v1/tenant/i18n-config` *(Authorization: Bearer eyJhbG...)* **Expected Response (200 OK):** ```json { "data": { "namespace": "booking", "overrides": { "module_name": "Procurement", "header": { "title": "Procurement List" } } } } ```