422 lines
12 KiB
Markdown
422 lines
12 KiB
Markdown
# 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
|