# 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<Lang>('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 `<div style={{ display: 'flex', gap: 4 }}>`.

Inserted **left of** the existing theme button:

```tsx
<button className="theme-btn" onClick={toggleLang} title={t.sidebar.toggleLang}>
  {lang === 'en' ? 'EN' : '中'}
</button>
```

- 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
<ThemeProvider>
  <AuthProvider>
    <PageStateProvider>
      <AppRouter />
    </PageStateProvider>
  </AuthProvider>
</ThemeProvider>

// After
<LanguageProvider>
  <ThemeProvider>
    <AuthProvider>
      <PageStateProvider>
        <AppRouter />
      </PageStateProvider>
    </AuthProvider>
  </ThemeProvider>
</LanguageProvider>
```

`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 <button>{t.docs.uploadBtn}</button>;
}
```

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 `<LanguageProvider>` 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
