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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { createLocalStorage } from '@repo/core-storage';
|
||||
|
||||
export const AppStorageKey = {
|
||||
LOCALE: 'app_locale',
|
||||
LANGUAGE: 'app_language',
|
||||
} as const;
|
||||
|
||||
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
||||
|
||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.LOCALE,
|
||||
]);
|
||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([AppStorageKey.LANGUAGE]);
|
||||
|
||||
export const secureStorage = createLocalStorage<AppStorageKeyValue>({
|
||||
plainTextKeys: PLAIN_KEYS
|
||||
plainTextKeys: PLAIN_KEYS,
|
||||
});
|
||||
|
||||
@@ -22,8 +22,8 @@ import App from './app';
|
||||
async function bootstrap() {
|
||||
await setupI18n({
|
||||
storageAdapter: {
|
||||
getLanguage: async () => await secureStorage.getItem<string>(AppStorageKey.LOCALE),
|
||||
setLanguage: async (lng: string) => await secureStorage.setItem(AppStorageKey.LOCALE, lng),
|
||||
getLanguage: async () => await secureStorage.getItem<string>(AppStorageKey.LANGUAGE),
|
||||
setLanguage: async (lng: string) => await secureStorage.setItem(AppStorageKey.LANGUAGE, lng),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
VITE_APP_ENV=development
|
||||
VITE_APP_NAME=development_fe-monorepo-web
|
||||
VITE_APP_VERSION=0.0.1
|
||||
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
VITE_COUCHDB_BASE_URL=http://202.146.229.134:7700
|
||||
|
||||
|
||||
VITE_FARO_URL=https://telemetry.eigen.co.id/collect
|
||||
VITE_OTLP_TRACE_URL=https://telemetry.eigen.co.id/v1/traces
|
||||
|
||||
VITE_COUCHDB_USERNAME=root
|
||||
VITE_COUCHDB_PASSWORD=password
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import { lazy } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
const LoginPage = lazy(() => import('./login'));
|
||||
|
||||
export default function AuthModule() {
|
||||
return <div>auth module</div>;
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/" element={<Navigate to="/auth/login" replace={true} />} />
|
||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function LoginPage() {
|
||||
return <div>Login Page</div>;
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import { FullPageModuleConfig } from '../../domain/constants';
|
||||
import { fullPageDataService } from '../../domain/factories';
|
||||
import { FullPageEntity } from '../../domain/entities';
|
||||
|
||||
import fullPageId from '../locales/id/full-page.json';
|
||||
import fullPageEn from '../locales/en/full-page.json';
|
||||
import fullPageId from '../languages/id/full-page.json';
|
||||
import fullPageEn from '../languages/en/full-page.json';
|
||||
|
||||
const IndexPage = lazy(() => import('../pages/full-page.page.index'));
|
||||
const FormPage = lazy(() => import('../pages/full-page.page.form'));
|
||||
|
||||
+5
-2
@@ -4,6 +4,7 @@ import { Sparkles } from 'lucide-react';
|
||||
import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||
import { FullPageModuleConfig } from '../../domain/constants';
|
||||
import { useState } from 'react';
|
||||
import { FullPageEntity } from '../../domain/entities';
|
||||
|
||||
const cancelSchema = z.object({
|
||||
reason: z.string().min(1, 'Reason is required'),
|
||||
@@ -11,11 +12,13 @@ const cancelSchema = z.object({
|
||||
|
||||
export default function FullPagePageDetail() {
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
const [detailData, setDetailData] = useState();
|
||||
const [detailData, setDetailData] = useState<FullPageEntity>();
|
||||
|
||||
console.log({ detailData });
|
||||
|
||||
return (
|
||||
<EnterpriseDetailPageProvider
|
||||
onDetailLoaded={detailData}
|
||||
onDetailLoaded={setDetailData}
|
||||
cancelModalConfig={{
|
||||
title: t('common:confirmDialog.cancel.title'),
|
||||
schema: cancelSchema,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { SinglePageModuleConfig } from '../../domain/constants';
|
||||
import { singlePageDataService } from '../../domain/factories';
|
||||
import { SinglePageEntity } from '../../domain/entities';
|
||||
|
||||
import singlePageId from '../locales/id/single-page.json';
|
||||
import singlePageEn from '../locales/en/single-page.json';
|
||||
import singlePageId from '../languages/id/single-page.json';
|
||||
import singlePageEn from '../languages/en/single-page.json';
|
||||
|
||||
const IndexPage = lazy(() => import('../pages/single-page.page.index'));
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function HeaderLayout() {
|
||||
onChange={async (val) => {
|
||||
if (!val) return;
|
||||
await i18n.changeLanguage(val);
|
||||
await secureStorage.setItem(AppStorageKey.LOCALE, val);
|
||||
await secureStorage.setItem(AppStorageKey.LANGUAGE, val);
|
||||
}}
|
||||
styles={{
|
||||
input: {
|
||||
|
||||
@@ -7,12 +7,12 @@ import { BookmarkDrawer } from './components/bookmark';
|
||||
import { useHistoryTracker } from './hooks/useHistoryTracker';
|
||||
import { MENU_ITEMS } from './data/menu.data';
|
||||
|
||||
import navEn from './locales/en/nav.json';
|
||||
import navId from './locales/id/nav.json';
|
||||
import historyEn from './locales/en/history.json';
|
||||
import historyId from './locales/id/history.json';
|
||||
import bookmarkEn from './locales/en/bookmark.json';
|
||||
import bookmarkId from './locales/id/bookmark.json';
|
||||
import navEn from './languages/en/nav.json';
|
||||
import navId from './languages/id/nav.json';
|
||||
import historyEn from './languages/en/history.json';
|
||||
import historyId from './languages/id/history.json';
|
||||
import bookmarkEn from './languages/en/bookmark.json';
|
||||
import bookmarkId from './languages/id/bookmark.json';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Namespace Registration (Module Scope)
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useTranslation, registerModuleNamespace } from '@repo/core-i18n';
|
||||
import { Shortcut } from './components/shortcut';
|
||||
import { System } from './components/system';
|
||||
|
||||
import informationId from './locales/id/information.json';
|
||||
import informationEn from './locales/en/information.json';
|
||||
import informationId from './languages/id/information.json';
|
||||
import informationEn from './languages/en/information.json';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
registerModuleNamespace('information', {
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useTranslation, registerModuleNamespace } from '@repo/core-i18n';
|
||||
import { HistorySetting } from './components/history-setting';
|
||||
import { NotificationSetting } from './components/notification-setting';
|
||||
|
||||
import settingId from './locales/id/setting.json';
|
||||
import settingEn from './locales/en/setting.json';
|
||||
import settingId from './languages/id/setting.json';
|
||||
import settingEn from './languages/en/setting.json';
|
||||
|
||||
registerModuleNamespace('setting', {
|
||||
id: settingId,
|
||||
|
||||
@@ -75,7 +75,7 @@ export class BookingTransformer extends BaseDataTransformer<BookingEntity, Booki
|
||||
*/
|
||||
override transformToDTO(entity: BookingEntity): BookingDTO {
|
||||
return {
|
||||
id: entity.id,
|
||||
id: entity.id as string,
|
||||
booking_code: entity.bookingCode,
|
||||
customer_name: entity.customerName,
|
||||
check_in_date: entity.checkInDate,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation, changeLanguage, applyTenantOverrides, i18n } from '@repo/core-i18n';
|
||||
import { secureIndexedDB, AppStorageKey } from '../../../../../../core/storage/local';
|
||||
|
||||
// Decentralized locale imports
|
||||
import bookingId from '../locales/id/booking.json';
|
||||
import bookingEn from '../locales/en/booking.json';
|
||||
// Decentralized languages imports
|
||||
import bookingId from '../languages/id/booking.json';
|
||||
import bookingEn from '../languages/en/booking.json';
|
||||
|
||||
// ─── Shared Styles ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export const LoadingScreen = () => {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex h-screen w-screen items-center justify-center bg-white">
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader color="blue" size="lg" type="dots" />
|
||||
<Loader color="brand" size="lg" type="dots" />
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,19 @@
|
||||
* DO NOT use `import.meta.env` directly in components. Import this `ENV` object instead.
|
||||
*/
|
||||
export const ENV = {
|
||||
API_BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
|
||||
APP_ENV: (import.meta.env.VITE_APP_ENV || 'development') as 'development' | 'staging' | 'production',
|
||||
DEFAULT_LANGUAGE: 'en',
|
||||
|
||||
IS_PROD: import.meta.env.VITE_APP_ENV === 'production',
|
||||
APP_ENV: (import.meta.env.VITE_APP_ENV || 'development') as 'development' | 'staging' | 'production',
|
||||
|
||||
APP_NAME: import.meta.env.VITE_APP_NAME || 'fe-monorepo-web',
|
||||
APP_VERSION: import.meta.env.VITE_APP_VERSION || '0.0.0',
|
||||
|
||||
API_BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
|
||||
|
||||
// Observability
|
||||
FARO_URL: import.meta.env.VITE_FARO_URL || 'https://telemetry.eigen.co.id/collect',
|
||||
OTLP_TRACE_URL: import.meta.env.VITE_OTLP_TRACE_URL || 'https://telemetry.eigen.co.id/v1/traces',
|
||||
|
||||
// CouchDB Connection
|
||||
COUCHDB_BASE_URL: import.meta.env.VITE_COUCHDB_BASE_URL || 'http://localhost:5984',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createHttpClient } from '@repo/core-api/http-client';
|
||||
import { faroAdapter } from '@repo/core-api/observability';
|
||||
import { ENV } from '../environment';
|
||||
import { AppStorageKey, secureStorage } from '../storage/local';
|
||||
|
||||
/**
|
||||
* Enterprise HTTP client for `apps/web`.
|
||||
@@ -15,17 +17,24 @@ import { faroAdapter } from '@repo/core-api/observability';
|
||||
*/
|
||||
export const apiClient = createHttpClient(
|
||||
{
|
||||
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||
baseURL: ENV.API_BASE_URL,
|
||||
timeout: 15000,
|
||||
observability: faroAdapter,
|
||||
},
|
||||
{
|
||||
// ── Auth Interceptor ──────────────────────────────────────────
|
||||
onRequest: async (config) => {
|
||||
config.headers['ex-app-name'] = ENV.APP_NAME;
|
||||
config.headers['ex-app-version'] = ENV.APP_VERSION;
|
||||
config.headers['ex-timezone-offset-minutes'] = new Date().getTimezoneOffset();
|
||||
config.headers['ex-timezone-offset-hours'] = Math.floor(new Date().getTimezoneOffset() / 60);
|
||||
|
||||
const language = await secureStorage.getItem<string>(AppStorageKey.LANGUAGE);
|
||||
config.headers['ex-language'] = language ?? ENV.DEFAULT_LANGUAGE;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
|
||||
|
||||
export const AppStorageKey = {
|
||||
USER_PROFILE: 'user_profile',
|
||||
LOCALE: 'app_locale',
|
||||
LANGUAGE: 'app_language',
|
||||
THEME: 'app_theme',
|
||||
ACCESS_TOKEN: 'access_token',
|
||||
REFRESH_TOKEN: 'refresh_token',
|
||||
@@ -22,7 +22,7 @@ export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
|
||||
]);
|
||||
|
||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.LOCALE,
|
||||
AppStorageKey.LANGUAGE,
|
||||
AppStorageKey.THEME,
|
||||
AppStorageKey.MOCK_DB_COMPANY_A,
|
||||
AppStorageKey.OFFLINE_DRAFT,
|
||||
|
||||
+17
-13
@@ -4,11 +4,11 @@
|
||||
import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||
|
||||
initTelemetry({
|
||||
appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-web',
|
||||
appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0',
|
||||
telemetryUrl: import.meta.env.VITE_FARO_URL || 'https://telemetry.eigen.co.id/collect',
|
||||
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || 'https://telemetry.eigen.co.id/v1/traces',
|
||||
environment: import.meta.env.VITE_ENV || 'development',
|
||||
appName: ENV.APP_NAME,
|
||||
appVersion: ENV.APP_VERSION,
|
||||
telemetryUrl: ENV.FARO_URL,
|
||||
otlpTraceUrl: ENV.OTLP_TRACE_URL,
|
||||
environment: ENV.APP_ENV,
|
||||
});
|
||||
|
||||
// ─── Application Bootstrap ──────────────────────────────────────
|
||||
@@ -17,21 +17,25 @@ import { lazy, StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { setupI18n } from '@repo/core-i18n';
|
||||
import { secureStorage, AppStorageKey } from './core/storage/local';
|
||||
import { ENV } from './core/environment';
|
||||
|
||||
const App = lazy(() => import('./apps'));
|
||||
|
||||
async function bootstrap() {
|
||||
// Initialize i18next and load language from secureStorage
|
||||
await setupI18n({
|
||||
storageAdapter: {
|
||||
getLanguage: async () => {
|
||||
return secureStorage.getItem<string>(AppStorageKey.LOCALE);
|
||||
},
|
||||
setLanguage: async (lng: string) => {
|
||||
await secureStorage.setItem(AppStorageKey.LOCALE, lng);
|
||||
await setupI18n(
|
||||
{
|
||||
storageAdapter: {
|
||||
getLanguage: async () => {
|
||||
return secureStorage.getItem<string>(AppStorageKey.LANGUAGE);
|
||||
},
|
||||
setLanguage: async (lng: string) => {
|
||||
await secureStorage.setItem(AppStorageKey.LANGUAGE, lng);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
ENV.DEFAULT_LANGUAGE,
|
||||
);
|
||||
|
||||
createRoot(document.getElementById('app')!).render(
|
||||
<StrictMode>
|
||||
|
||||
Reference in New Issue
Block a user