# 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 `` | | `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({ lang: 'en', t: en, toggleLang: () => {}, }); export function LanguageProvider({ children }: { children: React.ReactNode }) { const [lang, setLang] = useState('en'); const toggleLang = () => setLang(l => (l === 'en' ? 'zh' : 'en')); const t = lang === 'en' ? en : zh; return ( {children} ); } 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 ( ); } 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 (
{title}
{items.map(item => ( `nav-item${isActive ? ' active' : ''}`} > {item.icon} {item.label} {item.badge !== undefined && item.badge > 0 && ( {item.badge} )} ))}
); } /** 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: , label: t.nav.overview }, { to: '/signals', icon: , label: t.nav.signals }, { to: '/status', icon: , label: t.nav.status }, ]; const workbenchNav: NavItem[] = [ { to: '/documents', icon: , label: t.nav.documents }, { to: '/compliance', icon: , label: t.nav.compliance }, ]; const chatNav: NavItem[] = [ { to: '/chat', icon: , label: t.nav.chat }, ]; return ( ); } ``` - [ ] **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: , to: '/status', desc: t.overview.screenStatusDesc }, { id: 'signals', label: t.overview.screenSignals, icon: , to: '/signals', desc: t.overview.screenSignalsDesc }, { id: 'documents', label: t.overview.screenDocuments, icon: , to: '/documents', desc: t.overview.screenDocumentsDesc }, { id: 'compliance', label: t.overview.screenCompliance, icon: , to: '/compliance', desc: t.overview.screenComplianceDesc }, { id: 'chat', label: t.overview.screenChat, icon: , to: '/chat', desc: t.overview.screenChatDesc }, { id: 'analytics', label: t.overview.screenAnalytics, icon: , 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 (

{t.overview.eyebrow}

{t.overview.heroTitle.split('\n').map((line, i) => ( {line}{i === 0 &&
}
))}

{t.overview.heroDesc}

6 {t.overview.statScreens}
5 {t.overview.statFlows}
AI {t.overview.statReviewPosture}

{t.overview.sectionHowItWorks}

{STEPS.map(s => (
{s.num}
{s.label}
{s.desc}
))}

{t.overview.sectionScreens}

{SCREENS.map(s => ( ))}
); } ``` - [ ] **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
} /> ``` Replace with: ```tsx
} /> ``` - [ ] **Step 3: Replace stat labels** Find: ```tsx
Documents total
``` Replace: `
{t.status.statTotal}
` Find: `
Indexed
` (first occurrence in stats grid) Replace: `
{t.status.statIndexed}
` Find: `
Failed
` (in stats grid) Replace: `
{t.status.statFailed}
` Find: `
Vector chunks
` Replace: `
{t.status.statChunks}
` Find: `Index coverage` Replace: `{t.status.statCoverage}` - [ ] **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` ``: 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 (
{name} {detail && {detail}} {status === 'ok' ? t.status.badgeOnline : status === 'error' ? t.status.badgeError : status === 'warn' ? t.status.badgeDegraded : t.status.badgeUnknown}
); } ``` - [ ] **Step 5: Replace card headers, service names, and config labels** Find: `System health` Replace: `{t.status.cardHealth}` 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: `
System configuration
` Replace: `
{t.status.cardConfig}
` 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: `
Could not load config
` Replace: `
{t.status.configLoadError}
` Find: `
Document breakdown
` Replace: `
{t.status.cardBreakdown}
` 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: `Total vector chunks` Replace: `{t.status.totalChunks}` Find: `
Runtime info
` Replace: `
{t.status.cardRuntime}
` 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 Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? 'All systems operational' : 'Degraded') : 'Checking…'} ``` Replace: ```tsx Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? t.status.footerAllOk : t.status.footerDegraded) : t.status.footerChecking} ``` - [ ] **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
Select a signal to run impact analysis

` Replace: `

{t.signals.emptySelectSignal}

` Find: `` Replace: `` Find: `` Replace: `` Find: `Source` Replace: `{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: `
Scope & Summary
` Replace: `
{t.signals.cardScopeHeader}
` Find: `
义务条款
` Replace: `
{t.signals.cardObligationsHeader}
` Find: `

暂无结构化数据。点击右上角"Run impact analysis"触发提取。

` Replace: `

{t.signals.obligationsEmpty}

` Find: `义务描述` Replace: `{t.signals.colObligationDesc}` Find: `主体` Replace: `{t.signals.colSubject}` Find: `类型` Replace: `{t.signals.colType}` Find: `
截止日期
` Replace: `
{t.signals.colDeadline}
` Find: `{d.date || '待定'}` Replace: `{d.date || t.signals.deadlinePending}` Find: `
Affected documents
` Replace: `
{t.signals.cardAffectedDocs}
` Find: `

No affected documents found.

` Replace: `

{t.signals.noAffectedDocs}

` Find: `
AI Impact Analysis
` Replace: `
{t.signals.cardAIImpact}
` - [ ] **Step 9: Replace diff tab headers** Find: `
变更对比
` Replace: `
{t.signals.diffCardHeader}
` Find: `
旧版
` Replace: `
{t.signals.diffOld}
` Find: `
新版
` Replace: `
{t.signals.diffNew}
` - [ ] **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: `Live feed · Regulation Hub` Replace: `{t.signals.footerText}` - [ ] **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 (
e.stopPropagation()} >
{t.docs.confirmDeleteTitle}

{message}

); } ``` 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 => ( ))} ``` Replace the `'All types'` dropdown option display (keep value as English for filter logic): ```tsx ``` Replace `Topbar`: ```tsx
setSearch(e.target.value)} />
} /> ``` - [ ] **Step 4: Replace batch bar and table headers** Find: `{selected.size} document{selected.size > 1 ? 's' : ''} selected` This string has pluralisation logic. Keep it simple — just use the count: ```tsx {selected.size} {t.docs.filterAll === 'All' ? 'document(s)' : '份文档'} selected ``` Actually, simpler — since the text is "X documents selected", we can just do: ```tsx {selected.size} {selected.size > 1 ? (t.docs.colName === 'Document name' ? 'documents' : '份文档') : (t.docs.colName === 'Document name' ? 'document' : '份文档')} selected ``` Cleanest approach — add a helper: Find the batch bar span and replace with: ```tsx {selected.size}{' '} {t.docs.colName === 'Document name' ? `document${selected.size > 1 ? 's' : ''} selected` : `份文档已选择`} ``` Find: `Delete selected` Replace: `{t.docs.deleteSelected}` Replace table header spans: ```tsx {t.docs.colName} {t.docs.colStatus} {t.docs.colUploaded} {t.docs.colChunks} {t.docs.colSize} {t.docs.colType} {t.docs.colActions} ``` 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 Export chat } /> ``` Replace: ```tsx {t.ragchat.exportBtn} } /> ``` - [ ] **Step 3: Replace history pane header** Find: `
Quick prompts
` Replace: `
{t.ragchat.quickPromptsHeader}
` - [ ] **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
Sources {citations.length > 0 && `(${citations.length})`}
``` Replace: ```tsx
{t.ragchat.citationsHeader}{citations.length > 0 && ` (${citations.length})`}
``` 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`.