# Internationalisation (i18n) Design — Frontend Chinese/English Toggle **Date:** 2026-06-08 **Scope:** UI framework strings only (nav labels, button labels, status messages, placeholders). Mock data, API-returned content, and domain regulation text are explicitly excluded. --- ## Goals Add a language toggle button (EN ↔ 中) in the Sidebar footer, immediately left of the existing theme-toggle button, so users can switch the UI between English and Simplified Chinese. Default language is English on every page load; preference is not persisted across sessions. --- ## Architecture ### Approach Custom `LanguageContext` following the same pattern as the existing `ThemeContext`. No external library dependencies. Translation strings live in two TypeScript modules (`locales/en.ts` and `locales/zh.ts`) that export identical-shape objects. ### Layering ``` src/ ├── contexts/ │ └── LanguageContext.tsx # type Lang, LanguageProvider, useLanguage() └── locales/ ├── en.ts # English translations (default) └── zh.ts # Simplified Chinese translations ``` `LanguageProvider` wraps the entire app in `App.tsx` — outermost provider so every component can consume it. ### Context interface ```ts type Lang = 'en' | 'zh'; interface LanguageContextValue { lang: Lang; t: Translations; // typed translation object toggleLang: () => void; } ``` `useState('en')` — hardcoded default, no localStorage read on mount. ### Translation object shape (both files export `Translations`) ```ts export interface Translations { nav: { groupMain: string; groupWorkbench: string; groupChat: string; overview: string; signals: string; status: string; documents: string; compliance: string; chat: string; }; sidebar: { toggleTheme: string; toggleLang: string; signOut: string; }; overview: { eyebrow: string; heroTitle: string; heroDesc: string; openDashboard: string; jumpToChat: string; sectionHowItWorks: string; sectionScreens: string; stepUpload: string; stepUploadDesc: string; stepProcess: string; stepProcessDesc: string; stepMonitor: string; stepMonitorDesc: string; stepAnalyze: string; stepAnalyzeDesc: string; stepReview: string; stepReviewDesc: string; stepChat: string; stepChatDesc: string; statScreens: string; statFlows: string; statReviewPosture: string; navLiveHealth: string; navRegulatoryChanges: string; navUploadDocs: string; navComplianceWorkspace: string; navChatCited: string; navKPIs: string; }; signals: { topbarTitle: string; topbarSub: string; searchPlaceholder: string; refreshBtn: string; crawlingBtn: string; statTotal: string; statHigh: string; statMedium: string; statLast90: string; badgeFinal: string; badgeDraft: string; badgeUrgent: string; badgePublished: string; emptySelectSignal: string; runAnalysis: string; stopBtn: string; sourceLink: string; tabOverview: string; tabObligations: string; tabImpact: string; tabChanges: string; cardScopeHeader: string; cardObligationsHeader: string; obligationsEmpty: string; colObligationDesc: string; colSubject: string; colType: string; colDeadline: string; deadlinePending: string; cardAffectedDocs: string; noAffectedDocs: string; cardAIImpact: string; footerText: string; statusConnecting: string; statusNoStream: string; statusCrawling: string; statusProcessing: string; statusComplete: string; statusUpdateComplete: string; statusError: string; statusConnFailed: string; }; status: { topbarTitle: string; searchPlaceholder: string; exportBtn: string; refreshBtn: string; newUploadBtn: string; statTotal: string; statIndexed: string; statFailed: string; statChunks: string; statCoverage: string; cardHealth: string; badgeOnline: string; badgeError: string; badgeDegraded: string; badgeUnknown: string; healthEndpointError: string; serviceEnabled: string; serviceDisabled: string; serviceNotLoaded: string; cardConfig: string; labelLLMProvider: string; labelLLMModel: string; labelEmbeddingModel: string; labelEmbeddingDim: string; labelMilvusCollection: string; labelParserBackend: string; labelChunkBackend: string; labelParserFailureMode: string; configLoadError: string; cardBreakdown: string; breakdownIndexed: string; breakdownProcessing: string; breakdownFailed: string; cardRuntime: string; labelActiveSessions: string; labelSessionCapacity: string; labelReranker: string; labelBM25: string; statusActive: string; statusUnavailable: string; footerAllOk: string; footerDegraded: string; footerChecking: string; }; docs: { topbarTitle: string; searchPlaceholder: string; refreshBtn: string; uploadBtn: string; confirmDeleteTitle: string; cancelBtn: string; deleteBtn: string; filterAll: string; filterReady: string; filterProcessing: string; filterFailed: string; filterPending: string; filterAllTypes: string; selectedCount: string; // '{n} document(s) selected' — use {n} placeholder deleteSelected: string; colName: string; colStatus: string; colUploaded: string; colChunks: string; colSize: string; colType: string; colActions: string; loading: string; emptyNoDocuments: string; emptyNoMatch: string; footerCount: string; // '{n} of {m} document(s)' titleDownload: string; titleRetry: string; titleDelete: string; confirmSingle: string; // '{name}' placeholder confirmBatch: string; // '{n}' placeholder }; compliance: { topbarTitle: string; searchPlaceholder: string; clearBtn: string; exportBtn: string; exportJSON: string; exportText: string; newAnalysisBtn: string; statusAnalyzing: string; statusComplete: string; statusError: string; emptyTitle: string; emptyDesc: string; colRetrieved: string; // 'Retrieved Regulations {count}' retrievingMsg: string; defaultRegulation: string; matchSuffix: string; colParagraph: string; extractingMsg: string; noTextExtracted: string; stagesHeader: string; stageExtraction: string; stageClauseSplit: string; stageRetrieval: string; stageSynthesis: string; colFindings: string; // 'Findings {count}' gapInProgress: string; askAIBtn: string; chatBtn: string; conclusionHeader: string; riskScoreTooltip: string; statusCovered: string; statusGap: string; statusCritical: string; statusInfo: string; sourceTypePasted: string; sourceTypeIndexed: string; sourceTypeUploaded: string; chatSidebarHeader: string; chatThinking: string; quickQ1: string; quickQ2: string; quickQ3: string; chatPlaceholder: string; sendBtn: string; analysisFailed: string; exportReportHeader: string; exportSectionParagraph: string; exportSectionFindings: string; exportSectionConclusion: string; exportSectionActions: string; historyHeader: string; downloadReport: string; historyEmpty: string; historyDeleteConfirm: string; drawerClose: string; drawerChatEmpty: string; drawerSuggestionsHeader: string; }; ragchat: { topbarTitle: string; exportBtn: string; quickPromptsHeader: string; inputPlaceholder: string; citationsHeader: string; // 'Sources {count}' citationsEmpty: string; jumpToSource: string; // 'Jump to source [N]' apiError: string; quickPrompt1: string; quickPrompt2: string; quickPrompt3: string; quickPrompt4: string; }; } ``` --- ## Language Toggle Button Location: `Sidebar.tsx` footer `
`. Inserted **left of** the existing theme button: ```tsx ``` - Reuses existing `theme-btn` CSS class — no new styles needed. - Displays two-character label: `EN` or `中`. - `title` attribute (tooltip) translates with the rest of the UI. --- ## Translation Files (complete values) ### `locales/en.ts` (English — default) Key values (representative; full file contains all keys above): ```ts nav: { groupMain: 'Main', groupWorkbench: 'Workbench', groupChat: 'Chat', overview: 'Overview', signals: 'Regulatory Signals', status: 'System Status', documents: 'Documents', compliance: 'Compliance Analysis', chat: 'Regulation Q&A' }, sidebar: { toggleTheme: 'Toggle theme', toggleLang: 'Switch language', signOut: 'Sign out' }, signals: { refreshBtn: 'Refresh Sources', crawlingBtn: 'Crawling...', ... }, docs: { uploadBtn: 'Upload document', deleteBtn: 'Delete', cancelBtn: 'Cancel', ... }, compliance: { newAnalysisBtn: 'New analysis', analyzeBtn: 'Analyze', sendBtn: 'Send', ... }, ragchat: { exportBtn: 'Export chat', inputPlaceholder: 'Ask about your regulations…', ... }, ``` ### `locales/zh.ts` (Simplified Chinese) Key values: ```ts nav: { groupMain: '主菜单', groupWorkbench: '工作台', groupChat: '对话', overview: '概览', signals: '法规信号', status: '系统状态', documents: '文档管理', compliance: '合规分析', chat: '法规问答' }, sidebar: { toggleTheme: '切换主题', toggleLang: '切换语言', signOut: '退出' }, signals: { refreshBtn: '刷新数据源', crawlingBtn: '抓取中...', ... }, docs: { uploadBtn: '上传文档', deleteBtn: '删除', cancelBtn: '取消', ... }, compliance: { newAnalysisBtn: '新建分析', analyzeBtn: '开始分析', sendBtn: '发送', ... }, ragchat: { exportBtn: '导出对话', inputPlaceholder: '请输入关于法规的问题…', ... }, ``` --- ## App.tsx Provider Wrapping ```tsx // Before // After ``` `LanguageProvider` is outermost so it is available to all components including the theme toggle itself. --- ## Usage in Components ```tsx import { useLanguage } from '../../contexts/LanguageContext'; function MyComponent() { const { t } = useLanguage(); return ; } ``` No wrapping needed — `t` is always the correct object for the current language. --- ## Files Changed | File | Action | |------|--------| | `src/contexts/LanguageContext.tsx` | New — `LanguageProvider`, `useLanguage()`, `Lang` type | | `src/locales/en.ts` | New — complete English `Translations` object | | `src/locales/zh.ts` | New — complete Chinese `Translations` object | | `src/App.tsx` | Add `` wrapper | | `src/components/layout/Sidebar.tsx` | Add language toggle button; replace nav group titles and labels with `t.nav.*` | | `src/pages/Overview/OverviewPage.tsx` | Replace all UI strings with `t.overview.*` | | `src/pages/Perception/PerceptionPage.tsx` | Replace all UI strings with `t.signals.*` | | `src/pages/Status/StatusPage.tsx` | Replace all UI strings with `t.status.*` | | `src/pages/Docs/DocsPage.tsx` | Replace all UI strings with `t.docs.*` | | `src/pages/Compliance/CompliancePage.tsx` | Replace all UI strings with `t.compliance.*` | | `src/pages/RagChat/RagChatPage.tsx` | Replace all UI strings with `t.ragchat.*` | | `src/pages/Compliance/HistoryRail.tsx` | Replace UI strings with `t.compliance.*` | | `src/pages/Compliance/FindingChatDrawer.tsx` | Replace UI strings with `t.compliance.*` | --- ## Non-Goals - Persistence across sessions (no localStorage for language preference) - More than two languages - RTL layout support - Pluralisation helpers (simple string substitution with `{n}` placeholders is sufficient — callers replace via `t.docs.selectedCount.replace('{n}', String(count))`) - Translation of API-returned content, mock data, regulation names, or document file names - Date/number formatting localisation --- ## Constraints - Zero new npm dependencies - Follow existing `ThemeContext` pattern exactly - Backend comments/docstrings: English only (no backend changes in this feature) - Git commits made by the user, never automated