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

12 KiB

Internationalisation (i18n) Design — Frontend Chinese/English Toggle

Date: 2026-06-08 Scope: UI framework strings only (nav labels, button labels, status messages, placeholders). Mock data, API-returned content, and domain regulation text are explicitly excluded.


Goals

Add a language toggle button (EN ↔ 中) in the Sidebar footer, immediately left of the existing theme-toggle button, so users can switch the UI between English and Simplified Chinese. Default language is English on every page load; preference is not persisted across sessions.


Architecture

Approach

Custom LanguageContext following the same pattern as the existing ThemeContext. No external library dependencies. Translation strings live in two TypeScript modules (locales/en.ts and locales/zh.ts) that export identical-shape objects.

Layering

src/
├── contexts/
│   └── LanguageContext.tsx    # type Lang, LanguageProvider, useLanguage()
└── locales/
    ├── en.ts                  # English translations (default)
    └── zh.ts                  # Simplified Chinese translations

LanguageProvider wraps the entire app in App.tsx — outermost provider so every component can consume it.

Context interface

type Lang = 'en' | 'zh';

interface LanguageContextValue {
  lang: Lang;
  t: Translations;       // typed translation object
  toggleLang: () => void;
}

useState<Lang>('en') — hardcoded default, no localStorage read on mount.

Translation object shape (both files export Translations)

