update for 1. 优化 2.中英切换
This commit is contained in:
421
docs/superpowers/specs/2026-06-08-i18n-design.md
Normal file
421
docs/superpowers/specs/2026-06-08-i18n-design.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user