Files
AIRegulation-DocAnalysis/docs/superpowers/plans/2026-06-08-i18n.md

1928 lines
64 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.