/** * @file index.ts * @description i18next bootstrap for the dashboard client. Loads the JSON locale * files for English (`en`) and Vietnamese (`vi`) lazily — only the active language * (and the `en` fallback) — wires browser language detection, and exports the * configured singleton consumed by `react-i18next` plus `i18nReady`, the promise * the entry point awaits before the first render. * * ## Namespaces * Translations are split by feature area (`dashboard`, `sessions`, `settings`, * etc.) so pages import only the keys they need via `useTranslation("ns")`. * `common` is the default namespace for shared labels and buttons. * * ## Detection order * 1. `localStorage` key `i18nextLng` (user override from Settings) * 2. `navigator.language` fallback * * Unknown regional variants (e.g. `en-US`) map to supported base languages via * `nonExplicitSupportedLngs`. * * `` follows the resolved language on init and on every change. * * @author Nguyễn Ngọc Trí Vĩ */ /* ============================================================================= * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) * ============================================================================= * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. * * ## Design constraints * - Local-first: no telemetry leaves the machine unless the user configures webhooks. * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). * - Destructive flows stay behind explicit confirmation modals and server-side gates. * - Internationalization: user-visible strings belong in i18n JSON, not literals here. * * ## Remote data & SSH * Remote Data Sources let operators aggregate multiple machines. SSH entries describe * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * * ## Internal dependencies * - `./locales//.json` (lazy, via `import.meta.glob`) * * ## Testing pointers * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. * - Server contract changes require `npm run test:server` and OpenAPI sync. * * ## Related docs * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. * - `docs/API.md` — REST reference. * - `skills/file-headers/` — mandatory `@author` header policy. * ============================================================================= */ import i18n, { type BackendModule, type ResourceKey } from "i18next"; import { initReactI18next } from "react-i18next"; import LanguageDetector from "i18next-browser-languagedetector"; // Every locale file is its own lazy chunk (vite.config.ts groups them into one // chunk per language), so a page loads only the language it shows. i18next // reads them through this inline backend: `init` fetches the detected language // (plus the `en` fallback), and `changeLanguage` fetches a new one BEFORE it // switches, so no raw key is ever painted. const localeFiles = import.meta.glob>("./locales/*/*.json", { import: "default", }); const lazyLocales: BackendModule = { type: "backend", init() {}, read(lng, ns, cb) { const load = localeFiles[`./locales/${lng}/${ns}.json`]; if (!load) return cb(null, {}); load().then( (data) => cb(null, data as ResourceKey), (err) => cb(err, false) ); }, }; /** Keep `` on the active language, so a screen reader speaks the * page with the right voice and the browser hyphenates/translates correctly. * Registered before `init`, which fires the first `languageChanged`. */ export function syncDocumentLang(): void { if (typeof document === "undefined") return; document.documentElement.lang = i18n.resolvedLanguage || i18n.language || "en"; } i18n.on("languageChanged", syncDocumentLang); /** Resolves once the detected language's namespaces are loaded; `main.tsx` * awaits it before the first render. */ export const i18nReady = i18n .use(lazyLocales) .use(LanguageDetector) .use(initReactI18next) .init({ supportedLngs: ["en", "vi"], nonExplicitSupportedLngs: true, fallbackLng: "en", ns: [ "common", "nav", "dashboard", "sessions", "activity", "analytics", "workflows", "settings", "kanban", "errors", "updates", "ccConfig", "codexConfig", "run", "alerts", "splash", "lanes", "users", "onboarding", "artifacts", ], defaultNS: "common", interpolation: { escapeValue: false }, detection: { order: ["localStorage", "navigator"], caches: ["localStorage"], lookupLocalStorage: "i18nextLng", }, }); syncDocumentLang(); /** Configured i18next instance — import side effects start {@link init}. */ export default i18n;