Refactor i18n structure and update language files
- Removed old validation and common language files for English and Indonesian. - Created new language files for English and Indonesian under the updated directory structure. - Updated setup to reflect new language file paths and changed default language to English. - Modified storage keys from 'locale' to 'language' for consistency. - Added new language files for various modules including bookmarks, history, navigation, and system settings. - Introduced new booking module language files for both English and Indonesian. - Implemented a login page component with basic structure.
This commit is contained in:
@@ -4,9 +4,10 @@
|
||||
>
|
||||
> **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.
|
||||
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.
|
||||
|
||||
@@ -46,10 +47,10 @@ graph TD
|
||||
DICT -.->|lazy loads| I18N
|
||||
COMMON --->|preloads| I18N
|
||||
I18N <===>|reads / persists| STORE
|
||||
|
||||
|
||||
I18N --->|changeLanguage sync| SYNC
|
||||
SYNC -.->|fails? rollback| I18N
|
||||
|
||||
|
||||
TENANT -.->|applyTenantOverrides| I18N
|
||||
|
||||
%% ─── Apply Styles ───
|
||||
@@ -68,7 +69,7 @@ graph TD
|
||||
|
||||
## 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.
|
||||
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
|
||||
@@ -83,17 +84,19 @@ async function bootstrap() {
|
||||
await setupI18n({
|
||||
storageAdapter: {
|
||||
getLanguage: async () => {
|
||||
const stored = await secureStorage.getItem(AppStorageKey.LOCALE);
|
||||
const stored = await secureStorage.getItem(AppStorageKey.LANGUAGE);
|
||||
return typeof stored === 'string' ? stored : null;
|
||||
},
|
||||
setLanguage: async (lng: string) => {
|
||||
await secureStorage.setItem(AppStorageKey.LOCALE, lng);
|
||||
await secureStorage.setItem(AppStorageKey.LANGUAGE, lng);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById('app')!).render(
|
||||
<StrictMode><App /></StrictMode>,
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
bootstrap();
|
||||
@@ -106,6 +109,7 @@ bootstrap();
|
||||
Dictionaries live right next to the UI components that use them.
|
||||
|
||||
### Folder Structure
|
||||
|
||||
```text
|
||||
apps/web/src/apps/modules/booking/
|
||||
├── presentation/BookingTable.tsx
|
||||
@@ -138,11 +142,7 @@ registerModuleNamespace(BookingModuleConfig.translationNamespace, {
|
||||
});
|
||||
|
||||
export default function BookingModule() {
|
||||
return (
|
||||
<EnterpriseModuleProvider config={BookingModuleConfig}>
|
||||
{/* Routes and Pages */}
|
||||
</EnterpriseModuleProvider>
|
||||
);
|
||||
return <EnterpriseModuleProvider config={BookingModuleConfig}>{/* Routes and Pages */}</EnterpriseModuleProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -159,17 +159,16 @@ 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 };
|
||||
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.
|
||||
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')`.
|
||||
> [!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
|
||||
@@ -189,6 +188,7 @@ export default function BookingPageIndex() {
|
||||
```
|
||||
|
||||
**4. Dynamic Variables (Interpolation):**
|
||||
|
||||
```json
|
||||
// booking.json
|
||||
{
|
||||
@@ -197,6 +197,7 @@ export default function BookingPageIndex() {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Inside component
|
||||
<h1>{t('messages.welcome', { name: 'Firman', count: 5 })}</h1>
|
||||
@@ -221,7 +222,7 @@ export const getErrorMessage = (code: string) => {
|
||||
|
||||
## 4. Real-World Implementation Flow
|
||||
|
||||
The engine supports robust flows for authenticated apps, including Tenant Vocabulary Overrides and Backend Synchronization.
|
||||
The engine supports robust flows for authenticated apps, including Tenant Vocabulary Overrides and Backend Synchronization.
|
||||
|
||||
### A. The Tenant Override Flow (After Login)
|
||||
|
||||
@@ -239,18 +240,15 @@ export function AuthProvider({ children }) {
|
||||
try {
|
||||
// 1. Fetch tenant-specific overrides from the API
|
||||
const response = await api.get('/v1/tenant/i18n-config');
|
||||
|
||||
// 2. Inject into the engine.
|
||||
|
||||
// 2. Inject into the engine.
|
||||
// `deep: true` ensures only provided keys are overridden.
|
||||
applyTenantOverrides(
|
||||
response.data.namespace,
|
||||
response.data.overrides
|
||||
);
|
||||
applyTenantOverrides(response.data.namespace, response.data.overrides);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch tenant configuration", err);
|
||||
console.error('Failed to fetch tenant configuration', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fetchTenantConfig();
|
||||
}, []);
|
||||
|
||||
@@ -269,7 +267,7 @@ import { api } from '@/api';
|
||||
const handleSwitch = async (newLng: string) => {
|
||||
try {
|
||||
await changeLanguage(newLng, async (lng) => {
|
||||
// The core engine waits for this Promise.
|
||||
// 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 });
|
||||
});
|
||||
@@ -279,6 +277,7 @@ const handleSwitch = async (newLng: string) => {
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
> [!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.
|
||||
|
||||
@@ -289,18 +288,21 @@ const handleSwitch = async (newLng: string) => {
|
||||
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**.
|
||||
|
||||
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...)*
|
||||
_(Authorization: Bearer eyJhbG...)_
|
||||
|
||||
**Expected Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
@@ -313,4 +315,4 @@ If the frontend dictionary has `header.title` and `header.subtitle`, and the bac
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user