export interface Translations {
  nav: {
    groupMain: string;
    groupWorkbench: string;
    groupChat: string;
    overview: string;
    signals: string;
    status: string;
    documents: string;
    compliance: string;
    chat: string;
  };
  sidebar: {
    toggleTheme: string;
    toggleLang: string;
    signOut: string;
  };
  overview: {
    eyebrow: string;
    heroTitle: string;
    heroDesc: string;
    openDashboard: string;
    jumpToChat: string;
    sectionHowItWorks: string;
    sectionScreens: string;
    stepUpload: string; stepUploadDesc: string;
    stepProcess: string; stepProcessDesc: string;
    stepMonitor: string; stepMonitorDesc: string;
    stepAnalyze: string; stepAnalyzeDesc: string;
    stepReview: string; stepReviewDesc: string;
    stepChat: string; stepChatDesc: string;
    statScreens: string;
    statFlows: string;
    statReviewPosture: string;
    navLiveHealth: string;
    navRegulatoryChanges: string;
    navUploadDocs: string;
    navComplianceWorkspace: string;
    navChatCited: string;
    navKPIs: string;
  };
  signals: {
    topbarTitle: string;
    topbarSub: string;
    searchPlaceholder: string;
    refreshBtn: string;
    crawlingBtn: string;
    statTotal: string;
    statHigh: string;
    statMedium: string;
    statLast90: string;
    badgeFinal: string;
    badgeDraft: string;
    badgeUrgent: string;
    badgePublished: string;
    emptySelectSignal: string;
    runAnalysis: string;
    stopBtn: string;
    sourceLink: string;
    tabOverview: string;
    tabObligations: string;
    tabImpact: string;
    tabChanges: string;
    cardScopeHeader: string;
    cardObligationsHeader: string;
    obligationsEmpty: string;
    colObligationDesc: string;
    colSubject: string;
    colType: string;
    colDeadline: string;
    deadlinePending: string;
    cardAffectedDocs: string;
    noAffectedDocs: string;
    cardAIImpact: string;
    footerText: string;
    statusConnecting: string;
    statusNoStream: string;
    statusCrawling: string;
    statusProcessing: string;
    statusComplete: string;
    statusUpdateComplete: string;
    statusError: string;
    statusConnFailed: string;
  };
  status: {
    topbarTitle: string;
    searchPlaceholder: string;
    exportBtn: string;
    refreshBtn: string;
    newUploadBtn: string;
    statTotal: string;
    statIndexed: string;
    statFailed: string;
    statChunks: string;
    statCoverage: string;
    cardHealth: string;
    badgeOnline: string;
    badgeError: string;
    badgeDegraded: string;
    badgeUnknown: string;
    healthEndpointError: string;
    serviceEnabled: string;
    serviceDisabled: string;
    serviceNotLoaded: string;
    cardConfig: string;
    labelLLMProvider: string;
    labelLLMModel: string;
    labelEmbeddingModel: string;
    labelEmbeddingDim: string;
    labelMilvusCollection: string;
    labelParserBackend: string;
    labelChunkBackend: string;
    labelParserFailureMode: string;
    configLoadError: string;
    cardBreakdown: string;
    breakdownIndexed: string;
    breakdownProcessing: string;
    breakdownFailed: string;
    cardRuntime: string;
    labelActiveSessions: string;
    labelSessionCapacity: string;
    labelReranker: string;
    labelBM25: string;
    statusActive: string;
    statusUnavailable: string;
    footerAllOk: string;
    footerDegraded: string;
    footerChecking: string;
  };
  docs: {
    topbarTitle: string;
    searchPlaceholder: string;
    refreshBtn: string;
    uploadBtn: string;
    confirmDeleteTitle: string;
    cancelBtn: string;
    deleteBtn: string;
    filterAll: string;
    filterReady: string;
    filterProcessing: string;
    filterFailed: string;
    filterPending: string;
    filterAllTypes: string;
    selectedCount: string;   // '{n} document(s) selected' — use {n} placeholder
    deleteSelected: string;
    colName: string;
    colStatus: string;
    colUploaded: string;
    colChunks: string;
    colSize: string;
    colType: string;
    colActions: string;
    loading: string;
    emptyNoDocuments: string;
    emptyNoMatch: string;
    footerCount: string;     // '{n} of {m} document(s)'
    titleDownload: string;
    titleRetry: string;
    titleDelete: string;
    confirmSingle: string;   // '{name}' placeholder
    confirmBatch: string;    // '{n}' placeholder
  };
  compliance: {
    topbarTitle: string;
    searchPlaceholder: string;
    clearBtn: string;
    exportBtn: string;
    exportJSON: string;
    exportText: string;
    newAnalysisBtn: string;
    statusAnalyzing: string;
    statusComplete: string;
    statusError: string;
    emptyTitle: string;
    emptyDesc: string;
    colRetrieved: string;    // 'Retrieved Regulations {count}'
    retrievingMsg: string;
    defaultRegulation: string;
    matchSuffix: string;
    colParagraph: string;
    extractingMsg: string;
    noTextExtracted: string;
    stagesHeader: string;
    stageExtraction: string;
    stageClauseSplit: string;
    stageRetrieval: string;
    stageSynthesis: string;
    colFindings: string;     // 'Findings {count}'
    gapInProgress: string;
    askAIBtn: string;
    chatBtn: string;
    conclusionHeader: string;
    riskScoreTooltip: string;
    statusCovered: string;
    statusGap: string;
    statusCritical: string;
    statusInfo: string;
    sourceTypePasted: string;
    sourceTypeIndexed: string;
    sourceTypeUploaded: string;
    chatSidebarHeader: string;
    chatThinking: string;
    quickQ1: string;
    quickQ2: string;
    quickQ3: string;
    chatPlaceholder: string;
    sendBtn: string;
    analysisFailed: string;
    exportReportHeader: string;
    exportSectionParagraph: string;
    exportSectionFindings: string;
    exportSectionConclusion: string;
    exportSectionActions: string;
    historyHeader: string;
    downloadReport: string;
    historyEmpty: string;
    historyDeleteConfirm: string;
    drawerClose: string;
    drawerChatEmpty: string;
    drawerSuggestionsHeader: string;
  };
  ragchat: {
    topbarTitle: string;
    exportBtn: string;
    quickPromptsHeader: string;
    inputPlaceholder: string;
    citationsHeader: string;    // 'Sources {count}'
    citationsEmpty: string;
    jumpToSource: string;       // 'Jump to source [N]'
    apiError: string;
    quickPrompt1: string;
    quickPrompt2: string;
    quickPrompt3: string;
    quickPrompt4: string;
  };
}

Language Toggle Button

Location: Sidebar.tsx footer <div style={{ display: 'flex', gap: 4 }}>.

Inserted left of the existing theme button:

<button className="theme-btn" onClick={toggleLang} title={t.sidebar.toggleLang}>
  {lang === 'en' ? 'EN' : '中'}
</button>
  • Reuses existing theme-btn CSS class — no new styles needed.
  • Displays two-character label: EN or .
  • title attribute (tooltip) translates with the rest of the UI.

