refactor: decouple core-storage services from static keys and update i18n setup to use configurable storage adapters
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
|
||||
[← Back to Root](../../README.md)
|
||||
|
||||
A highly decoupled, type-safe internationalization engine for the Eigen Monorepo.
|
||||
A highly decoupled, type-safe internationalization engine for the monorepo.
|
||||
|
||||
It uses a **Hybrid Namespace Strategy**:
|
||||
1. **Centralized Engine**: Setup, local persistence (`@repo/core-storage`), and global words (`common`).
|
||||
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 and networking decisions entirely to the consuming applications.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -65,18 +65,29 @@ graph TD
|
||||
|
||||
## 1. App-Level Setup (Bootstrap)
|
||||
|
||||
Initialize the engine *before* your React application mounts to prevent UI flashing.
|
||||
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 storage & inits i18next
|
||||
await setupI18n();
|
||||
// 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(
|
||||
<StrictMode><App /></StrictMode>,
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-storage": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"i18next": "^24.2.2",
|
||||
"react-i18next": "^15.4.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import i18n from 'i18next';
|
||||
import { secureStorage, StorageKey } from '@repo/core-storage';
|
||||
import { globalStorageAdapter } from './setup';
|
||||
|
||||
/**
|
||||
* Changes the active language, saves the preference locally, and optionally syncs with the backend.
|
||||
@@ -16,7 +16,9 @@ export async function changeLanguage(
|
||||
if (prevLng === newLng) return;
|
||||
|
||||
// 1. Update local storage & i18next optimistically
|
||||
await secureStorage.setItem(StorageKey.LOCALE, newLng);
|
||||
if (globalStorageAdapter) {
|
||||
await globalStorageAdapter.setLanguage(newLng);
|
||||
}
|
||||
await i18n.changeLanguage(newLng);
|
||||
|
||||
// 2. Trigger optional backend sync
|
||||
@@ -26,7 +28,9 @@ export async function changeLanguage(
|
||||
} catch (error) {
|
||||
console.error('[i18n] Backend sync failed, rolling back language', error);
|
||||
// Rollback on failure
|
||||
await secureStorage.setItem(StorageKey.LOCALE, prevLng);
|
||||
if (globalStorageAdapter) {
|
||||
await globalStorageAdapter.setLanguage(prevLng);
|
||||
}
|
||||
await i18n.changeLanguage(prevLng);
|
||||
throw error; // Rethrow so the caller can show an error toast
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import { secureStorage, StorageKey } from '@repo/core-storage';
|
||||
import commonEn from './locales/en/common.json';
|
||||
import commonId from './locales/id/common.json';
|
||||
|
||||
@@ -14,18 +13,33 @@ export const resources = {
|
||||
id: { common: commonId.common },
|
||||
} as const;
|
||||
|
||||
export interface I18nStorageAdapter {
|
||||
getLanguage(): Promise<string | null>;
|
||||
setLanguage(lng: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface I18nConfig {
|
||||
storageAdapter?: I18nStorageAdapter;
|
||||
}
|
||||
|
||||
// Store the adapter module-wide so manager.ts can access it
|
||||
export let globalStorageAdapter: I18nStorageAdapter | undefined;
|
||||
|
||||
/**
|
||||
* Bootstraps the central i18n engine.
|
||||
*
|
||||
* This reads the preferred locale from secureStorage and initializes
|
||||
* i18next synchronously before React renders.
|
||||
* It accepts an optional storage adapter to read the initial language.
|
||||
*/
|
||||
export async function setupI18n(): Promise<void> {
|
||||
export async function setupI18n(config: I18nConfig = {}): Promise<void> {
|
||||
globalStorageAdapter = config.storageAdapter;
|
||||
|
||||
let initialLng = DEFAULT_LANGUAGE;
|
||||
try {
|
||||
const storedLng = await secureStorage.getItem<string>(StorageKey.LOCALE);
|
||||
if (storedLng && SUPPORTED_LANGUAGES.includes(storedLng as SupportedLanguage)) {
|
||||
initialLng = storedLng;
|
||||
if (globalStorageAdapter) {
|
||||
const storedLng = await globalStorageAdapter.getLanguage();
|
||||
if (storedLng && SUPPORTED_LANGUAGES.includes(storedLng as SupportedLanguage)) {
|
||||
initialLng = storedLng;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[i18n] Failed to read locale from storage', err);
|
||||
|
||||
Reference in New Issue
Block a user