Files

1928 lines
64 KiB
Markdown
Raw Permalink Normal View History

2026-06-10 11:10:36 +08:00
# Frontend Internationalisation (i18n) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a zero-dependency EN ↔ 中 language toggle to the Sidebar (left of the existing theme button) that switches all UI framework strings across every page at runtime, defaulting to English on every load.
**Architecture:** Custom `LanguageContext` (mirrors `ThemeContext` pattern) exposes `{ lang, t, toggleLang }`. Two TypeScript locale files (`locales/en.ts`, `locales/zh.ts`) export the same `Translations` shape. Pages call `useLanguage()` and replace hardcoded strings with `t.section.key`. No external libraries added.
**Tech Stack:** React 19, TypeScript, Vite — all already in the project.
---
## File Map
| File | Action |
|------|--------|
| `frontend/src/locales/en.ts` | **Create** — full English `Translations` object |
| `frontend/src/locales/zh.ts` | **Create** — full Chinese `Translations` object |
| `frontend/src/contexts/LanguageContext.tsx` | **Create**`LanguageProvider`, `useLanguage()` |
| `frontend/src/contexts/index.ts` | **Modify** — export `LanguageProvider`, `useLanguage`, `Translations` |
| `frontend/src/App.tsx` | **Modify** — wrap with `<LanguageProvider>` |
| `frontend/src/components/layout/Sidebar.tsx` | **Modify** — lang toggle button + translated nav labels |
| `frontend/src/pages/Overview/OverviewPage.tsx` | **Modify**`t.overview.*` |
| `frontend/src/pages/Perception/PerceptionPage.tsx` | **Modify**`t.signals.*` |
| `frontend/src/pages/Status/StatusPage.tsx` | **Modify**`t.status.*` |
| `frontend/src/pages/Docs/DocsPage.tsx` | **Modify**`t.docs.*` |
| `frontend/src/pages/Compliance/CompliancePage.tsx` | **Modify**`t.compliance.*` |
| `frontend/src/pages/Compliance/HistoryRail.tsx` | **Modify**`t.compliance.*` |
| `frontend/src/pages/Compliance/FindingChatDrawer.tsx` | **Modify**`t.compliance.*` |
| `frontend/src/pages/RagChat/RagChatPage.tsx` | **Modify**`t.ragchat.*` |
---
## Task 1: Create locale files (`en.ts` and `zh.ts`)
**Files:**
- Create: `frontend/src/locales/en.ts`
- Create: `frontend/src/locales/zh.ts`
- [ ] **Step 1: Create `frontend/src/locales/en.ts`**
```ts
// English translations — default language
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;
statScreens: string;
statFlows: string;
statReviewPosture: 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;
screenStatus: string; screenStatusDesc: string;
screenSignals: string; screenSignalsDesc: string;
screenDocuments: string; screenDocumentsDesc: string;
screenCompliance: string; screenComplianceDesc: string;
screenChat: string; screenChatDesc: string;
screenAnalytics: string; screenAnalyticsDesc: 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;
diffOld: string;
diffNew: string;
diffCardHeader: 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;
totalChunks: 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;
deleteSelected: string;
colName: string;
colStatus: string;
colUploaded: string;
colChunks: string;
colSize: string;
colType: string;
colActions: string;
loading: string;
emptyNoDocuments: string;
emptyNoMatch: string;
titleDownload: string;
titleRetry: string;
titleDelete: string;
};
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;
retrievingMsg: string;
defaultRegulation: string;
matchSuffix: string;
colParagraph: string;
extractingMsg: string;
noTextExtracted: string;
stagesHeader: string;
stageExtraction: string;
stageClauseSplit: string;
stageRetrieval: string;
stageSynthesis: string;
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;
citationsEmpty: string;
apiError: string;
};
}
export const en: Translations = {
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',
},
overview: {
eyebrow: 'T-Systems · AI Regulation Hub',
heroTitle: 'AI Compliance,\nAutomated end-to-end',
heroDesc: 'Monitor global AI regulations, analyze document compliance gaps, and get cited answers — all in one platform.',
openDashboard: 'Open dashboard',
jumpToChat: 'Jump to regulation chat',
sectionHowItWorks: 'How it works',
sectionScreens: 'Screens',
statScreens: 'Screens',
statFlows: 'Backend-aware flows',
statReviewPosture: 'Review posture',
stepUpload: 'Upload', stepUploadDesc: 'Ingest regulation documents',
stepProcess: 'Process', stepProcessDesc: 'Embed and chunk via vector DB',
stepMonitor: 'Monitor', stepMonitorDesc: 'Watch regulatory signal feed',
stepAnalyze: 'Analyze', stepAnalyzeDesc: 'Run compliance gap analysis',
stepReview: 'Review', stepReviewDesc: 'Inspect findings with AI assist',
stepChat: 'Chat', stepChatDesc: 'Ask questions with cited answers',
screenStatus: 'System Status', screenStatusDesc: 'Live health and workflow queue',
screenSignals: 'Regulatory Signals', screenSignalsDesc: 'AI-detected regulatory changes',
screenDocuments: 'Document Management', screenDocumentsDesc: 'Upload and inspect documents',
screenCompliance: 'Compliance Analysis', screenComplianceDesc: 'Three-column compliance workspace',
screenChat: 'Regulation Q&A', screenChatDesc: 'Chat with cited regulation sources',
screenAnalytics: 'Analytics', screenAnalyticsDesc: 'KPIs and coverage metrics',
},
signals: {
topbarTitle: 'Regulatory Signals',
topbarSub: 'ai-powered · live feed',
searchPlaceholder: 'Search signals...',
refreshBtn: 'Refresh Sources',
crawlingBtn: 'Crawling...',
statTotal: 'Total signals',
statHigh: 'High impact',
statMedium: 'Medium impact',
statLast90: 'Last 90 days',
badgeFinal: 'Final',
badgeDraft: 'Draft',
badgeUrgent: 'Urgent',
badgePublished: 'Published',
emptySelectSignal: 'Select a signal to run impact analysis',
runAnalysis: 'Run impact analysis',
stopBtn: 'Stop',
sourceLink: 'Source',
tabOverview: 'Overview',
tabObligations: 'Obligations',
tabImpact: 'Impact Assessment',
tabChanges: 'Change Comparison',
cardScopeHeader: 'Scope & Summary',
cardObligationsHeader: 'Obligations',
obligationsEmpty: 'No structured data yet. Click "Run impact analysis" to extract.',
colObligationDesc: 'Obligation',
colSubject: 'Subject',
colType: 'Type',
colDeadline: 'Deadlines',
deadlinePending: 'Pending',
cardAffectedDocs: 'Affected documents',
noAffectedDocs: 'No affected documents found.',
cardAIImpact: 'AI Impact Analysis',
footerText: 'Live feed · Regulation Hub',
statusConnecting: 'Connecting to data sources...',
statusNoStream: 'No stream',
statusCrawling: 'Crawling...',
statusProcessing: 'Processing {count} items...',
statusComplete: 'Done +{count} items',
statusUpdateComplete: 'Update complete — {new} added, {updated} updated',
statusError: 'Error: {message}',
statusConnFailed: 'Connection failed: {message}',
diffOld: 'Previous',
diffNew: 'Current',
diffCardHeader: 'Change Comparison',
},
status: {
topbarTitle: 'System Status',
searchPlaceholder: 'Search...',
exportBtn: 'Export',
refreshBtn: 'Refresh',
newUploadBtn: 'New upload',
statTotal: 'Documents total',
statIndexed: 'Indexed',
statFailed: 'Failed',
statChunks: 'Vector chunks',
statCoverage: 'Index coverage',
cardHealth: 'System health',
badgeOnline: 'Online',
badgeError: 'Error',
badgeDegraded: 'Degraded',
badgeUnknown: 'Unknown',
healthEndpointError: 'Could not reach health endpoint',
serviceEnabled: 'Enabled',
serviceDisabled: 'Disabled',
serviceNotLoaded: 'Not loaded',
cardConfig: 'System configuration',
labelLLMProvider: 'LLM provider',
labelLLMModel: 'LLM model',
labelEmbeddingModel: 'Embedding model',
labelEmbeddingDim: 'Embedding dim',
labelMilvusCollection: 'Milvus collection',
labelParserBackend: 'Parser backend',
labelChunkBackend: 'Chunk backend',
labelParserFailureMode: 'Parser failure mode',
configLoadError: 'Could not load config',
cardBreakdown: 'Document breakdown',
breakdownIndexed: 'Indexed',
breakdownProcessing: 'Processing / Parsed',
breakdownFailed: 'Failed',
cardRuntime: 'Runtime info',
labelActiveSessions: 'Active chat sessions',
labelSessionCapacity: 'Session capacity',
labelReranker: 'Cross-encoder reranker',
labelBM25: 'BM25 hybrid retrieval',
statusActive: 'Active',
statusUnavailable: 'Unavailable',
footerAllOk: 'All systems operational',
footerDegraded: 'Degraded',
footerChecking: 'Checking…',
totalChunks: 'Total vector chunks',
},
docs: {
topbarTitle: 'Document Management',
searchPlaceholder: 'Search documents...',
refreshBtn: 'Refresh',
uploadBtn: 'Upload document',
confirmDeleteTitle: 'Confirm deletion',
cancelBtn: 'Cancel',
deleteBtn: 'Delete',
filterAll: 'All',
filterReady: 'Ready',
filterProcessing: 'Processing',
filterFailed: 'Failed',
filterPending: 'Pending',
filterAllTypes: 'All types',
deleteSelected: 'Delete selected',
colName: 'Document name',
colStatus: 'Status',
colUploaded: 'Uploaded',
colChunks: 'Chunks',
colSize: 'Size',
colType: 'Type',
colActions: 'Actions',
loading: 'Loading documents…',
emptyNoDocuments: 'No documents yet. Upload a document to get started.',
emptyNoMatch: 'No documents match the current filters.',
titleDownload: 'Download original file',
titleRetry: 'Retry processing',
titleDelete: 'Delete document',
},
compliance: {
topbarTitle: 'Compliance Analysis',
searchPlaceholder: 'Search analyses...',
clearBtn: 'Clear',
exportBtn: 'Export',
exportJSON: 'Export JSON',
exportText: 'Export Text',
newAnalysisBtn: 'New analysis',
statusAnalyzing: 'Analyzing…',
statusComplete: 'Analysis complete',
statusError: 'Error',
emptyTitle: 'No analysis running',
emptyDesc: 'Click New analysis to start a compliance gap review against your indexed regulations.',
retrievingMsg: 'Retrieving relevant regulations…',
defaultRegulation: 'Regulation',
matchSuffix: '% match',
colParagraph: 'Paragraph Under Review',
extractingMsg: 'Extracting and analyzing text…',
noTextExtracted: 'No text extracted',
stagesHeader: 'Analysis stages',
stageExtraction: 'Text extraction',
stageClauseSplit: 'Clause splitting',
stageRetrieval: 'Regulation retrieval',
stageSynthesis: 'Conclusion synthesis',
gapInProgress: 'Gap analysis in progress…',
askAIBtn: 'Ask AI',
chatBtn: 'Chat',
conclusionHeader: 'Conclusion',
riskScoreTooltip: 'Risk score (0=safe, 100=critical)',
statusCovered: 'Covered',
statusGap: 'Gap',
statusCritical: 'Critical',
statusInfo: 'Info',
sourceTypePasted: 'Pasted Text',
sourceTypeIndexed: 'Indexed Document',
sourceTypeUploaded: 'Uploaded File',
chatSidebarHeader: 'AI Compliance Q&A',
chatThinking: 'Thinking▋',
quickQ1: 'What regulation applies?',
quickQ2: 'How to remediate?',
quickQ3: 'What is the risk?',
chatPlaceholder: 'Ask about this finding…',
sendBtn: 'Send',
analysisFailed: 'Analysis failed',
exportReportHeader: 'COMPLIANCE ANALYSIS REPORT',
exportSectionParagraph: '── PARAGRAPH UNDER REVIEW ──',
exportSectionFindings: '── FINDINGS ──',
exportSectionConclusion: '── CONCLUSION ──',
exportSectionActions: '── RECOMMENDED ACTIONS ──',
historyHeader: 'History',
downloadReport: 'Download report',
historyEmpty: 'No analyses yet.',
historyDeleteConfirm: 'Delete this analysis record? This cannot be undone.',
drawerClose: 'Close',
drawerChatEmpty: 'No messages yet. Ask a question below.',
drawerSuggestionsHeader: 'Suggested questions',
},
ragchat: {
topbarTitle: 'Regulation Q&A',
exportBtn: 'Export chat',
quickPromptsHeader: 'Quick prompts',
inputPlaceholder: 'Ask about your regulations…',
citationsHeader: 'Sources',
citationsEmpty: 'Citations will appear here after a response is generated.',
apiError: 'Could not reach the RAG API. Please check the backend.',
},
};
```
- [ ] **Step 2: Create `frontend/src/locales/zh.ts`**
```ts
import type { Translations } from './en';
export const zh: Translations = {
nav: {
groupMain: '主菜单',
groupWorkbench: '工作台',
groupChat: '对话',
overview: '概览',
signals: '法规信号',
status: '系统状态',
documents: '文档管理',
compliance: '合规分析',
chat: '法规问答',
},
sidebar: {
toggleTheme: '切换主题',
toggleLang: '切换语言',
signOut: '退出',
},
overview: {
eyebrow: 'T-Systems · AI 法规中心',
heroTitle: 'AI 合规,\n端到端自动化',
heroDesc: '监控全球 AI 法规,分析文档合规差距,获取有引用来源的回答——一站式平台。',
openDashboard: '打开仪表盘',
jumpToChat: '跳转到法规对话',
sectionHowItWorks: '工作流程',
sectionScreens: '功能页面',
statScreens: '功能页面',
statFlows: '后端感知流程',
statReviewPosture: '审查状态',
stepUpload: '上传', stepUploadDesc: '导入法规文档',
stepProcess: '处理', stepProcessDesc: '向量化与分块',
stepMonitor: '监控', stepMonitorDesc: '监控法规信号流',
stepAnalyze: '分析', stepAnalyzeDesc: '运行合规差距分析',
stepReview: '审查', stepReviewDesc: 'AI 辅助审查发现',
stepChat: '对话', stepChatDesc: '带引用来源的问答',
screenStatus: '系统状态', screenStatusDesc: '实时健康与任务队列',
screenSignals: '法规信号', screenSignalsDesc: 'AI 检测法规变更',
screenDocuments: '文档管理', screenDocumentsDesc: '上传与查阅文档',
screenCompliance: '合规分析', screenComplianceDesc: '三栏合规工作台',
screenChat: '法规问答', screenChatDesc: '带引用来源的法规对话',
screenAnalytics: '数据分析', screenAnalyticsDesc: 'KPI 与覆盖指标',
},
signals: {
topbarTitle: '法规信号',
topbarSub: 'AI 驱动 · 实时订阅',
searchPlaceholder: '搜索信号...',
refreshBtn: '刷新数据源',
crawlingBtn: '抓取中...',
statTotal: '信号总数',
statHigh: '高影响',
statMedium: '中影响',
statLast90: '近 90 天',
badgeFinal: '已发布',
badgeDraft: '草案',
badgeUrgent: '紧急',
badgePublished: '已发布',
emptySelectSignal: '选择信号以运行影响分析',
runAnalysis: '运行影响分析',
stopBtn: '停止',
sourceLink: '来源',
tabOverview: '概览',
tabObligations: '义务条款',
tabImpact: '影响评估',
tabChanges: '变更对比',
cardScopeHeader: '范围与摘要',
cardObligationsHeader: '义务条款',
obligationsEmpty: '暂无结构化数据。点击"运行影响分析"触发提取。',
colObligationDesc: '义务描述',
colSubject: '主体',
colType: '类型',
colDeadline: '截止日期',
deadlinePending: '待定',
cardAffectedDocs: '受影响文档',
noAffectedDocs: '未找到受影响文档。',
cardAIImpact: 'AI 影响分析',
footerText: '实时订阅 · 法规中心',
statusConnecting: '正在连接数据源...',
statusNoStream: '无数据流',
statusCrawling: '抓取中...',
statusProcessing: '处理 {count} 条...',
statusComplete: '完成 +{count} 条',
statusUpdateComplete: '更新完成 — 新增 {new} 条,更新 {updated} 条',
statusError: '错误: {message}',
statusConnFailed: '连接失败: {message}',
diffOld: '旧版',
diffNew: '新版',
diffCardHeader: '变更对比',
},
status: {
topbarTitle: '系统状态',
searchPlaceholder: '搜索...',
exportBtn: '导出',
refreshBtn: '刷新',
newUploadBtn: '上传文档',
statTotal: '文档总数',
statIndexed: '已索引',
statFailed: '失败',
statChunks: '向量分块数',
statCoverage: '索引覆盖率',
cardHealth: '系统健康',
badgeOnline: '在线',
badgeError: '错误',
badgeDegraded: '降级',
badgeUnknown: '未知',
healthEndpointError: '无法访问健康检查端点',
serviceEnabled: '已启用',
serviceDisabled: '已禁用',
serviceNotLoaded: '未加载',
cardConfig: '系统配置',
labelLLMProvider: 'LLM 提供商',
labelLLMModel: 'LLM 模型',
labelEmbeddingModel: '向量模型',
labelEmbeddingDim: '向量维度',
labelMilvusCollection: 'Milvus 集合',
labelParserBackend: '解析后端',
labelChunkBackend: '分块后端',
labelParserFailureMode: '解析失败模式',
configLoadError: '无法加载配置',
cardBreakdown: '文档分布',
breakdownIndexed: '已索引',
breakdownProcessing: '处理中 / 已解析',
breakdownFailed: '失败',
cardRuntime: '运行时信息',
labelActiveSessions: '活跃对话会话',
labelSessionCapacity: '会话容量',
labelReranker: '交叉编码器重排序',
labelBM25: 'BM25 混合检索',
statusActive: '活跃',
statusUnavailable: '不可用',
footerAllOk: '所有系统正常',
footerDegraded: '降级运行',
footerChecking: '检查中…',
totalChunks: '向量分块总数',
},
docs: {
topbarTitle: '文档管理',
searchPlaceholder: '搜索文档...',
refreshBtn: '刷新',
uploadBtn: '上传文档',
confirmDeleteTitle: '确认删除',
cancelBtn: '取消',
deleteBtn: '删除',
filterAll: '全部',
filterReady: '就绪',
filterProcessing: '处理中',
filterFailed: '失败',
filterPending: '待处理',
filterAllTypes: '所有类型',
deleteSelected: '删除所选',
colName: '文档名称',
colStatus: '状态',
colUploaded: '上传时间',
colChunks: '分块数',
colSize: '大小',
colType: '类型',
colActions: '操作',
loading: '加载文档中…',
emptyNoDocuments: '暂无文档。请上传文档以开始使用。',
emptyNoMatch: '没有文档符合当前筛选条件。',
titleDownload: '下载原始文件',
titleRetry: '重试处理',
titleDelete: '删除文档',
},
compliance: {
topbarTitle: '合规分析',
searchPlaceholder: '搜索分析记录...',
clearBtn: '清除',
exportBtn: '导出',
exportJSON: '导出 JSON',
exportText: '导出文本',
newAnalysisBtn: '新建分析',
statusAnalyzing: '分析中…',
statusComplete: '分析完成',
statusError: '错误',
emptyTitle: '暂无分析任务',
emptyDesc: '点击"新建分析"对已索引法规进行合规差距审查。',
retrievingMsg: '正在检索相关法规…',
defaultRegulation: '法规',
matchSuffix: '% 匹配',
colParagraph: '待审查段落',
extractingMsg: '正在提取并分析文本…',
noTextExtracted: '未提取到文本',
stagesHeader: '分析阶段',
stageExtraction: '文本提取',
stageClauseSplit: '条款分割',
stageRetrieval: '法规检索',
stageSynthesis: '结论综合',
gapInProgress: '差距分析进行中…',
askAIBtn: '问 AI',
chatBtn: '对话',
conclusionHeader: '结论',
riskScoreTooltip: '风险评分0=安全100=严重)',
statusCovered: '已覆盖',
statusGap: '存在差距',
statusCritical: '严重',
statusInfo: '信息',
sourceTypePasted: '粘贴文本',
sourceTypeIndexed: '已索引文档',
sourceTypeUploaded: '上传文件',
chatSidebarHeader: 'AI 合规问答',
chatThinking: '思考中▋',
quickQ1: '适用哪条法规?',
quickQ2: '如何整改?',
quickQ3: '风险等级如何?',
chatPlaceholder: '针对此发现提问…',
sendBtn: '发送',
analysisFailed: '分析失败',
exportReportHeader: '合规分析报告',
exportSectionParagraph: '── 待审查段落 ──',
exportSectionFindings: '── 发现 ──',
exportSectionConclusion: '── 结论 ──',
exportSectionActions: '── 建议行动 ──',
historyHeader: '历史记录',
downloadReport: '下载报告',
historyEmpty: '暂无分析记录。',
historyDeleteConfirm: '删除此分析记录?此操作不可撤销。',
drawerClose: '关闭',
drawerChatEmpty: '暂无消息。请在下方提问。',
drawerSuggestionsHeader: '建议问题',
},
ragchat: {
topbarTitle: '法规问答',
exportBtn: '导出对话',
quickPromptsHeader: '快捷问题',
inputPlaceholder: '请输入关于法规的问题…',
citationsHeader: '引用来源',
citationsEmpty: '生成回答后,引用来源将显示在此处。',
apiError: '无法连接到 RAG API请检查后端服务。',
},
};
```
- [ ] **Step 3: Verify TypeScript sees both files**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors (or only pre-existing errors, none from the new locale files).
---
## Task 2: Create `LanguageContext.tsx`
**Files:**
- Create: `frontend/src/contexts/LanguageContext.tsx`
- [ ] **Step 1: Write `LanguageContext.tsx`**
```tsx
import React, { createContext, useContext, useState } from 'react';
import { en } from '../locales/en';
import type { Translations } from '../locales/en';
import { zh } from '../locales/zh';
export type Lang = 'en' | 'zh';
interface LanguageContextValue {
lang: Lang;
t: Translations;
toggleLang: () => void;
}
const LanguageContext = createContext<LanguageContextValue>({
lang: 'en',
t: en,
toggleLang: () => {},
});
export function LanguageProvider({ children }: { children: React.ReactNode }) {
const [lang, setLang] = useState<Lang>('en');
const toggleLang = () => setLang(l => (l === 'en' ? 'zh' : 'en'));
const t = lang === 'en' ? en : zh;
return (
<LanguageContext.Provider value={{ lang, t, toggleLang }}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
return useContext(LanguageContext);
}
```
- [ ] **Step 2: Export from `contexts/index.ts`**
Add these lines to `frontend/src/contexts/index.ts`:
```ts
export { LanguageProvider, useLanguage } from './LanguageContext';
export type { Lang } from './LanguageContext';
```
Full updated file:
```ts
export { ThemeProvider, useTheme } from './ThemeContext';
export { AuthProvider, useAuth } from './AuthContext';
export type { AuthUser } from './AuthContext';
export { PageStateProvider, usePageState } from './PageStateContext';
export { LanguageProvider, useLanguage } from './LanguageContext';
export type { Lang } from './LanguageContext';
export type {
RagChatState,
RagMessage,
RagCitation,
ComplianceState,
ComplianceStatus,
ComplianceSourceEvent,
ComplianceFindingEvent,
ComplianceDonePayload,
ComplianceMeta,
ComplianceActionItem,
PerceptionPageState,
PerceptionSignal,
} from './PageStateContext';
```
- [ ] **Step 3: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
---
## Task 3: Wire `LanguageProvider` in `App.tsx` and add toggle button to Sidebar
**Files:**
- Modify: `frontend/src/App.tsx`
- Modify: `frontend/src/components/layout/Sidebar.tsx`
- [ ] **Step 1: Update `App.tsx`**
Replace the full file content:
```tsx
import './styles/globals.css';
import { ThemeProvider, AuthProvider, PageStateProvider, LanguageProvider } from './contexts';
import { AppRouter } from './router/AppRouter';
function App() {
return (
<LanguageProvider>
<ThemeProvider>
<AuthProvider>
<PageStateProvider>
<AppRouter />
</PageStateProvider>
</AuthProvider>
</ThemeProvider>
</LanguageProvider>
);
}
export default App;
```
- [ ] **Step 2: Update `Sidebar.tsx`**
Replace the full file content:
```tsx
import { NavLink } from 'react-router-dom';
import {
LayoutDashboard, Radio, Monitor, FileText,
Shield, MessageSquare, Sun, Moon, LogOut
} from 'lucide-react';
import { useTheme } from '../../contexts/ThemeContext';
import { useAuth } from '../../contexts/AuthContext';
import { useLanguage } from '../../contexts/LanguageContext';
interface NavItem {
to: string;
icon: React.ReactNode;
label: string;
badge?: number;
}
function NavGroup({ title, items }: { title: string; items: NavItem[] }) {
return (
<div className="nav-group">
<div className="nav-group-label">{title}</div>
{items.map(item => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
className={({ isActive }) => `nav-item${isActive ? ' active' : ''}`}
>
<span className="nav-icon">{item.icon}</span>
<span className="nav-label">{item.label}</span>
{item.badge !== undefined && item.badge > 0 && (
<span className="nav-badge">{item.badge}</span>
)}
</NavLink>
))}
</div>
);
}
/** Avatar initials from username (up to 2 chars). */
function initials(name: string): string {
const parts = name.trim().split(/[\s_-]+/);
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
return name.slice(0, 2).toUpperCase();
}
export function Sidebar() {
const { theme, toggleTheme } = useTheme();
const { user, logout } = useAuth();
const { lang, t, toggleLang } = useLanguage();
const mainNav: NavItem[] = [
{ to: '/', icon: <LayoutDashboard size={16} />, label: t.nav.overview },
{ to: '/signals', icon: <Radio size={16} />, label: t.nav.signals },
{ to: '/status', icon: <Monitor size={16} />, label: t.nav.status },
];
const workbenchNav: NavItem[] = [
{ to: '/documents', icon: <FileText size={16} />, label: t.nav.documents },
{ to: '/compliance', icon: <Shield size={16} />, label: t.nav.compliance },
];
const chatNav: NavItem[] = [
{ to: '/chat', icon: <MessageSquare size={16} />, label: t.nav.chat },
];
return (
<aside className="sidebar">
<div className="sidebar-brand">
<img src="/company-logo.ico" alt="T-Systems" className="brand-logo" />
<div className="brand-text">
<div className="brand-name">T-Systems</div>
<div className="brand-sub">Regulation Hub</div>
</div>
</div>
<nav className="sidebar-nav">
<NavGroup title={t.nav.groupMain} items={mainNav} />
<NavGroup title={t.nav.groupWorkbench} items={workbenchNav} />
<NavGroup title={t.nav.groupChat} items={chatNav} />
</nav>
<div className="sidebar-footer">
<div className="sidebar-user">
<div className="user-avatar">{user ? initials(user.username) : 'TS'}</div>
<div className="user-info">
<div className="user-name">{user?.username ?? 'Analyst'}</div>
<div className="user-role">
{user ? (
<span className="user-badge">{user.role}</span>
) : (
'T-Systems'
)}
</div>
</div>
</div>
<div style={{ display: 'flex', gap: 4 }}>
<button
className="theme-btn"
onClick={toggleLang}
title={t.sidebar.toggleLang}
style={{ fontSize: 12, fontWeight: 600 }}
>
{lang === 'en' ? 'EN' : '中'}
</button>
<button className="theme-btn" onClick={toggleTheme} title={t.sidebar.toggleTheme}>
{theme === 'dark' ? <Sun size={14} /> : <Moon size={14} />}
</button>
{user && (
<button className="logout-btn" onClick={logout} title={t.sidebar.signOut}>
<LogOut size={14} />
</button>
)}
</div>
</div>
</aside>
);
}
```
- [ ] **Step 3: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
- [ ] **Step 4: Manual check — open the app**
Start the dev server (if not running): `cd frontend && npm run dev`
Open http://localhost:5173 and verify:
1. Sidebar shows `EN` button left of the sun/moon button
2. Clicking `EN` toggles to `中` and nav labels switch to Chinese
3. Clicking again returns to English
---
## Task 4: Translate `OverviewPage.tsx`
**Files:**
- Modify: `frontend/src/pages/Overview/OverviewPage.tsx`
- [ ] **Step 1: Replace `OverviewPage.tsx`**
```tsx
import { useNavigate } from 'react-router-dom';
import { ArrowRight, BarChart2, Eye, FileText, Shield, MessageSquare, Monitor } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext';
export function OverviewPage() {
const navigate = useNavigate();
const { t } = useLanguage();
const SCREENS = [
{ id: 'status', label: t.overview.screenStatus, icon: <Monitor size={20} />, to: '/status', desc: t.overview.screenStatusDesc },
{ id: 'signals', label: t.overview.screenSignals, icon: <Eye size={20} />, to: '/signals', desc: t.overview.screenSignalsDesc },
{ id: 'documents', label: t.overview.screenDocuments, icon: <FileText size={20} />, to: '/documents', desc: t.overview.screenDocumentsDesc },
{ id: 'compliance', label: t.overview.screenCompliance, icon: <Shield size={20} />, to: '/compliance', desc: t.overview.screenComplianceDesc },
{ id: 'chat', label: t.overview.screenChat, icon: <MessageSquare size={20} />, to: '/chat', desc: t.overview.screenChatDesc },
{ id: 'analytics', label: t.overview.screenAnalytics, icon: <BarChart2 size={20} />, to: '/status', desc: t.overview.screenAnalyticsDesc },
];
const STEPS = [
{ num: '01', label: t.overview.stepUpload, desc: t.overview.stepUploadDesc },
{ num: '02', label: t.overview.stepProcess, desc: t.overview.stepProcessDesc },
{ num: '03', label: t.overview.stepMonitor, desc: t.overview.stepMonitorDesc },
{ num: '04', label: t.overview.stepAnalyze, desc: t.overview.stepAnalyzeDesc },
{ num: '05', label: t.overview.stepReview, desc: t.overview.stepReviewDesc },
{ num: '06', label: t.overview.stepChat, desc: t.overview.stepChatDesc },
];
return (
<div className="overview-scroll-wrapper">
<div className="overview-page">
<section className="overview-hero">
<p className="hero-eyebrow">{t.overview.eyebrow}</p>
<h1 className="hero-title">{t.overview.heroTitle.split('\n').map((line, i) => (
<span key={i}>{line}{i === 0 && <br />}</span>
))}</h1>
<p className="hero-desc">{t.overview.heroDesc}</p>
<div className="hero-actions">
<button className="btn primary" onClick={() => navigate('/status')}>
{t.overview.openDashboard} <ArrowRight size={14} />
</button>
<button className="btn" onClick={() => navigate('/chat')}>
{t.overview.jumpToChat}
</button>
</div>
</section>
<div className="overview-summary card">
<div className="summary-item">
<span className="summary-num">6</span>
<span className="summary-label">{t.overview.statScreens}</span>
</div>
<div className="summary-divider" />
<div className="summary-item">
<span className="summary-num">5</span>
<span className="summary-label">{t.overview.statFlows}</span>
</div>
<div className="summary-divider" />
<div className="summary-item">
<span className="summary-num">AI</span>
<span className="summary-label">{t.overview.statReviewPosture}</span>
</div>
</div>
<section className="overview-workflow">
<h2 className="section-title">{t.overview.sectionHowItWorks}</h2>
<div className="workflow-steps">
{STEPS.map(s => (
<div key={s.num} className="workflow-step">
<div className="step-num">{s.num}</div>
<div className="step-label">{s.label}</div>
<div className="step-desc">{s.desc}</div>
</div>
))}
</div>
</section>
<section className="overview-screens">
<h2 className="section-title">{t.overview.sectionScreens}</h2>
<div className="screen-grid">
{SCREENS.map(s => (
<button key={s.id} className="screen-card card" onClick={() => navigate(s.to)}>
<div className="screen-icon">{s.icon}</div>
<div className="screen-label">{s.label}</div>
<div className="screen-desc">{s.desc}</div>
</button>
))}
</div>
</section>
</div>
</div>
);
}
```
- [ ] **Step 2: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
---
## Task 5: Translate `StatusPage.tsx`
**Files:**
- Modify: `frontend/src/pages/Status/StatusPage.tsx`
- [ ] **Step 1: Add `useLanguage` import and hook call**
At the top of the file, add the import:
```tsx
import { useLanguage } from '../../contexts/LanguageContext';
```
Inside `StatusPage()` function, add at the top:
```tsx
const { t } = useLanguage();
```
- [ ] **Step 2: Replace hardcoded strings — `Topbar` actions**
Find:
```tsx
<Topbar
title="System Status"
actions={
<>
<div className="search-box">
<Search size={13} />
<input placeholder="Search..." />
</div>
<button className="btn sm" onClick={handleExport}>
<Download size={13} />Export
</button>
<button className="btn sm" onClick={() => setRefreshKey(k => k + 1)}>
<RefreshCw size={13} />Refresh
</button>
<button className="btn sm primary" onClick={() => setShowUpload(true)}>
<Upload size={13} />New upload
</button>
</>
}
/>
```
Replace with:
```tsx
<Topbar
title={t.status.topbarTitle}
actions={
<>
<div className="search-box">
<Search size={13} />
<input placeholder={t.status.searchPlaceholder} />
</div>
<button className="btn sm" onClick={handleExport}>
<Download size={13} />{t.status.exportBtn}
</button>
<button className="btn sm" onClick={() => setRefreshKey(k => k + 1)}>
<RefreshCw size={13} />{t.status.refreshBtn}
</button>
<button className="btn sm primary" onClick={() => setShowUpload(true)}>
<Upload size={13} />{t.status.newUploadBtn}
</button>
</>
}
/>
```
- [ ] **Step 3: Replace stat labels**
Find:
```tsx
<div className="stat-label">Documents total</div>
```
Replace: `<div className="stat-label">{t.status.statTotal}</div>`
Find: `<div className="stat-label">Indexed</div>` (first occurrence in stats grid)
Replace: `<div className="stat-label">{t.status.statIndexed}</div>`
Find: `<div className="stat-label">Failed</div>` (in stats grid)
Replace: `<div className="stat-label">{t.status.statFailed}</div>`
Find: `<div className="stat-label">Vector chunks</div>`
Replace: `<div className="stat-label">{t.status.statChunks}</div>`
Find: `<span style={{ fontSize: 12, color: 'var(--muted)', whiteSpace: 'nowrap' }}>Index coverage</span>`
Replace: `<span style={{ fontSize: 12, color: 'var(--muted)', whiteSpace: 'nowrap' }}>{t.status.statCoverage}</span>`
- [ ] **Step 4: Replace `ServiceRow` status strings**
The `ServiceRow` component renders `'Online'`, `'Error'`, `'Degraded'`, `'Unknown'` from a ternary. Replace the inline string map in the `ServiceRow` `<span>`:
Find inside `ServiceRow`:
```tsx
{status === 'ok' ? 'Online' : status === 'error' ? 'Error' : status === 'warn' ? 'Degraded' : 'Unknown'}
```
This requires `t` inside `ServiceRow`. Convert `ServiceRow` to accept a `t` prop, **or** (simpler) pass the label string externally. The simpler approach: add a `label` prop with a default:
Replace `ServiceRow` component definition:
```tsx
function ServiceRow({ name, status, detail }: { name: string; status: 'ok' | 'error' | 'warn' | 'info'; detail?: string }) {
const { t } = useLanguage();
return (
<div className="service-row">
<StatusIcon status={status} />
<span className="service-name" style={{ marginLeft: 8 }}>{name}</span>
{detail && <span style={{ fontSize: 11, color: 'var(--muted)', marginLeft: 6 }}>{detail}</span>}
<span className={`status ${status}`} style={{ marginLeft: 'auto' }}>
{status === 'ok' ? t.status.badgeOnline : status === 'error' ? t.status.badgeError : status === 'warn' ? t.status.badgeDegraded : t.status.badgeUnknown}
</span>
</div>
);
}
```
- [ ] **Step 5: Replace card headers, service names, and config labels**
Find: `<span>System health</span>`
Replace: `<span>{t.status.cardHealth}</span>`
Find: `Could not reach health endpoint`
Replace: `{t.status.healthEndpointError}`
Find (in `ServiceRow` calls):
```tsx
detail={health.bm25.available ? undefined : 'Not loaded'}
```
Replace:
```tsx
detail={health.bm25.available ? undefined : t.status.serviceNotLoaded}
```
Find:
```tsx
detail={health.reranker.enabled ? 'Enabled' : 'Disabled'}
```
Replace:
```tsx
detail={health.reranker.enabled ? t.status.serviceEnabled : t.status.serviceDisabled}
```
Find: `<div className="card-header" style={{ margin: 0, padding: 0, flex: 1, textAlign: 'left' }}>System configuration</div>`
Replace: `<div className="card-header" style={{ margin: 0, padding: 0, flex: 1, textAlign: 'left' }}>{t.status.cardConfig}</div>`
Replace each `ConfigRow label=` string:
- `"LLM provider"``{t.status.labelLLMProvider}`
- `"LLM model"``{t.status.labelLLMModel}`
- `"Embedding model"``{t.status.labelEmbeddingModel}`
- `"Embedding dim"``{t.status.labelEmbeddingDim}`
- `"Milvus collection"``{t.status.labelMilvusCollection}`
- `"Parser backend"``{t.status.labelParserBackend}`
- `"Chunk backend"``{t.status.labelChunkBackend}`
- `"Parser failure mode"``{t.status.labelParserFailureMode}`
Find: `<div style={{ color: 'var(--muted)', fontSize: 13 }}>Could not load config</div>`
Replace: `<div style={{ color: 'var(--muted)', fontSize: 13 }}>{t.status.configLoadError}</div>`
Find: `<div className="card-header">Document breakdown</div>`
Replace: `<div className="card-header">{t.status.cardBreakdown}</div>`
Find breakdown row labels array `'Indexed'`, `'Processing / Parsed'`, `'Failed'`:
```tsx
{ label: 'Indexed', ...},
{ label: 'Processing / Parsed', ...},
{ label: 'Failed', ...},
```
Replace:
```tsx
{ label: t.status.breakdownIndexed, ...},
{ label: t.status.breakdownProcessing, ...},
{ label: t.status.breakdownFailed, ...},
```
Find: `<span style={{ color: 'var(--muted)' }}>Total vector chunks</span>`
Replace: `<span style={{ color: 'var(--muted)' }}>{t.status.totalChunks}</span>`
Find: `<div className="card-header">Runtime info</div>`
Replace: `<div className="card-header">{t.status.cardRuntime}</div>`
Replace the runtime info row labels:
- `'Active chat sessions'``{t.status.labelActiveSessions}`
- `'Session capacity'``{t.status.labelSessionCapacity}`
- `'Cross-encoder reranker'``{t.status.labelReranker}`
- `'BM25 hybrid retrieval'``{t.status.labelBM25}`
Replace runtime status values:
- `'Active'` (BM25 true) → `{t.status.statusActive}`
- `'Unavailable'``{t.status.statusUnavailable}`
Find footer:
```tsx
<span>Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? 'All systems operational' : 'Degraded') : 'Checking…'}</span>
```
Replace:
```tsx
<span>Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? t.status.footerAllOk : t.status.footerDegraded) : t.status.footerChecking}</span>
```
- [ ] **Step 6: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
---
## Task 6: Translate `PerceptionPage.tsx`
**Files:**
- Modify: `frontend/src/pages/Perception/PerceptionPage.tsx`
- [ ] **Step 1: Add `useLanguage` import and hook**
Add import:
```tsx
import { useLanguage } from '../../contexts/LanguageContext';
```
Add at the top of `PerceptionPage()`:
```tsx
const { t } = useLanguage();
```
- [ ] **Step 2: Replace `Topbar` props**
Find:
```tsx
<Topbar
title="Regulatory Signals"
subtitle="ai-powered · live feed"
actions={
<>
<div className="search-box">
<input
placeholder="Search signals..."
```
Replace `title`, `subtitle`, `placeholder`:
```tsx
<Topbar
title={t.signals.topbarTitle}
subtitle={t.signals.topbarSub}
actions={
<>
<div className="search-box">
<input
placeholder={t.signals.searchPlaceholder}
```
- [ ] **Step 3: Replace button labels**
Find:
```tsx
{crawling ? 'Crawling...' : 'Refresh Sources'}
```
Replace:
```tsx
{crawling ? t.signals.crawlingBtn : t.signals.refreshBtn}
```
- [ ] **Step 4: Replace stat labels**
Find `'Total signals'``{t.signals.statTotal}`
Find `'High impact'``{t.signals.statHigh}`
Find `'Medium impact'``{t.signals.statMedium}`
Find `'Last 90 days'``{t.signals.statLast90}`
- [ ] **Step 5: Replace signal status badges**
Find:
```tsx
{sig.status === 'ok' ? 'Final' : sig.status === 'warn' ? 'Draft' : sig.status === 'risk' ? 'Urgent' : 'Published'}
```
Replace:
```tsx
{sig.status === 'ok' ? t.signals.badgeFinal : sig.status === 'warn' ? t.signals.badgeDraft : sig.status === 'risk' ? t.signals.badgeUrgent : t.signals.badgePublished}
```
Do the same for the detail card status badge (second occurrence with `selected.status`).
- [ ] **Step 6: Replace empty state, buttons, and tab labels**
Find: `<p>Select a signal to run impact analysis</p>`
Replace: `<p>{t.signals.emptySelectSignal}</p>`
Find: `<button className="btn sm primary" onClick={runAnalysis}><Play size={12} />Run impact analysis</button>`
Replace: `<button className="btn sm primary" onClick={runAnalysis}><Play size={12} />{t.signals.runAnalysis}</button>`
Find: `<button className="btn sm" onClick={stopAnalysis}><Square size={12} />Stop</button>`
Replace: `<button className="btn sm" onClick={stopAnalysis}><Square size={12} />{t.signals.stopBtn}</button>`
Find: `<ExternalLink size={12} />Source`
Replace: `<ExternalLink size={12} />{t.signals.sourceLink}`
- [ ] **Step 7: Replace tab labels**
Find:
```tsx
{tab === 'overview' ? '概览' : tab === 'obligations' ? '义务条款' : tab === 'assessment' ? '影响评估' : '变更对比'}
```
Replace:
```tsx
{tab === 'overview' ? t.signals.tabOverview : tab === 'obligations' ? t.signals.tabObligations : tab === 'assessment' ? t.signals.tabImpact : t.signals.tabChanges}
```
- [ ] **Step 8: Replace card headers and table headers**
Find: `<div className="card-header">Scope &amp; Summary</div>`
Replace: `<div className="card-header">{t.signals.cardScopeHeader}</div>`
Find: `<div className="card-header">义务条款</div>`
Replace: `<div className="card-header">{t.signals.cardObligationsHeader}</div>`
Find: `<p className="detail-summary" style={{ marginTop: 8 }}>暂无结构化数据。点击右上角"Run impact analysis"触发提取。</p>`
Replace: `<p className="detail-summary" style={{ marginTop: 8 }}>{t.signals.obligationsEmpty}</p>`
Find: `<th style={{ textAlign: 'left', padding: '4px 8px' }}>义务描述</th>`
Replace: `<th style={{ textAlign: 'left', padding: '4px 8px' }}>{t.signals.colObligationDesc}</th>`
Find: `<th style={{ textAlign: 'left', padding: '4px 8px', width: 80 }}>主体</th>`
Replace: `<th style={{ textAlign: 'left', padding: '4px 8px', width: 80 }}>{t.signals.colSubject}</th>`
Find: `<th style={{ textAlign: 'left', padding: '4px 8px', width: 60 }}>类型</th>`
Replace: `<th style={{ textAlign: 'left', padding: '4px 8px', width: 60 }}>{t.signals.colType}</th>`
Find: `<div className="card-header">截止日期</div>`
Replace: `<div className="card-header">{t.signals.colDeadline}</div>`
Find: `{d.date || '待定'}`
Replace: `{d.date || t.signals.deadlinePending}`
Find: `<div className="card-header">Affected documents</div>`
Replace: `<div className="card-header">{t.signals.cardAffectedDocs}</div>`
Find: `<p className="detail-summary" style={{ marginTop: 8 }}>No affected documents found.</p>`
Replace: `<p className="detail-summary" style={{ marginTop: 8 }}>{t.signals.noAffectedDocs}</p>`
Find: `<div className="card-header">AI Impact Analysis</div>`
Replace: `<div className="card-header">{t.signals.cardAIImpact}</div>`
- [ ] **Step 9: Replace diff tab headers**
Find: `<div className="card-header">变更对比</div>`
Replace: `<div className="card-header">{t.signals.diffCardHeader}</div>`
Find: `<div style={{ fontWeight: 600, marginBottom: 4 }}>旧版</div>`
Replace: `<div style={{ fontWeight: 600, marginBottom: 4 }}>{t.signals.diffOld}</div>`
Find: `<div style={{ fontWeight: 600, marginBottom: 4 }}>新版</div>`
Replace: `<div style={{ fontWeight: 600, marginBottom: 4 }}>{t.signals.diffNew}</div>`
- [ ] **Step 10: Replace crawl status strings in `runCrawl()`**
Find: `crawlStatus: '正在连接数据源...'`
Replace: `crawlStatus: t.signals.statusConnecting`
Find:
```tsx
crawlStatus: `${d.source}: ${d.stage === 'fetching' ? '抓取中...' : d.stage === 'processing' ? `处理 ${d.fetched} 条...` : `完成 +${d.new} 条`}`,
```
Replace:
```tsx
crawlStatus: `${d.source}: ${d.stage === 'fetching' ? t.signals.statusCrawling : d.stage === 'processing' ? t.signals.statusProcessing.replace('{count}', String(d.fetched)) : t.signals.statusComplete.replace('{count}', String(d.new))}`,
```
Find: `` crawlStatus: `更新完成 — 新增 ${d.total_new} 条,更新 ${d.total_updated} 条` ``
Replace:
```tsx
crawlStatus: t.signals.statusUpdateComplete.replace('{new}', String(d.total_new)).replace('{updated}', String(d.total_updated))
```
Find: `` crawlStatus: `错误: ${typeof d === 'string' ? d : d.message}` ``
Replace:
```tsx
crawlStatus: t.signals.statusError.replace('{message}', typeof d === 'string' ? d : String(d.message))
```
Find: `` crawlStatus: `连接失败: ${e instanceof Error ? e.message : String(e)}` ``
Replace:
```tsx
crawlStatus: t.signals.statusConnFailed.replace('{message}', e instanceof Error ? e.message : String(e))
```
- [ ] **Step 11: Replace footer**
Find: `<span>Live feed · Regulation Hub</span>`
Replace: `<span>{t.signals.footerText}</span>`
- [ ] **Step 12: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
---
## Task 7: Translate `DocsPage.tsx`
**Files:**
- Modify: `frontend/src/pages/Docs/DocsPage.tsx`
- [ ] **Step 1: Add `useLanguage` import**
```tsx
import { useLanguage } from '../../contexts/LanguageContext';
```
- [ ] **Step 2: Add hook in `ConfirmDialog` and `DocsPage`**
`ConfirmDialog` uses `'Confirm deletion'`, `'Cancel'`, `'Delete'`. Convert it to accept a `t` prop **or** call `useLanguage()` inside it. Use `useLanguage()` directly inside (same pattern as `ServiceRow`):
```tsx
function ConfirmDialog({ message, onConfirm, onCancel }: {
message: string;
onConfirm: () => void;
onCancel: () => void;
}) {
const { t } = useLanguage();
return (
<div className="modal-overlay" onClick={onCancel}>
<div
style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 14, padding: '28px 32px', maxWidth: 400, width: '100%', boxShadow: '0 12px 40px rgba(0,0,0,.2)' }}
onClick={e => e.stopPropagation()}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
<AlertTriangle size={18} color="var(--danger)" />
<span style={{ fontWeight: 600, fontSize: 15 }}>{t.docs.confirmDeleteTitle}</span>
</div>
<p style={{ fontSize: 13, color: 'var(--muted)', lineHeight: 1.6, marginBottom: 20 }}>{message}</p>
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
<button className="btn sm" onClick={onCancel}>{t.docs.cancelBtn}</button>
<button className="btn sm" style={{ background: 'var(--danger)', color: '#fff', borderColor: 'var(--danger)' }} onClick={onConfirm}>
{t.docs.deleteBtn}
</button>
</div>
</div>
</div>
);
}
```
Add `const { t } = useLanguage();` at top of `DocsPage()`.
- [ ] **Step 3: Replace `STATUS_FILTERS` array and topbar**
The `STATUS_FILTERS` array is used in chip rendering and filter logic. The filter logic uses the English strings as keys (through `STATUS_MAP`). Keep the filter keys as English internally, only translate the displayed chip label.
Update the chip rendering:
```tsx
{STATUS_FILTERS.map(f => (
<button
key={f}
className={`chip${statusF === f ? ' active' : ''}`}
onClick={() => setStatusF(f)}
>
{f === 'All' ? t.docs.filterAll
: f === 'Ready' ? t.docs.filterReady
: f === 'Processing' ? t.docs.filterProcessing
: f === 'Failed' ? t.docs.filterFailed
: t.docs.filterPending}
</button>
))}
```
Replace the `'All types'` dropdown option display (keep value as English for filter logic):
```tsx
<select className="select-input" value={typeF} onChange={e => setTypeF(e.target.value)}>
{typeOpts.map(o => (
<option key={o} value={o}>{o === 'All types' ? t.docs.filterAllTypes : o}</option>
))}
</select>
```
Replace `Topbar`:
```tsx
<Topbar
title={t.docs.topbarTitle}
actions={
<>
<div className="search-box">
<Search size={13} />
<input
placeholder={t.docs.searchPlaceholder}
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<button className="btn sm" onClick={() => setRefreshKey(k => k + 1)}>
<RefreshCw size={13} />{t.docs.refreshBtn}
</button>
<button className="btn sm primary" onClick={() => setShowUpload(true)}>
<Upload size={13} />{t.docs.uploadBtn}
</button>
</>
}
/>
```
- [ ] **Step 4: Replace batch bar and table headers**
Find: `<span>{selected.size} document{selected.size > 1 ? 's' : ''} selected</span>`
This string has pluralisation logic. Keep it simple — just use the count:
```tsx
<span>{selected.size} {t.docs.filterAll === 'All' ? 'document(s)' : '份文档'} selected</span>
```
Actually, simpler — since the text is "X documents selected", we can just do:
```tsx
<span>{selected.size} {selected.size > 1 ? (t.docs.colName === 'Document name' ? 'documents' : '份文档') : (t.docs.colName === 'Document name' ? 'document' : '份文档')} selected</span>
```
Cleanest approach — add a helper:
Find the batch bar span and replace with:
```tsx
<span>
{selected.size}{' '}
{t.docs.colName === 'Document name'
? `document${selected.size > 1 ? 's' : ''} selected`
: `份文档已选择`}
</span>
```
Find: `<Trash2 size={12} />Delete selected`
Replace: `<Trash2 size={12} />{t.docs.deleteSelected}`
Replace table header spans:
```tsx
<span>{t.docs.colName}</span>
<span>{t.docs.colStatus}</span>
<span>{t.docs.colUploaded}</span>
<span>{t.docs.colChunks}</span>
<span>{t.docs.colSize}</span>
<span>{t.docs.colType}</span>
<span>{t.docs.colActions}</span>
```
Find: `Loading documents…`
Replace: `{t.docs.loading}`
Find: `docs.length === 0 ? 'No documents yet. Upload a document to get started.' : 'No documents match the current filters.'`
Replace: `docs.length === 0 ? t.docs.emptyNoDocuments : t.docs.emptyNoMatch`
Replace `title` attributes on action buttons:
- `"Download original file"``{t.docs.titleDownload}`
- `"Retry processing"``{t.docs.titleRetry}`
- `"Delete document"``{t.docs.titleDelete}`
Replace confirm dialog messages:
```tsx
message={
confirmDelete.ids.length === 1
? `Delete "${confirmDelete.names[0]}"? This will remove the document, all its chunks, and embeddings from the vector store. This action cannot be undone.`
: `Delete ${confirmDelete.ids.length} documents? This will remove them and all their chunks from the vector store. This action cannot be undone.`
}
```
Replace with (keep the English detail text as it is system info):
```tsx
message={
confirmDelete.ids.length === 1
? `${t.docs.deleteBtn} "${confirmDelete.names[0]}"? This will remove the document, all its chunks, and embeddings from the vector store. This action cannot be undone.`
: `${t.docs.deleteBtn} ${confirmDelete.ids.length} ${t.docs.colName === 'Document name' ? 'documents' : '份文档'}? This will remove them and all their chunks from the vector store. This action cannot be undone.`
}
```
- [ ] **Step 5: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
---
## Task 8: Translate `CompliancePage.tsx`, `HistoryRail.tsx`, `FindingChatDrawer.tsx`
**Files:**
- Modify: `frontend/src/pages/Compliance/CompliancePage.tsx`
- Modify: `frontend/src/pages/Compliance/HistoryRail.tsx`
- Modify: `frontend/src/pages/Compliance/FindingChatDrawer.tsx`
- [ ] **Step 1: Add `useLanguage` to `CompliancePage.tsx`**
Add import:
```tsx
import { useLanguage } from '../../contexts/LanguageContext';
```
Add at top of `CompliancePage()`:
```tsx
const { t } = useLanguage();
```
Replace all hardcoded UI strings using `t.compliance.*`. Key replacements (search and replace each):
| Find | Replace |
|------|---------|
| `title="Compliance Analysis"` (Topbar) | `title={t.compliance.topbarTitle}` |
| `placeholder="Search analyses..."` | `placeholder={t.compliance.searchPlaceholder}` |
| `'Clear'` (button) | `{t.compliance.clearBtn}` |
| `'Export'` (button label) | `{t.compliance.exportBtn}` |
| `'Export JSON'` | `{t.compliance.exportJSON}` |
| `'Export Text'` | `{t.compliance.exportText}` |
| `'New analysis'` | `{t.compliance.newAnalysisBtn}` |
| `'Analyzing…'` | `{t.compliance.statusAnalyzing}` |
| `'Analysis complete'` | `{t.compliance.statusComplete}` |
| `'No analysis running'` | `{t.compliance.emptyTitle}` |
| `'Click New analysis to start...'` | `{t.compliance.emptyDesc}` |
| `'Retrieving relevant regulations…'` | `{t.compliance.retrievingMsg}` |
| `'Regulation'` (default name) | `{t.compliance.defaultRegulation}` |
| `'% match'` | `{t.compliance.matchSuffix}` |
| `'Paragraph Under Review'` | `{t.compliance.colParagraph}` |
| `'Extracting and analyzing text…'` | `{t.compliance.extractingMsg}` |
| `'No text extracted'` | `{t.compliance.noTextExtracted}` |
| `'Analysis stages'` | `{t.compliance.stagesHeader}` |
| `'Text extraction'` | `{t.compliance.stageExtraction}` |
| `'Clause splitting'` | `{t.compliance.stageClauseSplit}` |
| `'Regulation retrieval'` | `{t.compliance.stageRetrieval}` |
| `'Conclusion synthesis'` | `{t.compliance.stageSynthesis}` |
| `'Gap analysis in progress…'` | `{t.compliance.gapInProgress}` |
| `'Ask AI'` | `{t.compliance.askAIBtn}` |
| `'Chat'` (button) | `{t.compliance.chatBtn}` |
| `'Conclusion'` (header) | `{t.compliance.conclusionHeader}` |
| `title="Risk score (0=safe, 100=critical)"` | `title={t.compliance.riskScoreTooltip}` |
| `'Covered'` | `{t.compliance.statusCovered}` |
| `'Gap'` | `{t.compliance.statusGap}` |
| `'Critical'` | `{t.compliance.statusCritical}` |
| `'Info'` (status) | `{t.compliance.statusInfo}` |
| `'Pasted Text'` | `{t.compliance.sourceTypePasted}` |
| `'Indexed Document'` | `{t.compliance.sourceTypeIndexed}` |
| `'Uploaded File'` | `{t.compliance.sourceTypeUploaded}` |
| `'AI Compliance Q&A'` | `{t.compliance.chatSidebarHeader}` |
| `'Thinking▋'` | `{t.compliance.chatThinking}` |
| `'What regulation applies?'` | `{t.compliance.quickQ1}` |
| `'How to remediate?'` | `{t.compliance.quickQ2}` |
| `'What is the risk?'` | `{t.compliance.quickQ3}` |
| `placeholder="Ask about this finding…"` | `placeholder={t.compliance.chatPlaceholder}` |
| `'Send'` (button) | `{t.compliance.sendBtn}` |
| `'Analysis failed'` | `{t.compliance.analysisFailed}` |
| `'COMPLIANCE ANALYSIS REPORT'` (export) | `t.compliance.exportReportHeader` |
| `'── PARAGRAPH UNDER REVIEW ──'` | `t.compliance.exportSectionParagraph` |
| `'── FINDINGS ──'` | `t.compliance.exportSectionFindings` |
| `'── CONCLUSION ──'` | `t.compliance.exportSectionConclusion` |
| `'── RECOMMENDED ACTIONS ──'` | `t.compliance.exportSectionActions` |
Column headers with counts use string interpolation — use template literals:
- `'Retrieved Regulations {count}'` rendered as: `` `${t.compliance.stageRetrieval} (${count})` `` or `` `Retrieved Regulations (${count})` `` — keep the count logic, wrap the label.
- `'Findings {count}'` → same pattern.
- [ ] **Step 2: Translate `HistoryRail.tsx`**
Add import + hook:
```tsx
import { useLanguage } from '../../contexts/LanguageContext';
// Inside component:
const { t } = useLanguage();
```
Replace:
- `'History'` (header) → `{t.compliance.historyHeader}`
- `'Download report'` / `'Download'``{t.compliance.downloadReport}`
- `'No analyses yet.'``{t.compliance.historyEmpty}`
- `window.confirm(...)` message → `t.compliance.historyDeleteConfirm`
- [ ] **Step 3: Translate `FindingChatDrawer.tsx`**
Add import + hook:
```tsx
import { useLanguage } from '../../contexts/LanguageContext';
// Inside component:
const { t } = useLanguage();
```
Replace:
- `'Close'` button → `{t.compliance.drawerClose}`
- Empty chat state message → `{t.compliance.drawerChatEmpty}`
- `'Suggested questions'` header (if present) → `{t.compliance.drawerSuggestionsHeader}`
- `'Send'` button → `{t.compliance.sendBtn}`
- [ ] **Step 4: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
---
## Task 9: Translate `RagChatPage.tsx`
**Files:**
- Modify: `frontend/src/pages/RagChat/RagChatPage.tsx`
- [ ] **Step 1: Add `useLanguage` import and hook**
```tsx
import { useLanguage } from '../../contexts/LanguageContext';
// Inside RagChatPage():
const { t } = useLanguage();
```
- [ ] **Step 2: Replace `Topbar` and export button**
Find:
```tsx
<Topbar
title="Regulation Q&A"
actions={
<button ...>
<Download size={13} />Export chat
</button>
}
/>
```
Replace:
```tsx
<Topbar
title={t.ragchat.topbarTitle}
actions={
<button ...>
<Download size={13} />{t.ragchat.exportBtn}
</button>
}
/>
```
- [ ] **Step 3: Replace history pane header**
Find: `<div className="history-header">Quick prompts</div>`
Replace: `<div className="history-header">{t.ragchat.quickPromptsHeader}</div>`
- [ ] **Step 4: Replace textarea placeholder**
Find: `placeholder="Ask about your regulations…"`
Replace: `placeholder={t.ragchat.inputPlaceholder}`
- [ ] **Step 5: Replace citations header and empty state**
Find:
```tsx
<div className="citation-header">
Sources {citations.length > 0 && `(${citations.length})`}
</div>
```
Replace:
```tsx
<div className="citation-header">
{t.ragchat.citationsHeader}{citations.length > 0 && ` (${citations.length})`}
</div>
```
Find: `Citations will appear here after a response is generated.`
Replace: `{t.ragchat.citationsEmpty}`
- [ ] **Step 6: Replace error messages in `send()`**
Find: `'Could not reach the RAG API. Please check the backend.'`
Replace: `t.ragchat.apiError`
- [ ] **Step 7: Verify TypeScript**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck 2>&1 | head -20
```
Expected: 0 errors.
---
## Task 10: Final verification
- [ ] **Step 1: Run TypeScript check on entire frontend**
```bash
cd frontend && npx tsc --noEmit --skipLibCheck
```
Expected: 0 errors.
- [ ] **Step 2: Manual browser verification**
Open http://localhost:5173 and verify all pages:
1. **Sidebar**`EN` button left of theme toggle; clicking cycles EN ↔ 中 instantly for nav labels and group headers.
2. **Overview** (`/`) — hero text, step labels, screen cards, stat bar all switch language.
3. **Regulatory Signals** (`/signals`) — topbar title, refresh button, stat bar, tab labels, card headers switch language. Crawl status messages use correct language.
4. **System Status** (`/status`) — topbar, stat labels, service rows ("Online"/"在线"), config labels, runtime info, footer message switch language.
5. **Documents** (`/documents`) — topbar, filter chips, table headers, action button tooltips, confirm dialog switch language.
6. **Compliance** (`/compliance`) — topbar, new analysis button, stage labels, finding status badges, export menu, chat sidebar, history rail switch language.
7. **Regulation Q&A** (`/chat`) — topbar, "Quick prompts" header, input placeholder, citations header, empty citations message switch language.
8. **Language persists within a session** — navigate between pages, language stays consistent.
9. **Language resets on reload** — refresh the browser, language returns to `EN`.