Translation Files (complete values)

locales/en.ts (English — default)

Key values (representative; full file contains all keys above):

nav: { groupMain: 'Main', groupWorkbench: 'Workbench', groupChat: 'Chat',
       overview: 'Overview', signals: 'Regulatory Signals', status: 'System Status',
       documents: 'Documents', compliance: 'Compliance Analysis', chat: 'Regulation Q&A' },
sidebar: { toggleTheme: 'Toggle theme', toggleLang: 'Switch language', signOut: 'Sign out' },
signals: { refreshBtn: 'Refresh Sources', crawlingBtn: 'Crawling...', ... },
docs: { uploadBtn: 'Upload document', deleteBtn: 'Delete', cancelBtn: 'Cancel', ... },
compliance: { newAnalysisBtn: 'New analysis', analyzeBtn: 'Analyze', sendBtn: 'Send', ... },
ragchat: { exportBtn: 'Export chat', inputPlaceholder: 'Ask about your regulations…', ... },

locales/zh.ts (Simplified Chinese)

Key values:

nav: { groupMain: '主菜单', groupWorkbench: '工作台', groupChat: '对话',
       overview: '概览', signals: '法规信号', status: '系统状态',
       documents: '文档管理', compliance: '合规分析', chat: '法规问答' },
sidebar: { toggleTheme: '切换主题', toggleLang: '切换语言', signOut: '退出' },
signals: { refreshBtn: '刷新数据源', crawlingBtn: '抓取中...', ... },
docs: { uploadBtn: '上传文档', deleteBtn: '删除', cancelBtn: '取消', ... },
compliance: { newAnalysisBtn: '新建分析', analyzeBtn: '开始分析', sendBtn: '发送', ... },
ragchat: { exportBtn: '导出对话', inputPlaceholder: '请输入关于法规的问题…', ... },

App.tsx Provider Wrapping

// Before
<ThemeProvider>
  <AuthProvider>
    <PageStateProvider>
      <AppRouter />
    </PageStateProvider>
  </AuthProvider>
</ThemeProvider>

// After
<LanguageProvider>
  <ThemeProvider>
    <AuthProvider>
      <PageStateProvider>
        <AppRouter />
      </PageStateProvider>
    </AuthProvider>
  </ThemeProvider>
</LanguageProvider>

LanguageProvider is outermost so it is available to all components including the theme toggle itself.


Usage in Components

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

function MyComponent() {
  const { t } = useLanguage();
  return <button>{t.docs.uploadBtn}</button>;
}

No wrapping needed — t is always the correct object for the current language.


Files Changed

File Action
src/contexts/LanguageContext.tsx New — LanguageProvider, useLanguage(), Lang type
src/locales/en.ts New — complete English Translations object
src/locales/zh.ts New — complete Chinese Translations object
src/App.tsx Add <LanguageProvider> wrapper
src/components/layout/Sidebar.tsx Add language toggle button; replace nav group titles and labels with t.nav.*
src/pages/Overview/OverviewPage.tsx Replace all UI strings with t.overview.*
src/pages/Perception/PerceptionPage.tsx Replace all UI strings with t.signals.*
src/pages/Status/StatusPage.tsx Replace all UI strings with t.status.*
src/pages/Docs/DocsPage.tsx Replace all UI strings with t.docs.*
src/pages/Compliance/CompliancePage.tsx Replace all UI strings with t.compliance.*
src/pages/RagChat/RagChatPage.tsx Replace all UI strings with t.ragchat.*
src/pages/Compliance/HistoryRail.tsx Replace UI strings with t.compliance.*
src/pages/Compliance/FindingChatDrawer.tsx Replace UI strings with t.compliance.*

Non-Goals

  • Persistence across sessions (no localStorage for language preference)
  • More than two languages
  • RTL layout support
  • Pluralisation helpers (simple string substitution with {n} placeholders is sufficient — callers replace via t.docs.selectedCount.replace('{n}', String(count)))
  • Translation of API-returned content, mock data, regulation names, or document file names
  • Date/number formatting localisation

Constraints

  • Zero new npm dependencies
  • Follow existing ThemeContext pattern exactly
  • Backend comments/docstrings: English only (no backend changes in this feature)
  • Git commits made by the user, never automated