# i18n Architecture (`@repo/core-i18n`)
> **Architectural Foundation:** [i18next](https://www.i18next.com/) · [react-i18next](https://react.i18next.com/)
>
> **Description:** Hybrid namespace internationalization engine built on i18next, providing centralized common vocabularies with lazy-loaded feature dictionaries, tenant overrides, and backend sync with automatic rollback.
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
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
classDef coreEngine fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
classDef dataStore fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
classDef externalAPI fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
%% ─── Subgraphs ───
subgraph Apps ["apps/* (App Autonomy)"]
UI([React Components])
DICT[[Feature Dictionaries: 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
%% ─── Flow & Relationships ───
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
%% ─── Apply Styles ───
class UI,DICT appEntity;
class I18N coreEngine;
class STORE,COMMON dataStore;
class SYNC,TENANT externalAPI;
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
style Backend fill:transparent,stroke:#f59e0b,stroke-width:2px,stroke-dasharray: 5 5
```
---
## 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.LANGUAGE);
return typeof stored === 'string' ? stored : null;
},
setLanguage: async (lng: string) => {
await secureStorage.setItem(AppStorageKey.LANGUAGE, 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
```
### Namespace Registration & Context
To prevent unnecessary React re-renders and ensure dictionaries are loaded before the first paint, translations are registered at the module scope using `registerModuleNamespace`.
**1. Register the Namespace in the Module Factory:**
Call `registerModuleNamespace` at the top level of your module factory (e.g., `index.tsx`). This function is idempotent and safe to call outside the React render cycle. The namespace provided must match the `translationNamespace` defined in your module configuration.
```tsx
// apps/web/src/apps/modules/booking/presentation/factory/index.tsx
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
import { registerModuleNamespace } from '@repo/core-i18n';
import { BookingModuleConfig } from '../../domain/constants';
import bookingId from '../locales/id/booking.json';
import bookingEn from '../locales/en/booking.json';
// Called once at import time — safe, idempotent, outside React render cycle.
registerModuleNamespace(BookingModuleConfig.translationNamespace, {
id: bookingId,
en: bookingEn,
});
export default function BookingModule() {
return {/* Routes and Pages */};
}
```
**2. Augment Types for Autocomplete:**
To get native TypeScript autocomplete for nested keys (e.g., `header.title`), augment the global `react-i18next` 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 };
}
}
```
**3. Consume in Components:**
Inside your pages and components, use `useEnterpriseModuleTranslationContext()` instead of the raw `useTranslation` hook. The context automatically scopes the `t` function to your module's namespace and the global `common` namespace.
> [!WARNING] > **Best Practice:** Do not hardcode the module namespace prefix (e.g., `t('booking:header.title')`) when using `useEnterpriseModuleTranslationContext`. The provider already scopes it. Simply use `t('header.title')`. You can still access global keys using the common prefix: `t('common:edit')`.
```tsx
// apps/web/src/apps/modules/booking/presentation/pages/booking.page.index.tsx
import { Title } from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
export default function BookingPageIndex() {
const { t } = useEnterpriseModuleTranslationContext();
return (
{t('title')} {/* Automatically resolves to booking:title */}
{/* Fallback to global common vocabulary */}
);
}
```
**4. Dynamic Variables (Interpolation):**
```json
// booking.json
{
"messages": {
"welcome": "Welcome back, {{name}}! You have {{count}} new bookings."
}
}
```
```tsx
// Inside component
```
---
## 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"
}
}
}
}
```