Files
AgentHub/client/src/i18n/index.ts
T
nntrivi2001 b6b40b0c1b feat(admin): Administration page — Users tab + Server tab (hardware, per-user CPU/memory/home share)
/users becomes /admin (redirected), a tab shell over the existing Users view
and a new Server tab backed by GET /api/host (server/lib/host-stats.js):
hostname/OS/uptime, CPU and load, memory and swap, every real disk with
HDD/SSD, NVIDIA GPUs, and each user's live share from two /proc samples —
attributed by the member's Linux uid (S3) or, for host processes, by the
inherited AGENTHUB_LANE_ID's workspace owner. Read-only, staff-only (/host
in STAFF_ONLY_PREFIXES), comm names only. Members' tmux card moves to the
Server tab.
2026-10-15 16:33:32 +07:00

135 lines
5.0 KiB
TypeScript

/**
* @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`.
*
* `<html lang>` follows the resolved language on init and on every change.
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
* 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/<lng>/<ns>.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<Record<string, unknown>>("./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 `<html lang>` 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",
"admin",
"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;