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

64 KiB
Raw Permalink Blame History

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 CreateLanguageProvider, 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 Modifyt.overview.*
frontend/src/pages/Perception/PerceptionPage.tsx Modifyt.signals.*
frontend/src/pages/Status/StatusPage.tsx Modifyt.status.*
frontend/src/pages/Docs/DocsPage.tsx Modifyt.docs.*
frontend/src/pages/Compliance/CompliancePage.tsx Modifyt.compliance.*
frontend/src/pages/Compliance/HistoryRail.tsx Modifyt.compliance.*
frontend/src/pages/Compliance/FindingChatDrawer.tsx Modifyt.compliance.*
frontend/src/pages/RagChat/RagChatPage.tsx Modifyt.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

// 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
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
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

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:

export { LanguageProvider, useLanguage } from './LanguageContext';
export type { Lang } from './LanguageContext';

Full updated file:

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
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:

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:

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
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

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
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:

import { useLanguage } from '../../contexts/LanguageContext';

Inside StatusPage() function, add at the top:

const { t } = useLanguage();
  • Step 2: Replace hardcoded strings — Topbar actions

Find:

<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:

<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:

<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:

{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:

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):

detail={health.bm25.available ? undefined : 'Not loaded'}

Replace:

detail={health.bm25.available ? undefined : t.status.serviceNotLoaded}

Find:

detail={health.reranker.enabled ? 'Enabled' : 'Disabled'}

Replace:

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':

{ label: 'Indexed', ...},
{ label: 'Processing / Parsed', ...},
{ label: 'Failed', ...},

Replace:

{ 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:

<span>Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? 'All systems operational' : 'Degraded') : 'Checking…'}</span>

Replace:

<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
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:

import { useLanguage } from '../../contexts/LanguageContext';

Add at the top of PerceptionPage():

const { t } = useLanguage();
  • Step 2: Replace Topbar props

Find:

<Topbar
  title="Regulatory Signals"
  subtitle="ai-powered · live feed"
  actions={
    <>
      <div className="search-box">
        <input
          placeholder="Search signals..."

Replace title, subtitle, placeholder:

<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:

{crawling ? 'Crawling...' : 'Refresh Sources'}

Replace:

{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:

{sig.status === 'ok' ? 'Final' : sig.status === 'warn' ? 'Draft' : sig.status === 'risk' ? 'Urgent' : 'Published'}

Replace:

{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:

{tab === 'overview' ? '概览' : tab === 'obligations' ? '义务条款' : tab === 'assessment' ? '影响评估' : '变更对比'}

Replace:

{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:

crawlStatus: `${d.source}: ${d.stage === 'fetching' ? '抓取中...' : d.stage === 'processing' ? `处理 ${d.fetched} 条...` : `完成 +${d.new} 条`}`,

Replace:

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:

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:

crawlStatus: t.signals.statusError.replace('{message}', typeof d === 'string' ? d : String(d.message))

Find: crawlStatus: `连接失败: ${e instanceof Error ? e.message : String(e)}` Replace:

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
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

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):

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:

{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):

<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:

<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:

<span>{selected.size} {t.docs.filterAll === 'All' ? 'document(s)' : '份文档'} selected</span>

Actually, simpler — since the text is "X documents selected", we can just do:

<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:

<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:

<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:

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):

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
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:

import { useLanguage } from '../../contexts/LanguageContext';

Add at top of CompliancePage():

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:

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:

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

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

import { useLanguage } from '../../contexts/LanguageContext';
// Inside RagChatPage():
const { t } = useLanguage();
  • Step 2: Replace Topbar and export button

Find:

<Topbar
  title="Regulation Q&A"
  actions={
    <button ...>
      <Download size={13} />Export chat
    </button>
  }
/>

Replace:

<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:

<div className="citation-header">
  Sources {citations.length > 0 && `(${citations.length})`}
</div>

Replace:

<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
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
cd frontend && npx tsc --noEmit --skipLibCheck

Expected: 0 errors.

  • Step 2: Manual browser verification

Open http://localhost:5173 and verify all pages:

  1. SidebarEN 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.