Files
trackgo-fe/packages/core-i18n

Enterprise i18n Architecture (@repo/core-i18n)

← Back to Root

A highly decoupled, type-safe internationalization engine for the Eigen Monorepo.

It uses a Hybrid Namespace Strategy:

  1. Centralized Engine: Setup, local persistence (@repo/core-storage), 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 and networking decisions entirely to the consuming applications.


Overview Architecture

graph TD
    subgraph Apps ["apps/* (App Autonomy)"]
        UI[React Components]
        DICT[Feature Dictionaries<br/>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.

// apps/web/src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { setupI18n } from '@repo/core-i18n';
import App from './app';

async function bootstrap() {
  // Synchronously reads preferred language from storage & inits i18next
  await setupI18n();

  createRoot(document.getElementById('app')!).render(
    <StrictMode><App /></StrictMode>,
  );
}
bootstrap();

2. Module-Level Setup (Decentralized Dictionaries)

Dictionaries live right next to the UI components that use them.

Folder Structure

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:

// 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:

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 <h1>{t('booking:header.title')}</h1>; // Autocomplete works!
}

3. Dynamic Variables (Interpolation):

// booking.json
{
  "messages": {
    "welcome": "Welcome back, {{name}}! You have {{count}} new bookings."
  }
}
// Inside component
<h1>{t('booking:messages.welcome', { name: 'Firman', count: 5 })}</h1>

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.

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.

// 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.

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):

{
  "data": {
    "namespace": "booking",
    "overrides": {
      "module_name": "Procurement",
      "header": {
        "title": "Procurement List"
      }
    }
  }
}