diff --git a/BitFun-Installer/src/i18n/locales/zh-TW.json b/BitFun-Installer/src/i18n/locales/zh-TW.json index 21296d385..e09da2242 100644 --- a/BitFun-Installer/src/i18n/locales/zh-TW.json +++ b/BitFun-Installer/src/i18n/locales/zh-TW.json @@ -5,13 +5,13 @@ }, "installPath": { "notAbsolute": "安裝路徑必須是絕對路徑。", - "filesystemRoot": "不能安裝到磁盤根目錄,請選擇磁盤下的某個文件夾。", - "pathNotDirectory": "所選路徑已存在但不是文件夾。", + "filesystemRoot": "不能安裝到磁碟根目錄,請選擇磁碟下的某個資料夾。", + "pathNotDirectory": "所選路徑已存在但不是資料夾。", "directoryMustBeEmptyOrBitfun": "安裝目錄必須為空,或已包含 BitFun 安裝。", "inspectDirectoryFailed": "無法讀取安裝目錄,請檢查權限後重試。", "directoryNotWritable": "安裝目錄不可寫入。請更換路徑,或以管理員身份運行安裝器(見下方說明)。", "parentNotWritable": "上級目錄不可寫入。系統目錄(如 Program Files)通常需要管理員權限(見下方說明)。", - "adminHint": "若需安裝到受保護位置(例如 Program Files),請關閉本安裝器,在安裝程序上右鍵選擇「以管理員身份運行」後重新安裝。也可安裝到當前用戶目錄(例如 %LOCALAPPDATA%\\Programs),一般無需管理員權限。" + "adminHint": "若需安裝到受保護位置(例如 Program Files),請關閉本安裝器,在安裝程式上右鍵選擇「以管理員身份運行」後重新安裝。也可安裝到目前用戶目錄(例如 %LOCALAPPDATA%\\Programs),一般無需管理員權限。" } }, "options": { @@ -24,10 +24,10 @@ "browse": "瀏覽", "required": "所需空間", "available": "可用空間", - "insufficientSpace": "磁盤空間不足", + "insufficientSpace": "磁碟空間不足", "optionsLabel": "安裝選項", - "desktopShortcut": "創建桌面快捷方式", - "startMenu": "添加到開始菜單", + "desktopShortcut": "建立桌面快捷方式", + "startMenu": "新增到開始菜單", "launchAfterInstall": "安裝後啟動 BitFun", "back": "返回", "install": "安裝", @@ -35,24 +35,24 @@ "existingInstallTitle": "檢測到本機已安裝 BitFun", "existingInstallVersion": "已安裝版本:{{version}}", "existingInstallLocation": "安裝位置:{{path}}", - "existingInstallBinaryMissing": "該路徑下未找到主程序文件,可先運行卸載程序或重新安裝。", - "existingInstallHint": "直接點擊「安裝」可在原位置升級或修復。若需先卸載,可點擊下方按鈕運行卸載程序。", - "existingInstallRunUninstaller": "運行卸載程序" + "existingInstallBinaryMissing": "該路徑下未找到主程式檔案,可先運行解除安裝程式或重新安裝。", + "existingInstallHint": "直接點擊「安裝」可在原位置升級或修復。若需先解除安裝,可點擊下方按鈕運行解除安裝程式。", + "existingInstallRunUninstaller": "運行解除安裝程式" }, "model": { "title": "模型", - "subtitle": "安裝完成,繼續配置模型與主題", + "subtitle": "安裝完成,繼續設定模型與主題", "provider": "服務商", - "config": "連接信息", + "config": "連接資訊", "modelName": "模型名稱(如 deepseek-v4-flash)", - "skip": "稍後配置", + "skip": "稍後設定", "nextTheme": "下一步:主題", "description": "設定和管理 AI 模型提供商", "providerLabel": "選擇模型提供商", "selectProvider": "或選擇預設提供商", - "customProvider": "自定義配置", + "customProvider": "自定義設定", "modelNamePlaceholder": "輸入自定義模型名稱...", - "baseUrlPlaceholder": "示例:https://open.bigmodel.cn/api/paas/v4/chat/completions", + "baseUrlPlaceholder": "範例:https://open.bigmodel.cn/api/paas/v4/chat/completions", "providers": { "openbitfun": { "name": "OpenBitFun", @@ -70,7 +70,7 @@ "name": "MiniMax", "description": "MiniMax 系列模型", "urlOptions": { - "default": "Anthropic格式-默認", + "default": "Anthropic 格式-默認", "openai": "OpenAI 相容格式" } }, @@ -86,18 +86,18 @@ "name": "智譜AI", "description": "智譜AI GLM 系列模型", "urlOptions": { - "default": "OpenAI格式-默認", - "anthropic": "Anthropic格式", - "codingPlan": "OpenAI格式-CodingPlan" + "default": "OpenAI 格式-默認", + "anthropic": "Anthropic 格式", + "codingPlan": "OpenAI 格式-CodingPlan" } }, "qwen": { "name": "通義千問", "description": "阿里雲百鍊大模型平臺", "urlOptions": { - "default": "OpenAI格式-默認", - "codingPlan": "OpenAI格式-Coding Plan", - "codingPlanAnthropic": "Anthropic格式-Coding Plan" + "default": "OpenAI 格式-默認", + "codingPlan": "OpenAI 格式-Coding Plan", + "codingPlanAnthropic": "Anthropic 格式-Coding Plan" } }, "volcengine": { @@ -108,8 +108,8 @@ "name": "硅基流動", "description": "硅基流動大模型平臺", "urlOptions": { - "default": "OpenAI格式-默認", - "anthropic": "Anthropic格式" + "default": "OpenAI 格式-默認", + "anthropic": "Anthropic 格式" } }, "nvidia": { @@ -130,8 +130,8 @@ "fetchingModels": "正在擷取模型清單...", "fetchFailedFallback": "拉取模型列表失敗,已回退到常用預設模型", "fetchEmptyFallback": "供應商未返回可用模型,已回退到常用預設模型", - "usingPresetModels": "當前顯示的是常用預設模型", - "addCustomModel": "添加自定義模型", + "usingPresetModels": "目前顯示的是常用預設模型", + "addCustomModel": "新增自定義模型", "form": { "baseUrl": "API 位址", "apiKey": "API密鑰", @@ -153,15 +153,15 @@ "progress": { "title": "安裝中", "prepare": "準備中", - "extract": "正在解壓文件", + "extract": "正在解壓檔案", "registry": "正在註冊應用", - "shortcuts": "正在創建快捷方式", + "shortcuts": "正在建立快捷方式", "path": "正在更新 PATH", "config": "正在應用啟動偏好設置", "complete": "即將完成", "starting": "啟動中...", "failed": "安裝失敗", - "confirmContinue": "繼續完成配置" + "confirmContinue": "繼續完成設定" }, "themeSetup": { "title": "主題與啟動", @@ -182,13 +182,13 @@ "finish": "完成" }, "uninstall": { - "title": "卸載 BitFun", + "title": "解除安裝 BitFun", "subtitle": "將移除 BitFun 及其集成項(快捷方式、右鍵菜單、PATH)。", "installPath": "安裝目錄", "pathUnknown": "未檢測到安裝目錄", - "confirm": "開始卸載", - "uninstalling": "正在卸載...", - "completed": "卸載已完成,可關閉窗口。", + "confirm": "開始解除安裝", + "uninstalling": "正在解除安裝...", + "completed": "解除安裝已完成,可關閉視窗。", "cancel": "取消", "close": "關閉" } diff --git a/docs/architecture/i18n.md b/docs/architecture/i18n.md index cad58e510..27822af68 100644 --- a/docs/architecture/i18n.md +++ b/docs/architecture/i18n.md @@ -88,7 +88,8 @@ to their current locale data. namespaces load lazily through the i18next backend when a component requests them with `useI18n(namespace)` or `i18nService.loadNamespace(namespace)`. - Mobile Web owns mobile-only resources and must not import Web UI locale - catalogs. + catalogs. User-visible date/time values should be formatted through the + Mobile Web i18n context instead of per-component `Intl` or `toLocale*` calls. - Installer owns installer-only resources and must not depend on app runtime locale catalogs. - Backend owns backend Fluent resources and uses generated contract helpers for @@ -226,7 +227,9 @@ durable architecture and development rules in this document and `scripts/i18n-locale-format-baseline.json` are temporary no-growth baselines for existing call-site debt. They should move downward as literal fallback copy and direct locale formatting are moved behind owned locale resources and i18n -formatting helpers. +formatting helpers. Direct `Intl` usage is allowed only inside surface i18n +owners that expose those helpers; product call sites should not be added to the +baseline when a helper can represent the same value. ## Backend And Frontend Language Contract diff --git a/docs/development/i18n.md b/docs/development/i18n.md index 5adc8c0db..a9821d50d 100644 --- a/docs/development/i18n.md +++ b/docs/development/i18n.md @@ -17,7 +17,8 @@ `BitFun-Installer`, Rust crates, or server apps. - Persist and send canonical app locale ids, not aliases. - Use surface i18n formatting helpers for user-visible dates, times, numbers, - and currency instead of direct `Intl.*` or `toLocale*` calls. + and currency instead of direct `Intl.*` or `toLocale*` calls. Direct `Intl` + usage belongs only inside the surface i18n owner that exposes those helpers. - In Web UI, request route or feature namespaces through `useI18n(namespace)`. Add a namespace to `WEB_UI_BOOTSTRAP_NAMESPACES` only when a synchronous `i18nService.t('namespace:key')` call must run during module initialization. @@ -153,8 +154,9 @@ such as a server-provided role name or description. Direct user-visible locale formatting calls are tracked by `scripts/i18n-locale-format-baseline.json`. New code should format through the surface i18n API, such as Web UI `useI18n().formatNumber` or -`i18nService.formatDate`, so language changes and fallback rules stay aligned. -Lower the baseline whenever a call site moves behind the shared helper. +`i18nService.formatDate`, or Mobile Web `useI18n().formatDate`, so language +changes and fallback rules stay aligned. Lower the baseline whenever a call +site moves behind the shared helper. Repeated values are not automatically wrong. Generic atoms such as `common:actions.cancel` should be reused when the meaning is identical, but diff --git a/scripts/i18n-audit.mjs b/scripts/i18n-audit.mjs index 2818edb28..596002530 100644 --- a/scripts/i18n-audit.mjs +++ b/scripts/i18n-audit.mjs @@ -44,6 +44,7 @@ let errorCount = 0; let warningCount = 0; let auditTypeScript = null; let cliOptions = { reportJsonPath: null }; +let governanceBaselineCache; const reportCategories = [ 'confirmedUnusedKeys', 'dynamicKeyCandidates', @@ -250,13 +251,16 @@ function sortByReportIdentity(left, right) { function countEntriesBy(entries, field, options = {}) { const emptyLabel = options.emptyLabel ?? ''; + const includeKeys = options.includeKeys ?? []; + const counts = entries.reduce((acc, entry) => { + const value = entry[field] ?? emptyLabel; + const key = value === '' ? emptyLabel : value; + acc.set(key, (acc.get(key) ?? 0) + 1); + return acc; + }, new Map(includeKeys.map((key) => [key, 0]))); + return Object.fromEntries( - Array.from(entries.reduce((counts, entry) => { - const value = entry[field] ?? emptyLabel; - const key = value === '' ? emptyLabel : value; - counts.set(key, (counts.get(key) ?? 0) + 1); - return counts; - }, new Map()).entries()) + Array.from(counts.entries()) .sort(([left], [right]) => String(left).localeCompare(String(right))), ); } @@ -267,6 +271,9 @@ function finalizeGovernanceReport() { governanceReport.summary.counts[category] = governanceReport[category].length; } + const governanceSurfaceIds = collectGovernanceBudgetSurfaceIds(); + const localeFormatSurfaceIds = collectLocaleFormatSurfaceIds(); + governanceReport.summary.byCategory = { confirmedUnusedKeys: { bySurface: countEntriesBy(governanceReport.confirmedUnusedKeys, 'surface'), @@ -279,12 +286,12 @@ function finalizeGovernanceReport() { sharedTermDuplicates: { byNamespace: countEntriesBy(governanceReport.sharedTermDuplicates, 'namespace', { emptyLabel: '' }), bySharedKey: countEntriesBy(governanceReport.sharedTermDuplicates, 'sharedKey'), - bySurface: countEntriesBy(governanceReport.sharedTermDuplicates, 'surface'), + bySurface: countEntriesBy(governanceReport.sharedTermDuplicates, 'surface', { includeKeys: governanceSurfaceIds }), }, l10nQualityCandidates: { byComparisonLocale: countEntriesBy(governanceReport.l10nQualityCandidates, 'comparisonLocale'), byNamespace: countEntriesBy(governanceReport.l10nQualityCandidates, 'namespace', { emptyLabel: '' }), - bySurface: countEntriesBy(governanceReport.l10nQualityCandidates, 'surface'), + bySurface: countEntriesBy(governanceReport.l10nQualityCandidates, 'surface', { includeKeys: governanceSurfaceIds }), }, literalDefaultValueFallbacks: { byFile: countEntriesBy(governanceReport.literalDefaultValueFallbacks, 'file'), @@ -292,7 +299,7 @@ function finalizeGovernanceReport() { }, localeFormatCandidates: { byFile: countEntriesBy(governanceReport.localeFormatCandidates, 'file'), - bySurface: countEntriesBy(governanceReport.localeFormatCandidates, 'surface'), + bySurface: countEntriesBy(governanceReport.localeFormatCandidates, 'surface', { includeKeys: localeFormatSurfaceIds }), }, }; } @@ -1242,8 +1249,13 @@ function readL10nIdenticalAllowlist() { } function readGovernanceBaseline() { + if (governanceBaselineCache !== undefined) { + return governanceBaselineCache; + } + if (!fs.existsSync(governanceBaselinePath)) { reportError('Missing scripts/i18n-governance-baseline.json'); + governanceBaselineCache = null; return null; } @@ -1252,6 +1264,7 @@ function readGovernanceBaseline() { baseline = readJsonFile(governanceBaselinePath); } catch (error) { reportError(`Failed to parse scripts/i18n-governance-baseline.json: ${error.message}`); + governanceBaselineCache = null; return null; } @@ -1260,12 +1273,29 @@ function readGovernanceBaseline() { } if (!isPlainObject(baseline.budgets)) { reportError('scripts/i18n-governance-baseline.json must define a budgets object'); + governanceBaselineCache = null; return null; } + governanceBaselineCache = baseline; return baseline; } +function collectGovernanceBudgetSurfaceIds() { + const baseline = readGovernanceBaseline(); + if (!baseline) return []; + + const surfaces = new Set(); + for (const budget of Object.values(baseline.budgets)) { + if (!isPlainObject(budget?.bySurface)) continue; + for (const surface of Object.keys(budget.bySurface)) { + surfaces.add(surface); + } + } + + return Array.from(surfaces).sort(); +} + function allowlistTargetForGroup(entry, group) { if (entry.surface !== group.surface) return null; if (entry.namespace && entry.namespace !== group.namespace) return null; @@ -1959,7 +1989,10 @@ function countCjkSourceLines(scanRoot, predicate) { function shouldSkipLocaleFormatSourceScan(file) { const normalized = toPosixPath(path.relative(root, file)); return ( + // Surface i18n owners are the only approved locations for direct Intl usage; + // product code must call their exported formatting helpers instead. normalized === 'src/web-ui/src/infrastructure/i18n/core/I18nService.ts' || + normalized === 'src/mobile-web/src/i18n/I18nProvider.tsx' || normalized.endsWith('/generatedLocaleContract.ts') || normalized.endsWith('.test.ts') || normalized.endsWith('.test.tsx') || @@ -1968,9 +2001,8 @@ function shouldSkipLocaleFormatSourceScan(file) { ); } -function collectLocaleFormatUsageFindings() { - const formatPattern = /\b(?:new\s+)?Intl\.(?:DateTimeFormat|NumberFormat|RelativeTimeFormat)\s*\(|\.\s*toLocale(?:String|DateString|TimeString)\s*\(/g; - const specs = [ +function createLocaleFormatScanSpecs() { + return [ { surface: 'web-ui', root: webSourceDir, @@ -2004,6 +2036,15 @@ function collectLocaleFormatUsageFindings() { predicate: (file) => file.endsWith('.js'), }, ]; +} + +function collectLocaleFormatSurfaceIds() { + return sortedUnique(createLocaleFormatScanSpecs().map((spec) => spec.surface)); +} + +function collectLocaleFormatUsageFindings() { + const formatPattern = /\b(?:new\s+)?Intl\.(?:DateTimeFormat|NumberFormat|RelativeTimeFormat)\s*\(|\.\s*toLocale(?:String|DateString|TimeString)\s*\(/g; + const specs = createLocaleFormatScanSpecs(); const findings = []; for (const spec of specs) { diff --git a/scripts/i18n-contract.test.mjs b/scripts/i18n-contract.test.mjs index 1d63a3c41..d1a65f708 100644 --- a/scripts/i18n-contract.test.mjs +++ b/scripts/i18n-contract.test.mjs @@ -65,6 +65,12 @@ function flattenKeys(value, prefix = '') { .sort(); } +function getValueByPath(value, dottedPath) { + return dottedPath.split('.').reduce((current, segment) => ( + current && typeof current === 'object' ? current[segment] : undefined + ), value); +} + function listFiles(dir, predicate) { const entries = fs.readdirSync(dir, { withFileTypes: true }); return entries.flatMap((entry) => { @@ -247,6 +253,60 @@ test('web-ui synchronous i18nService.t namespaces stay in the bootstrap set', () ); }); +test('web-ui slash command picker distinguishes all commands from quick actions', () => { + const source = readText('src/web-ui/src/flow_chat/components/ChatInput.tsx'); + const allCommandsStart = source.indexOf("if (slashCommandState.kind === 'all')"); + const allCommandsEnd = source.indexOf('if (!canSwitchModes)', allCommandsStart); + assert.notEqual(allCommandsStart, -1, 'ChatInput should render an all-command slash picker state'); + assert.notEqual(allCommandsEnd, -1, 'ChatInput all-command branch should stay before the mode-only branch'); + + const allCommandsBlock = source.slice(allCommandsStart, allCommandsEnd); + assert.match( + allCommandsBlock, + /t\('chatInput\.commands'\)/, + 'all-command slash picker should use a Commands label, not the quick-action label', + ); + assert.doesNotMatch( + allCommandsBlock, + /t\('chatInput\.quickAction'\)/, + 'all-command slash picker must not reuse the quick-action label', + ); + + const contract = readJson('src/shared/i18n/contract/locales.json'); + for (const locale of contract.locales.map((entry) => entry.id)) { + const resource = readJson(`src/web-ui/src/locales/${locale}/flow-chat.json`); + const commands = getValueByPath(resource, 'chatInput.commands'); + const quickAction = getValueByPath(resource, 'chatInput.quickAction'); + assert.equal(typeof commands, 'string', `${locale} flow-chat chatInput.commands should exist`); + assert.notEqual(commands, quickAction, `${locale} chatInput.commands should not duplicate chatInput.quickAction`); + } +}); + +test('review platform relative time labels are locale resources', () => { + const source = readText('src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx'); + + assert.match(source, /common:reviewPlatform\.relativeTime\.minutesAgo/, 'minute relative time label should be localized'); + assert.match(source, /common:reviewPlatform\.relativeTime\.hoursAgo/, 'hour relative time label should be localized'); + assert.match(source, /common:reviewPlatform\.relativeTime\.daysAgo/, 'day relative time label should be localized'); + assert.doesNotMatch(source, /`[^`]*\bm ago`/, 'relative minute text must not be hard-coded in ReviewPlatformPanel'); + assert.doesNotMatch(source, /`[^`]*\bh ago`/, 'relative hour text must not be hard-coded in ReviewPlatformPanel'); + assert.doesNotMatch(source, /`[^`]*\bd ago`/, 'relative day text must not be hard-coded in ReviewPlatformPanel'); + + const contract = readJson('src/shared/i18n/contract/locales.json'); + for (const locale of contract.locales.map((entry) => entry.id)) { + const resource = readJson(`src/web-ui/src/locales/${locale}/common.json`); + for (const key of [ + 'reviewPlatform.relativeTime.minutesAgo', + 'reviewPlatform.relativeTime.hoursAgo', + 'reviewPlatform.relativeTime.daysAgo', + ]) { + const value = getValueByPath(resource, key); + assert.equal(typeof value, 'string', `${locale} common ${key} should exist`); + assert.match(value, /\{\{\s*count\s*\}\}/, `${locale} common ${key} should interpolate count`); + } + } +}); + test('i18n audit enforces the checked-in hardcoded source candidate budget', () => { const baselineSource = readText('scripts/i18n-hardcoded-baseline.json'); const auditSource = readText('scripts/i18n-audit.mjs'); @@ -291,6 +351,31 @@ test('i18n audit enforces interpolation parameter parity across resource formats assert.match(auditSource, /extractFluentPlaceholders/, 'Fluent placeholder extraction should be explicit'); }); +test('i18n audit report surface summaries derive from owned scan and budget sources', () => { + const auditSource = readText('scripts/i18n-audit.mjs'); + + assert.doesNotMatch( + auditSource, + /const governanceSurfaceIds\s*=\s*\[/, + 'governance report surface summaries should derive from the governance baseline dimensions', + ); + assert.doesNotMatch( + auditSource, + /const localeFormatSurfaceIds\s*=\s*\[/, + 'locale format report surface summaries should derive from the locale-format scan specs', + ); + assert.match( + auditSource, + /collectGovernanceBudgetSurfaceIds/, + 'governance report should collect zero-count surfaces from the baseline bySurface budgets', + ); + assert.match( + auditSource, + /createLocaleFormatScanSpecs/, + 'locale format report should reuse the same scan specs for scanning and zero-count summaries', + ); +}); + test('i18n audit fails literal fallbacks and unknown static keys', () => { const auditSource = readText('scripts/i18n-audit.mjs'); const sourceTextAudit = auditSource.match(/function auditSourceText\(\) \{[\s\S]*?\n\}/)?.[0] ?? ''; diff --git a/scripts/i18n-governance-baseline.json b/scripts/i18n-governance-baseline.json index 1a4a6716b..ab5f14609 100644 --- a/scripts/i18n-governance-baseline.json +++ b/scripts/i18n-governance-baseline.json @@ -42,45 +42,45 @@ } }, "l10nQualityCandidates": { - "maxTotal": 1087, + "maxTotal": 768, "bySurface": { - "core": 36, - "installer": 24, - "mobile-web": 19, + "core": 24, + "installer": 18, + "mobile-web": 0, "relay-static-homepage": 1, - "web-ui": 1007 + "web-ui": 725 }, "byNamespace": { - "": 80, - "common": 158, - "components": 87, - "errors": 7, - "flow-chat": 210, - "flow-chat/processing-hints": 1, - "notifications": 13, - "panels/files": 25, - "panels/git": 54, - "panels/terminal": 2, - "scenes/agents": 38, + "": 43, + "common": 119, + "components": 58, + "errors": 4, + "flow-chat": 184, + "flow-chat/processing-hints": 0, + "notifications": 8, + "panels/files": 18, + "panels/git": 37, + "panels/terminal": 1, + "scenes/agents": 26, "scenes/miniapp": 7, - "scenes/profile": 40, - "scenes/skills": 28, - "settings": 70, - "settings/acp-agents": 17, - "settings/agentic-tools": 6, - "settings/ai-features": 13, - "settings/ai-model": 52, + "scenes/profile": 34, + "scenes/skills": 15, + "settings": 36, + "settings/acp-agents": 10, + "settings/agentic-tools": 5, + "settings/ai-features": 8, + "settings/ai-model": 32, "settings/basics": 10, - "settings/debug": 7, - "settings/default-model": 11, - "settings/editor": 13, - "settings/lsp": 14, - "settings/mcp": 31, + "settings/debug": 2, + "settings/default-model": 9, + "settings/editor": 11, + "settings/lsp": 13, + "settings/mcp": 20, "settings/quick-actions": 2, "settings/review": 4, - "settings/session-config": 5, - "settings/skills": 13, - "tools": 69 + "settings/session-config": 4, + "settings/skills": 5, + "tools": 43 } } } diff --git a/scripts/i18n-l10n-identical-allowlist.json b/scripts/i18n-l10n-identical-allowlist.json index e6979dc36..36b13e881 100644 --- a/scripts/i18n-l10n-identical-allowlist.json +++ b/scripts/i18n-l10n-identical-allowlist.json @@ -16,6 +16,35 @@ "statuses.done", "tools.explore" ] + }, + { + "id": "mobile-web-zh-tw-same-han-ui-atoms", + "surface": "mobile-web", + "locale": "zh-TW", + "comparisonLocale": "zh-CN", + "owner": "src/mobile-web/src/i18n/messages.ts", + "reason": "Reviewed mobile UI atoms whose Traditional Chinese spelling intentionally matches Simplified Chinese.", + "keys": [ + "chat.done", + "chat.modelFast", + "chat.modelPrimary", + "chat.readToolsRunning", + "chat.thinking", + "chat.waiting", + "common.back", + "common.cancel", + "common.daysAgo", + "common.other", + "common.stop", + "common.submit", + "common.submitted", + "common.submitting", + "sessions.assistant", + "sessions.assistantMode", + "sessions.cancel", + "sessions.recent", + "tools.ls" + ] } ] } diff --git a/scripts/i18n-literal-fallback-baseline.json b/scripts/i18n-literal-fallback-baseline.json index ddb6596d4..6b7d0f789 100644 --- a/scripts/i18n-literal-fallback-baseline.json +++ b/scripts/i18n-literal-fallback-baseline.json @@ -60,104 +60,6 @@ "settings/default-model:selection.primary": 1 } }, - { - "path": "src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx", - "literalDefaultValues": { - "flow-chat:childSession.stoppingReview": 2, - "flow-chat:childSession.stopReview": 2, - "flow-chat:childSession.stopReviewFailed": 1, - "flow-chat:deepReviewActionBar.minimizedDeep": 1, - "flow-chat:deepReviewActionBar.minimizedFix": 1, - "flow-chat:deepReviewActionBar.minimizedFixCompleted": 1, - "flow-chat:deepReviewActionBar.minimizedFixFailed": 1, - "flow-chat:deepReviewActionBar.minimizedFixReview": 1, - "flow-chat:deepReviewActionBar.minimizedResume": 1, - "flow-chat:deepReviewActionBar.minimizedReviewInterrupted": 1, - "flow-chat:deepReviewActionBar.minimizedReviewRunningDeep": 1, - "flow-chat:deepReviewActionBar.minimizedReviewRunningStandard": 1, - "flow-chat:deepReviewActionBar.minimizedStandard": 1, - "flow-chat:deepReviewActionBar.restore": 1, - "flow-chat:flowChatHeader.btwBackTooltipWithoutTurn": 1, - "flow-chat:flowChatHeader.btwBackTooltipWithTurn": 1 - } - }, - { - "path": "src/web-ui/src/flow_chat/components/ChatInput.tsx", - "literalDefaultValues": { - "flow-chat:btw.empty": 1, - "flow-chat:btw.nestedDisabled": 3, - "flow-chat:btw.noSession": 3, - "flow-chat:btw.title": 1, - "flow-chat:chatInput.compactAction": 1, - "flow-chat:chatInput.compactBusy": 1, - "flow-chat:chatInput.compactFailed": 1, - "flow-chat:chatInput.compactNoSession": 1, - "flow-chat:chatInput.compactUsage": 2, - "flow-chat:chatInput.deepreviewAction": 1, - "flow-chat:chatInput.deepreviewBusy": 1, - "flow-chat:chatInput.deepreviewFailed": 1, - "flow-chat:chatInput.deepreviewNestedDisabled": 1, - "flow-chat:chatInput.deepreviewNoSession": 1, - "flow-chat:chatInput.deepreviewThreadTitle": 1, - "flow-chat:chatInput.deepreviewUsage": 1, - "flow-chat:chatInput.goalAction": 1, - "flow-chat:chatInput.goalActivated": 1, - "flow-chat:chatInput.goalAiFailed": 1, - "flow-chat:chatInput.goalFailed": 1, - "flow-chat:chatInput.goalGenerating": 1, - "flow-chat:chatInput.goalNestedDisabled": 1, - "flow-chat:chatInput.goalNoSession": 1, - "flow-chat:chatInput.goalUsage": 2, - "flow-chat:chatInput.initAction": 1, - "flow-chat:chatInput.initBusy": 1, - "flow-chat:chatInput.initFailed": 1, - "flow-chat:chatInput.initNoSession": 1, - "flow-chat:chatInput.initUsage": 2, - "flow-chat:chatInput.noMatchingCommand": 3, - "flow-chat:chatInput.promptCacheGuardBody": 1, - "flow-chat:chatInput.promptCacheGuardCancel": 1, - "flow-chat:chatInput.promptCacheGuardConfirm": 1, - "flow-chat:chatInput.promptCacheGuardTitle": 1, - "flow-chat:chatInput.quickAction": 2, - "flow-chat:chatInput.usageAction": 1, - "flow-chat:chatInput.usageBusy": 1, - "flow-chat:chatInput.usageCommandUsage": 2, - "flow-chat:chatInput.usageFailed": 1, - "flow-chat:chatInput.usageNoSession": 2, - "flow-chat:chatInput.usageNoWorkspace": 1, - "flow-chat:input.largePastePlaceholder": 1, - "flow-chat:input.messageTooLarge": 2, - "flow-chat:input.removeImage": 1, - "flow-chat:usage.loading.markdown": 1 - } - }, - { - "path": "src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx", - "literalDefaultValues": { - "flow-chat:deepReviewConsent.cancel": 2, - "flow-chat:deepReviewConsent.confirm": 1, - "flow-chat:deepReviewConsent.dontShowAgain": 1, - "flow-chat:deepReviewConsent.eyebrow": 1, - "flow-chat:deepReviewConsent.runStrategy": 1, - "flow-chat:deepReviewConsent.selectedStrategy": 1, - "flow-chat:deepReviewConsent.sessionConcurrencyBody": 1, - "flow-chat:deepReviewConsent.sessionConcurrencyTitle": 1, - "flow-chat:deepReviewConsent.skippedGroupTitle": 1, - "flow-chat:deepReviewConsent.skippedMore": 1, - "flow-chat:deepReviewConsent.skippedReasons.budgetLimited": 1, - "flow-chat:deepReviewConsent.skippedReasons.disabled": 1, - "flow-chat:deepReviewConsent.skippedReasons.invalidTooling": 1, - "flow-chat:deepReviewConsent.skippedReasons.notApplicable": 1, - "flow-chat:deepReviewConsent.skippedReasons.skipped": 1, - "flow-chat:deepReviewConsent.skippedReasons.unavailable": 1, - "flow-chat:deepReviewConsent.skippedReviewers": 1, - "flow-chat:deepReviewConsent.strategyOverrideTitle": 1, - "flow-chat:deepReviewConsent.strategyRuntimeImpact": 1, - "flow-chat:deepReviewConsent.strategyTokenImpact": 1, - "flow-chat:deepReviewConsent.summaryTitle": 1, - "flow-chat:deepReviewConsent.title": 1 - } - }, { "path": "src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx", "literalDefaultValues": { @@ -194,27 +96,6 @@ "flow-chat:sessionFileModificationsBar.reviewSource": 1 } }, - { - "path": "src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx", - "literalDefaultValues": { - "flow-chat:sessionFilesBadge.actionsButton": 1, - "flow-chat:sessionFilesBadge.actionsButtonRunning": 1, - "flow-chat:sessionFilesBadge.actionsMenuHint": 1, - "flow-chat:sessionFilesBadge.collapseFileDiffList": 1, - "flow-chat:sessionFilesBadge.deepReview.displayMessage": 1, - "flow-chat:sessionFilesBadge.deepReview.displayMessageFiltered": 1, - "flow-chat:sessionFilesBadge.deepReview.threadTitle": 1, - "flow-chat:sessionFilesBadge.expandChangeListAriaCue": 1, - "flow-chat:sessionFilesBadge.expandChangeListCue": 1, - "flow-chat:sessionFilesBadge.filesSummaryCount": 3, - "flow-chat:sessionFilesBadge.review.displayMessageFiltered": 1, - "flow-chat:sessionFilesBadge.review.filteredNotice": 2, - "flow-chat:sessionFilesBadge.review.noEligibleFiles": 2, - "flow-chat:sessionFilesBadge.review.promptFiltered": 1, - "flow-chat:sessionFilesBadge.review.threadTitle": 1, - "flow-chat:sessionFilesBadge.reviewRunningHint": 1 - } - }, { "path": "src/web-ui/src/flow_chat/components/ScrollToTurnHeaderButton.tsx", "literalDefaultValues": { @@ -237,34 +118,6 @@ "flow-chat:toolCards.taskDetailPanel.stopSubagentHint": 1 } }, - { - "path": "src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx", - "literalDefaultValues": { - "flow-chat:deepReviewActionBar.capacityQueue.activeReviewerCount": 1, - "flow-chat:deepReviewActionBar.capacityQueue.activeReviewerDetail": 1, - "flow-chat:deepReviewActionBar.capacityQueue.activeReviewerTitle": 1, - "flow-chat:deepReviewActionBar.capacityQueue.cancelQueued": 1, - "flow-chat:deepReviewActionBar.capacityQueue.continueQueue": 1, - "flow-chat:deepReviewActionBar.capacityQueue.detail": 1, - "flow-chat:deepReviewActionBar.capacityQueue.elapsed": 2, - "flow-chat:deepReviewActionBar.capacityQueue.elapsedWithMax": 1, - "flow-chat:deepReviewActionBar.capacityQueue.longLaunchBatchWaitDetail": 1, - "flow-chat:deepReviewActionBar.capacityQueue.openReviewSettings": 1, - "flow-chat:deepReviewActionBar.capacityQueue.optionalReviewer": 1, - "flow-chat:deepReviewActionBar.capacityQueue.pausedTitle": 1, - "flow-chat:deepReviewActionBar.capacityQueue.pauseQueue": 1, - "flow-chat:deepReviewActionBar.capacityQueue.providerDetail": 1, - "flow-chat:deepReviewActionBar.capacityQueue.providerTitle": 1, - "flow-chat:deepReviewActionBar.capacityQueue.reason": 1, - "flow-chat:deepReviewActionBar.capacityQueue.reviewerStatusPaused": 1, - "flow-chat:deepReviewActionBar.capacityQueue.reviewerStatusQueued": 1, - "flow-chat:deepReviewActionBar.capacityQueue.sessionBusy": 1, - "flow-chat:deepReviewActionBar.capacityQueue.skipOptionalQueued": 1, - "flow-chat:deepReviewActionBar.capacityQueue.stopHint": 1, - "flow-chat:deepReviewActionBar.capacityQueue.title": 1, - "flow-chat:deepReviewActionBar.capacityQueue.waitingReviewersTitle": 1 - } - }, { "path": "src/web-ui/src/flow_chat/deep-review/action-bar/DecisionExecutionGate.tsx", "literalDefaultValues": { @@ -278,53 +131,6 @@ "flow-chat:toolCards.codeReview.remediationActions.recommended": 1 } }, - { - "path": "src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx", - "literalDefaultValues": { - "flow-chat:deepReviewActionBar.capacityQueue.controlFailed": 1, - "flow-chat:deepReviewActionBar.capacityQueue.controlFailedWithReason": 1, - "flow-chat:deepReviewActionBar.capacityQueue.controlPartiallyFailedWithReason": 1, - "flow-chat:deepReviewActionBar.contextOverflowTitle": 1, - "flow-chat:deepReviewActionBar.customInstructionsPlaceholder": 1, - "flow-chat:deepReviewActionBar.degradation.compressContextPending": 1, - "flow-chat:deepReviewActionBar.degradation.reduceReviewersPending": 1, - "flow-chat:deepReviewActionBar.diagnosticsCopied": 1, - "flow-chat:deepReviewActionBar.diagnosticsCopyFailed": 1, - "flow-chat:deepReviewActionBar.elapsedTime": 1, - "flow-chat:deepReviewActionBar.fixAndReviewRunning": 1, - "flow-chat:deepReviewActionBar.fixCompleted": 1, - "flow-chat:deepReviewActionBar.fixCompletedMessage": 1, - "flow-chat:deepReviewActionBar.fixFailed": 1, - "flow-chat:deepReviewActionBar.fixRunning": 1, - "flow-chat:deepReviewActionBar.fixTimeout": 1, - "flow-chat:deepReviewActionBar.hideCustomInput": 1, - "flow-chat:deepReviewActionBar.longRunningHint": 1, - "flow-chat:deepReviewActionBar.minimize": 1, - "flow-chat:deepReviewActionBar.progressResumePreserved": 1, - "flow-chat:deepReviewActionBar.replaceInputConfirmAction": 1, - "flow-chat:deepReviewActionBar.replaceInputConfirmMessage": 1, - "flow-chat:deepReviewActionBar.replaceInputConfirmTitle": 1, - "flow-chat:deepReviewActionBar.resumeBlocked": 1, - "flow-chat:deepReviewActionBar.resumeBlockedConfirmAction": 1, - "flow-chat:deepReviewActionBar.resumeBlockedConfirmMessage": 1, - "flow-chat:deepReviewActionBar.resumeBlockedConfirmTitle": 1, - "flow-chat:deepReviewActionBar.resumeBlockedWithReason": 1, - "flow-chat:deepReviewActionBar.resumeFailed": 1, - "flow-chat:deepReviewActionBar.resumeFailedMessage": 1, - "flow-chat:deepReviewActionBar.resumeFailedWithReason": 1, - "flow-chat:deepReviewActionBar.resumeRequestDisplay": 1, - "flow-chat:deepReviewActionBar.resumeRunning": 1, - "flow-chat:deepReviewActionBar.retryIncompleteFailed": 1, - "flow-chat:deepReviewActionBar.retryIncompleteRequestDisplay": 1, - "flow-chat:deepReviewActionBar.reviewError": 1, - "flow-chat:deepReviewActionBar.reviewErrorWithReason": 1, - "flow-chat:deepReviewActionBar.reviewInterrupted": 1, - "flow-chat:deepReviewActionBar.reviewInterruptedWithReason": 1, - "flow-chat:deepReviewActionBar.reviewWaitingCapacity": 1, - "flow-chat:deepReviewActionBar.showCustomInput": 1, - "flow-chat:reviewActionBar.noIssuesFound": 1 - } - }, { "path": "src/web-ui/src/flow_chat/deep-review/action-bar/PartialResultsPanel.tsx", "literalDefaultValues": { @@ -358,25 +164,6 @@ "flow-chat:toolCards.codeReview.remediationActions.ungrouped": 1 } }, - { - "path": "src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.tsx", - "literalDefaultValues": { - "flow-chat:deepReviewActionBar.continueFix": 1, - "flow-chat:deepReviewActionBar.copyDiagnostics": 1, - "flow-chat:deepReviewActionBar.fillBackInput": 1, - "flow-chat:deepReviewActionBar.fillBackInputHint": 1, - "flow-chat:deepReviewActionBar.fixInterrupted": 1, - "flow-chat:deepReviewActionBar.minimize": 1, - "flow-chat:deepReviewActionBar.openModelSettings": 1, - "flow-chat:deepReviewActionBar.resumeReview": 1, - "flow-chat:deepReviewActionBar.retryIncompleteSlices": 1, - "flow-chat:deepReviewActionBar.skipRemaining": 1, - "flow-chat:deepReviewActionBar.switchModel": 1, - "flow-chat:deepReviewActionBar.viewPartialResults": 1, - "flow-chat:toolCards.codeReview.remediationActions.fixAndReview": 1, - "flow-chat:toolCards.codeReview.remediationActions.startFix": 1 - } - }, { "path": "src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts", "literalDefaultValues": { @@ -391,35 +178,6 @@ "flow-chat:btw.threadLabel": 1 } }, - { - "path": "src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx", - "literalDefaultValues": { - "flow-chat:toolCards.codeReview.deepReviewResult": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.title": 2, - "flow-chat:toolCards.codeReview.remediationActions.certainty": 1, - "flow-chat:toolCards.codeReview.remediationActions.collapsePlan": 1, - "flow-chat:toolCards.codeReview.remediationActions.expandPlan": 1, - "flow-chat:toolCards.codeReview.remediationActions.location": 1, - "flow-chat:toolCards.codeReview.remediationActions.noRelatedIssue": 1, - "flow-chat:toolCards.codeReview.remediationActions.recommended": 1, - "flow-chat:toolCards.codeReview.remediationActions.relatedIssue": 1, - "flow-chat:toolCards.codeReview.remediationActions.severity": 1, - "flow-chat:toolCards.codeReview.remediationPlan": 1, - "flow-chat:toolCards.codeReview.reviewerIssues": 1, - "flow-chat:toolCards.codeReview.reviewerIssuesUnknown": 1, - "flow-chat:toolCards.codeReview.reviewerTeam": 1, - "flow-chat:toolCards.codeReview.reviewMode": 1, - "flow-chat:toolCards.codeReview.reviewScope": 1, - "flow-chat:toolCards.codeReview.runManifest.recommendedStrategy": 1, - "flow-chat:toolCards.codeReview.runManifest.reviewDepth": 1, - "flow-chat:toolCards.codeReview.runManifest.riskRecommendationTitle": 1, - "flow-chat:toolCards.codeReview.sectionItemCount": 3, - "flow-chat:toolCards.codeReview.sections.coverage": 1, - "flow-chat:toolCards.codeReview.sections.remediation": 1, - "flow-chat:toolCards.codeReview.sections.strengths": 1, - "flow-chat:toolCards.codeReview.sections.summary": 1 - } - }, { "path": "src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx", "literalDefaultValues": { @@ -435,36 +193,12 @@ "flow-chat:toolCards.getFileDiff.preparing": 1 } }, - { - "path": "src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx", - "literalDefaultValues": { - "flow-chat:toolCards.git.commandCopied": 4, - "flow-chat:toolCards.git.copyCommand": 4, - "flow-chat:toolCards.git.copyCommandFailed": 2 - } - }, { "path": "src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx", "literalDefaultValues": { "flow-chat:toolCards.readFile.permissionRequest": 1 } }, - { - "path": "src/web-ui/src/flow_chat/tool-cards/ReviewSessionSummaryCard.tsx", - "literalDefaultValues": { - "flow-chat:toolCards.reviewSessionSummary.changedFilesTitle": 1, - "flow-chat:toolCards.reviewSessionSummary.deepTitle": 1, - "flow-chat:toolCards.reviewSessionSummary.emptySummary": 1, - "flow-chat:toolCards.reviewSessionSummary.failed": 1, - "flow-chat:toolCards.reviewSessionSummary.filesChanged": 1, - "flow-chat:toolCards.reviewSessionSummary.issueCount": 1, - "flow-chat:toolCards.reviewSessionSummary.noIssues": 1, - "flow-chat:toolCards.reviewSessionSummary.openReview": 1, - "flow-chat:toolCards.reviewSessionSummary.running": 1, - "flow-chat:toolCards.reviewSessionSummary.standardTitle": 1, - "flow-chat:toolCards.reviewSessionSummary.waitingSummary": 1 - } - }, { "path": "src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.tsx", "literalDefaultValues": { diff --git a/scripts/i18n-locale-format-baseline.json b/scripts/i18n-locale-format-baseline.json index 06dadcb6e..08fa3f796 100644 --- a/scripts/i18n-locale-format-baseline.json +++ b/scripts/i18n-locale-format-baseline.json @@ -9,58 +9,6 @@ { "path": "src/crates/core/src/miniapp/builtin/assets/pr-review/ui.js", "maxLocaleFormatCalls": 1 - }, - { - "path": "src/mobile-web/src/pages/SessionListPage.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/app/scenes/my-agent/AssistantScheduleView.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx", - "maxLocaleFormatCalls": 3 - }, - { - "path": "src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/ContextCompressionCard.tsx", - "maxLocaleFormatCalls": 6 - }, - { - "path": "src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/flow_chat/components/TurnHistoryPanel.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/flow_chat/components/UserMessage.tsx", - "maxLocaleFormatCalls": 1 - }, - { - "path": "src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx", - "maxLocaleFormatCalls": 3 - }, - { - "path": "src/web-ui/src/shared/utils/format.ts", - "maxLocaleFormatCalls": 1 } ] } diff --git a/src/crates/core/locales/zh-TW.ftl b/src/crates/core/locales/zh-TW.ftl index bfcc2286d..8f8211ca0 100644 --- a/src/crates/core/locales/zh-TW.ftl +++ b/src/crates/core/locales/zh-TW.ftl @@ -3,38 +3,38 @@ # ==================== 通用 ==================== app-version = 版本 { $version } -loading = 加載中... +loading = 載入中... welcome = 歡迎使用 BitFun # ==================== 操作 ==================== action-confirm = 確認 action-cancel = 取消 -action-save = 保存 +action-save = 儲存 action-delete = 刪除 action-edit = 編輯 -action-create = 創建 -action-add = 添加 +action-create = 建立 +action-add = 新增 action-remove = 移除 action-close = 關閉 -action-open = 打開 +action-open = 開啟 action-copy = 複製 action-paste = 粘貼 action-undo = 撤銷 action-redo = 重做 -action-refresh = 刷新 -action-search = 搜索 +action-refresh = 重新整理 +action-search = 進行搜尋 action-retry = 重試 action-stop = 停止 action-start = 開始 # ==================== 狀態 ==================== -status-loading = 加載中 -status-saving = 保存中 -status-saved = 已保存 +status-loading = 正在載入 +status-saving = 儲存中 +status-saved = 已儲存 status-success = 成功 status-error = 錯誤 status-warning = 警告 -status-info = 信息 +status-info = 資訊 status-pending = 等待中 status-processing = 處理中 status-completed = 已完成 @@ -44,25 +44,25 @@ status-ready = 就緒 status-connected = 已連接 status-disconnected = 已斷開 -# ==================== 文件 ==================== -file-not-found = 文件未找到:{ $path } -file-read-error = 讀取文件失敗:{ $path } -file-write-error = 寫入文件失敗:{ $path } -file-delete-error = 刪除文件失敗:{ $path } +# ==================== 檔案 ==================== +file-not-found = 檔案未找到:{ $path } +file-read-error = 讀取檔案失敗:{ $path } +file-write-error = 寫入檔案失敗:{ $path } +file-delete-error = 刪除檔案失敗:{ $path } file-permission-denied = 權限不足:{ $path } -file-already-exists = 文件已存在:{ $path } -file-saved = 文件已保存:{ $path } -file-created = 文件已創建:{ $path } -file-deleted = 文件已刪除:{ $path } +file-already-exists = 檔案已存在:{ $path } +file-saved = 檔案已儲存:{ $path } +file-created = 檔案已建立:{ $path } +file-deleted = 檔案已刪除:{ $path } # ==================== 工作區 ==================== -workspace-opened = 工作區已打開:{ $path } +workspace-opened = 工作區已開啟:{ $path } workspace-closed = 工作區已關閉 workspace-not-found = 工作區未找到 -workspace-open-error = 打開工作區失敗 +workspace-open-error = 開啟工作區失敗 # ==================== Git ==================== -git-not-repository = 當前目錄不是 Git 倉庫 +git-not-repository = 目前目錄不是 Git 倉庫 git-commit-success = 提交成功 git-push-success = 推送成功 git-pull-success = 拉取成功 @@ -71,7 +71,7 @@ git-commit-error = 提交失敗 git-push-error = 推送失敗 git-pull-error = 拉取失敗 git-merge-conflict = 存在合併衝突 -git-branch-created = 分支已創建:{ $name } +git-branch-created = 分支已建立:{ $name } git-branch-deleted = 分支已刪除:{ $name } git-checkout-success = 已切換到分支:{ $name } @@ -86,25 +86,25 @@ ai-thinking = 思考中... ai-generating = 生成中... # ==================== 終端 ==================== -terminal-created = 終端已創建 +terminal-created = 終端已建立 terminal-closed = 終端已關閉 -terminal-create-error = 創建終端失敗 +terminal-create-error = 建立終端失敗 terminal-command-error = 執行命令失敗 terminal-shell-not-found = Shell 未找到 -# ==================== 配置 ==================== -config-loaded = 配置已加載 -config-saved = 配置已保存 -config-load-error = 加載配置失敗 -config-save-error = 保存配置失敗 -config-invalid = 配置格式無效 -config-reset = 配置已重置 +# ==================== 設定 ==================== +config-loaded = 設定已載入 +config-saved = 設定已儲存 +config-load-error = 載入設定失敗 +config-save-error = 儲存設定失敗 +config-invalid = 設定格式無效 +config-reset = 設定已重設 # ==================== 快照 ==================== -snapshot-created = 快照已創建:{ $name } +snapshot-created = 快照已建立:{ $name } snapshot-restored = 快照已恢復:{ $name } snapshot-deleted = 快照已刪除 -snapshot-create-error = 創建快照失敗 +snapshot-create-error = 建立快照失敗 snapshot-restore-error = 恢復快照失敗 snapshot-not-found = 快照未找到 @@ -114,7 +114,7 @@ language-not-supported = 不支持的語言:{ $language } # ==================== 通知 ==================== notification-copied = 已複製到剪貼板 -notification-settings-saved = 設置已保存 +notification-settings-saved = 設置已儲存 notification-connection-established = 連接已建立 notification-connection-lost = 連接已斷開 @@ -122,7 +122,7 @@ notification-connection-lost = 連接已斷開 error-unknown = 發生未知錯誤 error-network = 網絡錯誤 error-timeout = 請求超時 -error-server = 服務器錯誤 +error-server = 伺服器錯誤 error-unauthorized = 未授權 error-forbidden = 禁止訪問 diff --git a/src/mobile-web/src/i18n/I18nProvider.tsx b/src/mobile-web/src/i18n/I18nProvider.tsx index 137c17264..6bbb16a78 100644 --- a/src/mobile-web/src/i18n/I18nProvider.tsx +++ b/src/mobile-web/src/i18n/I18nProvider.tsx @@ -16,6 +16,7 @@ interface I18nContextValue { setLanguage: (language: MobileLanguage) => void; toggleLanguage: () => void; t: (key: string, params?: TranslateParams) => string; + formatDate: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string; } const STORAGE_KEY = 'bitfun-mobile-language'; @@ -97,6 +98,7 @@ export const I18nContext = createContext({ setLanguage: () => {}, toggleLanguage: () => {}, t: (key) => key, + formatDate: (date, options) => new Intl.DateTimeFormat(DEFAULT_LANGUAGE, options).format(date), }); export const I18nProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -124,6 +126,7 @@ export const I18nProvider: React.FC<{ children: React.ReactNode }> = ({ children setLanguage, toggleLanguage, t: (key, params) => translate(language, key, params), + formatDate: (date, options) => new Intl.DateTimeFormat(language, options).format(date), }), [language, setLanguage, toggleLanguage]); return ( diff --git a/src/mobile-web/src/pages/SessionListPage.tsx b/src/mobile-web/src/pages/SessionListPage.tsx index 81f0125c8..d91a82441 100644 --- a/src/mobile-web/src/pages/SessionListPage.tsx +++ b/src/mobile-web/src/pages/SessionListPage.tsx @@ -17,7 +17,11 @@ interface SessionListPageProps { onDisconnect: () => void; } -function formatTime(unixStr: string, language: string, t: (key: string, params?: Record) => string): string { +function formatTime( + unixStr: string, + formatDate: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string, + t: (key: string, params?: Record) => string, +): string { const ts = parseInt(unixStr, 10); if (!ts || isNaN(ts)) return ''; const date = new Date(ts * 1000); @@ -30,7 +34,7 @@ function formatTime(unixStr: string, language: string, t: (key: string, params?: if (diffHr < 24) return t('common.hoursAgo', { count: diffHr }); const diffDay = Math.floor(diffHr / 24); if (diffDay < 7) return t('common.daysAgo', { count: diffDay }); - return date.toLocaleDateString(language); + return formatDate(date); } function agentLabel(agentType: string, t: (key: string) => string): string { @@ -139,7 +143,7 @@ const ThemeToggleIcon: React.FC<{ isDark: boolean }> = ({ isDark }) => ( ); const SessionListPage: React.FC = ({ sessionMgr, onSelectSession, onOpenWorkspace, onDisconnect }) => { - const { t, language } = useI18n(); + const { t, formatDate } = useI18n(); const { sessions, setSessions, @@ -887,7 +891,7 @@ const SessionListPage: React.FC = ({ sessionMgr, onSelectS {agentLabel(s.agent_type, t)} -
{formatTime(s.updated_at, language, t)}
+
{formatTime(s.updated_at, formatDate, t)}
))} diff --git a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx index 012d74edf..d88b2690d 100644 --- a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx +++ b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx @@ -162,7 +162,7 @@ const FlexiblePanel: React.FC = memo(({ isActive = true, onFileMissingFromDiskChange, }) => { - const { t } = useI18n('components'); + const { t, formatDate } = useI18n('components'); // Use ref to save latest content, avoiding it in callback dependencies const contentRef = React.useRef(content); @@ -666,7 +666,12 @@ const FlexiblePanel: React.FC = memo(({
{t('flexiblePanel.aiSession.startTime')} - {content.data?.start_time ? new Date(content.data.start_time).toLocaleString() : t('flexiblePanel.aiSession.unknownTime')} + {content.data?.start_time + ? formatDate(new Date(content.data.start_time), { + dateStyle: 'medium', + timeStyle: 'short', + }) + : t('flexiblePanel.aiSession.unknownTime')}
diff --git a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx index 25080ac1b..d5670f235 100644 --- a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx +++ b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx @@ -26,6 +26,7 @@ import { Button, IconButton, Input, MarkdownRenderer, Modal, Select, Tabs, TabPa import { reviewPlatformAPI, systemAPI, type ReviewPlatformAccount, type ReviewPlatformAuthChallenge, type ReviewPlatformCiItem, type ReviewPlatformCiLog, type ReviewPlatformCommit, type ReviewPlatformDetailSection, type ReviewPlatformFile, type ReviewPlatformPagination, type ReviewPlatformPullRequest, type ReviewPlatformPullRequestDetail, type ReviewPlatformPullRequestDetailPage, type ReviewPlatformRemote, type ReviewPlatformRepositoryRef, type ReviewPlatformThread, type ReviewPlatformWorkspaceSnapshot } from '@/infrastructure/api'; import { createLogger } from '@/shared/utils/logger'; import { notificationService } from '@/shared/notification-system'; +import { i18nService } from '@/infrastructure/i18n'; import { openMainSession } from '@/flow_chat/services/openBtwSession'; import { flowChatStore } from '@/flow_chat/store/FlowChatStore'; import type { FlowToolItem, Session } from '@/flow_chat/types/flow-chat'; @@ -211,20 +212,20 @@ function formatRelativeTime(value: string): string { if (!Number.isFinite(time)) return ''; const diffMs = Date.now() - time; const minutes = Math.max(1, Math.floor(diffMs / 60000)); - if (minutes < 60) return `${minutes}m ago`; + if (minutes < 60) return i18nService.t('common:reviewPlatform.relativeTime.minutesAgo', { count: minutes }); const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; + if (hours < 24) return i18nService.t('common:reviewPlatform.relativeTime.hoursAgo', { count: hours }); + return i18nService.t('common:reviewPlatform.relativeTime.daysAgo', { count: Math.floor(hours / 24) }); } function formatAbsoluteTime(value: string): string { if (!value) return ''; const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; - return new Intl.DateTimeFormat(undefined, { + return i18nService.formatDate(date, { dateStyle: 'medium', timeStyle: 'short', - }).format(date); + }); } function getPrIcon(pr: ReviewPlatformPullRequest) { diff --git a/src/web-ui/src/app/scenes/my-agent/AssistantScheduleView.tsx b/src/web-ui/src/app/scenes/my-agent/AssistantScheduleView.tsx index 5f5a19590..15b55f63f 100644 --- a/src/web-ui/src/app/scenes/my-agent/AssistantScheduleView.tsx +++ b/src/web-ui/src/app/scenes/my-agent/AssistantScheduleView.tsx @@ -148,11 +148,12 @@ function getNextExecutionAtMs(job: CronJob): number | null { function formatScheduleSummary( schedule: CronSchedule, + formatDate: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string, t: (key: string, params?: Record) => string, ): string { switch (schedule.kind) { case 'at': - return `${t('nav.scheduledJobs.scheduleKinds.at')}: ${formatTimestamp(new Date(schedule.at).getTime(), t)}`; + return `${t('nav.scheduledJobs.scheduleKinds.at')}: ${formatTimestamp(new Date(schedule.at).getTime(), formatDate, t)}`; case 'every': return t('nav.scheduledJobs.scheduleSummary.every', { everyMinutes: formatEveryMinutes(schedule.everyMs) }); case 'cron': @@ -166,12 +167,13 @@ function formatScheduleSummary( function formatTimestamp( timestampMs: number | null | undefined, + formatDate: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string, t: (key: string, params?: Record) => string, ): string { if (!timestampMs || !Number.isFinite(timestampMs)) return t('nav.scheduledJobs.never'); - return new Intl.DateTimeFormat(undefined, { + return formatDate(timestampMs, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', - }).format(timestampMs); + }); } function resolveSessionLabel(session: Session): string { @@ -182,7 +184,7 @@ const AssistantScheduleView: React.FC = ({ workspacePath, sessionId, }) => { - const { t } = useI18n('common'); + const { t, formatDate } = useI18n('common'); const [flowChatState, setFlowChatState] = useState(() => flowChatStore.getState()); const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(false); @@ -415,10 +417,10 @@ const AssistantScheduleView: React.FC = ({ {job.name}
- {formatScheduleSummary(job.schedule, t)} + {formatScheduleSummary(job.schedule, formatDate, t)}
- {t('nav.scheduledJobs.nextRunLabel')}: {formatTimestamp(getNextExecutionAtMs(job), t)} + {t('nav.scheduledJobs.nextRunLabel')}: {formatTimestamp(getNextExecutionAtMs(job), formatDate, t)}
{job.state.lastError ? (
{job.state.lastError}
diff --git a/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx b/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx index 2ef7c733c..46006cd32 100644 --- a/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx +++ b/src/web-ui/src/app/scenes/my-agent/InsightsScene.tsx @@ -140,10 +140,10 @@ const ReportMetaCard: React.FC<{ meta: InsightsReportMeta; onSelect: (meta: InsightsReportMeta) => void; }> = ({ meta, onSelect }) => { - const { t } = useI18n('common'); + const { t, formatDate } = useI18n('common'); const date = new Date(meta.generated_at * 1000); - const dateStr = date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); - const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + const dateStr = formatDate(date, { year: 'numeric', month: 'short', day: 'numeric' }); + const timeStr = formatDate(date, { hour: '2-digit', minute: '2-digit' }); const rangeStart = meta.date_range.start.slice(0, 10); const rangeEnd = meta.date_range.end.slice(0, 10); const formatGoal = (g: string) => g.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); @@ -498,10 +498,8 @@ const formatDurationShort = (secs: number): string => { return `${(secs / 3600).toFixed(1)}h`; }; -const formatNumber = (n: number): string => n.toLocaleString(); - const StatsRow: React.FC<{ report: InsightsReport }> = ({ report }) => { - const { t } = useI18n('common'); + const { t, formatNumber } = useI18n('common'); const { stats } = report; const hasCodeChanges = (stats.total_lines_added ?? 0) > 0 || (stats.total_lines_removed ?? 0) > 0; diff --git a/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx b/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx index 893f145f6..232d896dc 100644 --- a/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx +++ b/src/web-ui/src/app/scenes/settings/components/ArchivedSessionsConfig.tsx @@ -25,6 +25,7 @@ import { createLogger } from '@/shared/utils/logger'; import { useSettingsStore } from '@/app/scenes/settings/settingsStore'; import { flowChatManager } from '@/flow_chat/services/FlowChatManager'; import type { SessionMetadata } from '@/shared/types/session-history'; +import { i18nService } from '@/infrastructure/i18n'; import './ArchivedSessionsConfig.scss'; const log = createLogger('ArchivedSessionsConfig'); @@ -45,7 +46,7 @@ function formatDateTime(timestampMs: number): string { if (!timestampMs) return ''; try { const d = new Date(timestampMs); - return d.toLocaleDateString(undefined, { + return i18nService.formatDate(d, { year: 'numeric', month: 'short', day: 'numeric', @@ -360,4 +361,4 @@ const ArchivedSessionsConfig: React.FC = () => { ); }; -export default ArchivedSessionsConfig; \ No newline at end of file +export default ArchivedSessionsConfig; diff --git a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx index ca5ac675f..a2e7c1b0f 100644 --- a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx +++ b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx @@ -23,7 +23,7 @@ import './WelcomeScene.scss'; const log = createLogger('WelcomeScene'); const WelcomeScene: React.FC = () => { - const { t } = useI18n('common'); + const { t, formatDate: formatLocaleDate } = useI18n('common'); const { hasWorkspace, currentWorkspace, recentWorkspaces, openWorkspace, switchWorkspace, removeWorkspaceFromRecent, @@ -102,11 +102,11 @@ const WelcomeScene: React.FC = () => { if (diffDays <= 1) return t('time.yesterday'); if (diffDays < 7) return t('startup.daysAgo', { count: diffDays }); if (diffDays < 30) return t('startup.weeksAgo', { count: Math.ceil(diffDays / 7) }); - return date.toLocaleDateString(); + return formatLocaleDate(date); } catch { return ''; } - }, [t]); + }, [formatLocaleDate, t]); return (
diff --git a/src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/ContextCompressionCard.tsx b/src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/ContextCompressionCard.tsx index ab26cf4a6..d70533cf8 100644 --- a/src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/ContextCompressionCard.tsx +++ b/src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/ContextCompressionCard.tsx @@ -38,7 +38,7 @@ export const ContextCompressionCard: React.FC = ({ displayMode = 'standard', ...baseProps }) => { - const { t } = useI18n('components'); + const { t, formatNumber } = useI18n('components'); const resolvedCompressionCount = compressionCount || result?.compression_count || 1; const resolvedTokensBefore = tokensBefore || result?.tokens_before || input?.tokens_before; @@ -91,13 +91,13 @@ export const ContextCompressionCard: React.FC = ({ {resolvedTokensBefore !== undefined && resolvedTokensAfter !== undefined && ( - {resolvedTokensBefore.toLocaleString()} → {resolvedTokensAfter.toLocaleString()} tokens + {formatNumber(resolvedTokensBefore)} → {formatNumber(resolvedTokensAfter)} tokens )} {status === 'completed' && savedTokens !== undefined && resolvedCompressionRatio !== undefined && ( - {t('flowChatCards.contextCompressionCard.savedTokens', { count: savedTokens.toLocaleString(), ratio: (resolvedCompressionRatio * 100).toFixed(0) })} + {t('flowChatCards.contextCompressionCard.savedTokens', { count: formatNumber(savedTokens), ratio: (resolvedCompressionRatio * 100).toFixed(0) })} )}
@@ -141,11 +141,11 @@ export const ContextCompressionCard: React.FC = ({ {resolvedTokensBefore !== undefined && resolvedTokensAfter !== undefined && (
- {resolvedTokensBefore?.toLocaleString()} → {resolvedTokensAfter?.toLocaleString()} tokens + {formatNumber(resolvedTokensBefore)} → {formatNumber(resolvedTokensAfter)} tokens {savedTokens !== undefined && resolvedCompressionRatio !== undefined && ( - {t('flowChatCards.contextCompressionCard.savedTokens', { count: savedTokens.toLocaleString(), ratio: (resolvedCompressionRatio * 100).toFixed(1) })} + {t('flowChatCards.contextCompressionCard.savedTokens', { count: formatNumber(savedTokens), ratio: (resolvedCompressionRatio * 100).toFixed(1) })} )}
diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 77e78e5d5..e9c7d0de1 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -545,22 +545,14 @@ export const ChatInput: React.FC = ({ } return confirmWarning( - t('chatInput.promptCacheGuardTitle', { - defaultValue: 'Switching this mode will reset prompt cache reuse', - }), + t('chatInput.promptCacheGuardTitle'), t('chatInput.promptCacheGuardBody', { - defaultValue: - 'The next request will switch from {{fromMode}} to {{toMode}}, so this session will stop reusing its current prompt cache. Continue?', fromMode: getModeDisplayName(lastSubmittedMode), toMode: getModeDisplayName(nextMode), }), { - confirmText: t('chatInput.promptCacheGuardConfirm', { - defaultValue: 'Send anyway', - }), - cancelText: t('chatInput.promptCacheGuardCancel', { - defaultValue: 'Stay here', - }), + confirmText: t('chatInput.promptCacheGuardConfirm'), + cancelText: t('chatInput.promptCacheGuardCancel'), }, ); }, [currentMode, effectiveTargetSession?.lastSubmittedMode, getModeDisplayName, modeInfoById, t]); @@ -662,7 +654,6 @@ export const ChatInput: React.FC = ({ const base = t('input.largePastePlaceholder', { count: charCount, - defaultValue: '[Pasted Content {{count}} chars]', }); const placeholder = nextSuffix === 1 ? base : `${base} #${nextSuffix}`; @@ -1195,25 +1186,25 @@ export const ChatInput: React.FC = ({ kind: 'action' as const, id: 'btw', command: '/btw', - label: t('btw.title', { defaultValue: 'Side question' }), + label: t('btw.title'), }]), { kind: 'action', id: 'goal', command: '/goal', - label: t('chatInput.goalAction', { defaultValue: 'Session goal' }), + label: t('chatInput.goalAction'), }, { kind: 'action', id: 'usage', command: '/usage', - label: t('chatInput.usageAction', { defaultValue: 'Usage report' }), + label: t('chatInput.usageAction'), }, { kind: 'action', id: 'deepreview', command: DEEP_REVIEW_SLASH_COMMAND, - label: t('chatInput.deepreviewAction', { defaultValue: 'Deep review' }), + label: t('chatInput.deepreviewAction'), }, ...(!derivedState?.isProcessing ? [ @@ -1221,13 +1212,13 @@ export const ChatInput: React.FC = ({ kind: 'action' as const, id: 'compact', command: '/compact', - label: t('chatInput.compactAction', { defaultValue: 'Compact session' }), + label: t('chatInput.compactAction'), }, { kind: 'action' as const, id: 'init', command: '/init', - label: t('chatInput.initAction', { defaultValue: 'Generate AGENTS.md' }), + label: t('chatInput.initAction'), }, ] : []), @@ -1376,11 +1367,11 @@ export const ChatInput: React.FC = ({ const submitBtwFromInput = useCallback(async () => { if (!derivedState) return; if (!currentSessionId) { - notificationService.error(t('btw.noSession', { defaultValue: 'No active session for /btw' })); + notificationService.error(t('btw.noSession')); return; } if (isBtwSession) { - notificationService.warning(t('btw.nestedDisabled', { defaultValue: 'Side questions cannot create another side question' })); + notificationService.warning(t('btw.nestedDisabled')); return; } @@ -1397,7 +1388,7 @@ export const ChatInput: React.FC = ({ setSlashCommandState({ isActive: false, kind: 'modes', query: '', selectedIndex: 0 }); if (!question) { - notificationService.warning(t('btw.empty', { defaultValue: 'Please provide a question after /btw' })); + notificationService.warning(t('btw.empty')); return; } @@ -1406,7 +1397,6 @@ export const ChatInput: React.FC = ({ t('input.messageTooLarge', { max: CHAT_INPUT_CONFIG.largePaste.maxMessageChars, count: messageCharCount, - defaultValue: 'Message exceeds the maximum length of {{max}} characters ({{count}} provided).', }), { duration: 4000 } ); @@ -1442,16 +1432,14 @@ export const ChatInput: React.FC = ({ const submitCompactFromInput = useCallback(async () => { if (!effectiveTargetSessionId || !effectiveTargetSession) { notificationService.error( - t('chatInput.compactNoSession', { defaultValue: 'No active session for /compact' }) + t('chatInput.compactNoSession') ); return; } if (derivedState?.isProcessing) { notificationService.warning( - t('chatInput.compactBusy', { - defaultValue: 'Wait until the session is idle before using /compact.', - }) + t('chatInput.compactBusy') ); return; } @@ -1459,7 +1447,7 @@ export const ChatInput: React.FC = ({ const message = inputState.value.trim(); if (!/^\/compact\s*$/i.test(message)) { notificationService.warning( - t('chatInput.compactUsage', { defaultValue: 'Use /compact without extra arguments.' }) + t('chatInput.compactUsage') ); return; } @@ -1486,7 +1474,7 @@ export const ChatInput: React.FC = ({ notificationService.error( error instanceof Error ? error.message : t('error.unknown'), { - title: t('chatInput.compactFailed', { defaultValue: 'Session compaction failed' }), + title: t('chatInput.compactFailed'), duration: 5000, } ); @@ -1503,7 +1491,7 @@ export const ChatInput: React.FC = ({ const runEffectiveSessionUsageReport = useCallback(async () => { if (!effectiveTargetSessionId || !effectiveTargetSession) { notificationService.error( - t('chatInput.usageNoSession', { defaultValue: 'No active session for /usage' }) + t('chatInput.usageNoSession') ); return; } @@ -1512,15 +1500,11 @@ export const ChatInput: React.FC = ({ const result = await runUsageReportCommand({ session: effectiveTargetSession, isProcessing: !!derivedState?.isProcessing, - busyMessage: t('chatInput.usageBusy', { - defaultValue: 'Wait until the session is idle before using /usage.', - }), - noWorkspaceMessage: t('chatInput.usageNoWorkspace', { - defaultValue: 'A workspace is required to build a usage report.', - }), - failedTitle: t('chatInput.usageFailed', { defaultValue: 'Usage report failed' }), + busyMessage: t('chatInput.usageBusy'), + noWorkspaceMessage: t('chatInput.usageNoWorkspace'), + failedTitle: t('chatInput.usageFailed'), unknownErrorMessage: t('error.unknown'), - loadingMarkdown: t('usage.loading.markdown', { defaultValue: 'Generating usage report...' }), + loadingMarkdown: t('usage.loading.markdown'), }); if (result.inserted) { @@ -1543,7 +1527,7 @@ export const ChatInput: React.FC = ({ const submitUsageFromInput = useCallback(async () => { if (!effectiveTargetSessionId || !effectiveTargetSession) { notificationService.error( - t('chatInput.usageNoSession', { defaultValue: 'No active session for /usage' }) + t('chatInput.usageNoSession') ); return; } @@ -1551,7 +1535,7 @@ export const ChatInput: React.FC = ({ const message = inputState.value.trim(); if (!/^\/usage\s*$/i.test(message)) { notificationService.warning( - t('chatInput.usageCommandUsage', { defaultValue: 'Use /usage without extra arguments.' }) + t('chatInput.usageCommandUsage') ); return; } @@ -1584,16 +1568,14 @@ export const ChatInput: React.FC = ({ const submitInitFromInput = useCallback(async () => { if (!effectiveTargetSessionId || !effectiveTargetSession) { notificationService.error( - t('chatInput.initNoSession', { defaultValue: 'No active session for /init' }) + t('chatInput.initNoSession') ); return; } if (derivedState?.isProcessing) { notificationService.warning( - t('chatInput.initBusy', { - defaultValue: 'Wait until the session is idle before using /init.', - }) + t('chatInput.initBusy') ); return; } @@ -1601,7 +1583,7 @@ export const ChatInput: React.FC = ({ const message = inputState.value.trim(); if (!/^\/init\s*$/i.test(message)) { notificationService.warning( - t('chatInput.initUsage', { defaultValue: 'Use /init without extra arguments.' }) + t('chatInput.initUsage') ); return; } @@ -1628,7 +1610,7 @@ export const ChatInput: React.FC = ({ notificationService.error( error instanceof Error ? error.message : t('error.unknown'), { - title: t('chatInput.initFailed', { defaultValue: 'Session init failed' }), + title: t('chatInput.initFailed'), duration: 5000, } ); @@ -1645,16 +1627,14 @@ export const ChatInput: React.FC = ({ const submitGoalFromInput = useCallback(async () => { if (!effectiveTargetSessionId || !effectiveTargetSession) { notificationService.error( - t('chatInput.goalNoSession', { defaultValue: 'No active session for /goal' }) + t('chatInput.goalNoSession') ); return; } if (isBtwSession) { notificationService.warning( - t('chatInput.goalNestedDisabled', { - defaultValue: 'Goal mode can only be started from the main session.', - }) + t('chatInput.goalNestedDisabled') ); return; } @@ -1663,9 +1643,7 @@ export const ChatInput: React.FC = ({ const parsed = parseGoalCommand(message); if (!parsed) { notificationService.warning( - t('chatInput.goalUsage', { - defaultValue: 'Use /goal with optional focus text, for example /goal fix the login bug.', - }) + t('chatInput.goalUsage') ); return; } @@ -1678,13 +1656,11 @@ export const ChatInput: React.FC = ({ const result = await runGoalCommandSafely({ session: effectiveTargetSession, userHint: parsed.userHint, - loadingMessage: t('chatInput.goalGenerating', { defaultValue: 'Generating session goal...' }), - failedTitle: t('chatInput.goalFailed', { defaultValue: 'Goal mode activation failed' }), + loadingMessage: t('chatInput.goalGenerating'), + failedTitle: t('chatInput.goalFailed'), unknownErrorMessage: t('error.unknown'), - aiFailedMessage: t('chatInput.goalAiFailed', { - defaultValue: 'Goal mode AI request failed. Check model configuration and try again.', - }), - activatedTitle: t('chatInput.goalActivated', { defaultValue: 'Session goal activated' }), + aiFailedMessage: t('chatInput.goalAiFailed'), + activatedTitle: t('chatInput.goalActivated'), }); if (!result) { @@ -1708,7 +1684,7 @@ export const ChatInput: React.FC = ({ const submitDeepreviewFromInput = useCallback(async () => { if (!effectiveTargetSessionId || !effectiveTargetSession) { notificationService.error( - t('chatInput.deepreviewNoSession', { defaultValue: 'No active session for /DeepReview' }) + t('chatInput.deepreviewNoSession') ); return; } @@ -1716,27 +1692,21 @@ export const ChatInput: React.FC = ({ const message = inputState.value.trim(); if (!isDeepReviewSlashCommand(message)) { notificationService.warning( - t('chatInput.deepreviewUsage', { - defaultValue: 'Use /DeepReview with optional focus text, for example /DeepReview review commit abc123 for security.', - }) + t('chatInput.deepreviewUsage') ); return; } if (isBtwSession) { notificationService.warning( - t('chatInput.deepreviewNestedDisabled', { - defaultValue: 'Deep Review can only be started from the main session.', - }), + t('chatInput.deepreviewNestedDisabled'), ); return; } if (shouldBlockDeepReviewCommand(message, currentReviewActivity)) { notificationService.warning( - t('chatInput.deepreviewBusy', { - defaultValue: 'A review is already running for this session. Stop or finish it before starting another Deep Review.', - }), + t('chatInput.deepreviewBusy'), ); return; } @@ -1779,9 +1749,7 @@ export const ChatInput: React.FC = ({ prompt, displayMessage: message, runManifest, - childSessionName: t('chatInput.deepreviewThreadTitle', { - defaultValue: 'Deep review', - }), + childSessionName: t('chatInput.deepreviewThreadTitle'), }); dispatchInput({ type: 'DEACTIVATE' }); } catch (error) { @@ -1795,7 +1763,7 @@ export const ChatInput: React.FC = ({ notificationService.error( getDeepReviewLaunchErrorMessage(error, t, t('error.unknown')), { - title: t('chatInput.deepreviewFailed', { defaultValue: 'Deep review failed' }), + title: t('chatInput.deepreviewFailed'), duration: 5000, } ); @@ -1825,7 +1793,7 @@ export const ChatInput: React.FC = ({ if (!command) { notificationService.warning( - t('chatInput.noMatchingCommand', { defaultValue: 'No matching command' }) + t('chatInput.noMatchingCommand') ); return; } @@ -1981,30 +1949,28 @@ export const ChatInput: React.FC = ({ if (message.toLowerCase().startsWith('/compact')) { notificationService.warning( - t('chatInput.compactUsage', { defaultValue: 'Use /compact without extra arguments.' }) + t('chatInput.compactUsage') ); return; } if (message.toLowerCase().startsWith('/usage')) { notificationService.warning( - t('chatInput.usageCommandUsage', { defaultValue: 'Use /usage without extra arguments.' }) + t('chatInput.usageCommandUsage') ); return; } if (message.toLowerCase().startsWith('/init')) { notificationService.warning( - t('chatInput.initUsage', { defaultValue: 'Use /init without extra arguments.' }) + t('chatInput.initUsage') ); return; } if (message.toLowerCase().startsWith('/goal') && !isGoalSlashCommand(message)) { notificationService.warning( - t('chatInput.goalUsage', { - defaultValue: 'Use /goal with optional focus text, for example /goal fix the login bug.', - }) + t('chatInput.goalUsage') ); return; } @@ -2014,7 +1980,6 @@ export const ChatInput: React.FC = ({ t('input.messageTooLarge', { max: CHAT_INPUT_CONFIG.largePaste.maxMessageChars, count: messageCharCount, - defaultValue: 'Message exceeds the maximum length of {{max}} characters ({{count}} provided).', }), { duration: 4000 } ); @@ -2212,12 +2177,12 @@ export const ChatInput: React.FC = ({ (e: React.SyntheticEvent) => { e.stopPropagation(); if (!currentSessionId) { - notificationService.error(t('btw.noSession', { defaultValue: 'No active session for /btw' })); + notificationService.error(t('btw.noSession')); return; } if (isBtwSession) { notificationService.warning( - t('btw.nestedDisabled', { defaultValue: 'Side questions cannot create another side question' }) + t('btw.nestedDisabled') ); return; } @@ -2234,11 +2199,11 @@ export const ChatInput: React.FC = ({ e.stopPropagation(); if (!currentSessionId) { - notificationService.error(t('btw.noSession', { defaultValue: 'No active session for /btw' })); + notificationService.error(t('btw.noSession')); return; } if (isBtwSession) { - notificationService.warning(t('btw.nestedDisabled', { defaultValue: 'Side questions cannot create another side question' })); + notificationService.warning(t('btw.nestedDisabled')); return; } @@ -2798,7 +2763,7 @@ export const ChatInput: React.FC = ({ diff --git a/src/web-ui/src/flow_chat/components/TurnHistoryPanel.tsx b/src/web-ui/src/flow_chat/components/TurnHistoryPanel.tsx index 56eb38aef..2008d3cda 100644 --- a/src/web-ui/src/flow_chat/components/TurnHistoryPanel.tsx +++ b/src/web-ui/src/flow_chat/components/TurnHistoryPanel.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { snapshotAPI } from '@/infrastructure/api'; +import { useI18n } from '@/infrastructure/i18n'; import type { TurnSnapshot } from '@/infrastructure/api/service-api/SnapshotAPI'; import { TurnRollbackButton } from './TurnRollbackButton'; import { createLogger } from '@/shared/utils/logger'; @@ -16,6 +17,7 @@ interface TurnHistoryPanelProps { * Shows all turns in the current session and allows rollback. */ export const TurnHistoryPanel: React.FC = ({ sessionId }) => { + const { formatDate } = useI18n('flow-chat'); const [turns, setTurns] = useState([]); const [loading, setLoading] = useState(false); const [currentTurnIndex, setCurrentTurnIndex] = useState(-1); @@ -96,7 +98,10 @@ export const TurnHistoryPanel: React.FC = ({ sessionId }) )}
- {new Date(turn.timestamp * 1000).toLocaleString()} + {formatDate(new Date(turn.timestamp * 1000), { + dateStyle: 'medium', + timeStyle: 'short', + })}
))} diff --git a/src/web-ui/src/flow_chat/components/UserMessage.tsx b/src/web-ui/src/flow_chat/components/UserMessage.tsx index fa67847cc..41a67531a 100644 --- a/src/web-ui/src/flow_chat/components/UserMessage.tsx +++ b/src/web-ui/src/flow_chat/components/UserMessage.tsx @@ -6,6 +6,7 @@ import React, { useMemo, useState, useRef, useEffect } from 'react'; import { File, Folder, Code, Image, Terminal, GitBranch, Link, FileText, GitPullRequest } from 'lucide-react'; import { Tag } from '@/component-library'; +import { useI18n } from '@/infrastructure/i18n'; import { shouldIgnoreCardToggleClick } from '@/shared/utils/textSelection'; import { SnapshotRollbackButton } from './SnapshotRollbackButton'; import './UserMessage.scss'; @@ -143,6 +144,7 @@ export const UserMessage: React.FC = React.memo(({ showSnapshotButton = false, isCurrentTurn = false }) => { + const { formatDate } = useI18n('flow-chat'); const messageContent = message || content || ''; const parts = useMemo(() => parseMessageContent(messageContent), [messageContent]); const [isExpanded, setIsExpanded] = useState(false); @@ -241,7 +243,10 @@ export const UserMessage: React.FC = React.memo(({
{showTimestamp && timestamp && (
- {new Date(timestamp).toLocaleTimeString()} + {formatDate(new Date(timestamp), { + hour: '2-digit', + minute: '2-digit', + })}
)} diff --git a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx index fd2bb1968..3a89d52d3 100644 --- a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx +++ b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx @@ -362,11 +362,9 @@ export const BtwSessionPanel: React.FC = ({ ? t('flowChatHeader.btwBackTooltipWithTurn', { title: parentLabel, turn: btwOrigin.parentTurnIndex, - defaultValue: `Go back to the source session: ${parentLabel} (Turn ${btwOrigin.parentTurnIndex})`, }) : t('flowChatHeader.btwBackTooltipWithoutTurn', { title: parentLabel, - defaultValue: `Go back to the source session: ${parentLabel}`, }); const remainingCount = actionBarRemediationItems.length - actionBarCompletedIds.size; @@ -383,48 +381,28 @@ export const BtwSessionPanel: React.FC = ({ switch (actionBarPhase) { case 'review_running': return isDeepReview - ? t('deepReviewActionBar.minimizedReviewRunningDeep', { - defaultValue: 'Deep Review running', - }) - : t('deepReviewActionBar.minimizedReviewRunningStandard', { - defaultValue: 'Code Review running', - }); + ? t('deepReviewActionBar.minimizedReviewRunningDeep') + : t('deepReviewActionBar.minimizedReviewRunningStandard'); case 'fix_running': return actionBarLastSubmittedAction === 'fix-review' - ? t('deepReviewActionBar.minimizedFixReview', { - defaultValue: 'Fixing and re-reviewing', - }) - : t('deepReviewActionBar.minimizedFix', { - defaultValue: 'Fixing', - }); + ? t('deepReviewActionBar.minimizedFixReview') + : t('deepReviewActionBar.minimizedFix'); case 'fix_completed': - return t('deepReviewActionBar.minimizedFixCompleted', { - defaultValue: 'Fix completed', - }); + return t('deepReviewActionBar.minimizedFixCompleted'); case 'fix_failed': case 'fix_timeout': case 'review_error': - return t('deepReviewActionBar.minimizedFixFailed', { - defaultValue: 'Needs attention', - }); + return t('deepReviewActionBar.minimizedFixFailed'); case 'review_interrupted': case 'resume_blocked': case 'resume_failed': - return t('deepReviewActionBar.minimizedReviewInterrupted', { - defaultValue: 'Review interrupted', - }); + return t('deepReviewActionBar.minimizedReviewInterrupted'); case 'resume_running': - return t('deepReviewActionBar.minimizedResume', { - defaultValue: 'Continuing review', - }); + return t('deepReviewActionBar.minimizedResume'); default: return isDeepReview - ? t('deepReviewActionBar.minimizedDeep', { - defaultValue: 'Deep Review', - }) - : t('deepReviewActionBar.minimizedStandard', { - defaultValue: 'Code Review', - }); + ? t('deepReviewActionBar.minimizedDeep') + : t('deepReviewActionBar.minimizedStandard'); } }, [actionBarPhase, actionBarLastSubmittedAction, isDeepReview, t]); @@ -764,9 +742,7 @@ export const BtwSessionPanel: React.FC = ({ } catch (error) { log.error('Failed to stop review session', { childSessionId, error }); notificationService.error( - t('childSession.stopReviewFailed', { - defaultValue: 'Failed to stop the review session.', - }), + t('childSession.stopReviewFailed'), ); } finally { setStoppingReview(false); @@ -830,11 +806,11 @@ export const BtwSessionPanel: React.FC = ({ onClick={() => void handleStopReviewSession()} disabled={!canStopReviewSession} tooltip={stoppingReview - ? t('childSession.stoppingReview', { defaultValue: 'Stopping review...' }) - : t('childSession.stopReview', { defaultValue: 'Stop review' })} + ? t('childSession.stoppingReview') + : t('childSession.stopReview')} aria-label={stoppingReview - ? t('childSession.stoppingReview', { defaultValue: 'Stopping review...' }) - : t('childSession.stopReview', { defaultValue: 'Stop review' })} + ? t('childSession.stoppingReview') + : t('childSession.stopReview')} data-testid="btw-session-panel-stop-review" > @@ -892,7 +868,6 @@ export const BtwSessionPanel: React.FC = ({ className="btw-session-panel__minimized-button" aria-label={t('deepReviewActionBar.restore', { label: minimizedActionLabel, - defaultValue: `Open ${minimizedActionLabel}`, })} > diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx index 7beed29d0..f5a6c3800 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx @@ -20,18 +20,18 @@ const buildRecoveryPlanMock = vi.hoisted(() => vi.fn(() => ({ const controlDeepReviewQueueMock = vi.hoisted(() => vi.fn()); const flowChatSessionsMock = vi.hoisted(() => new Map()); -vi.mock('react-i18next', () => ({ - initReactI18next: { - type: '3rdParty', - init: vi.fn(), - }, - useTranslation: () => ({ - t: (_key: string, options?: Record & { defaultValue?: string }) => { - const template = options?.defaultValue ?? _key; - return template.replace(/{{(\w+)}}/g, (_match, token: string) => String(options?.[token] ?? _match)); +vi.mock('react-i18next', async () => { + const { createTestI18nT } = await import('@/test/i18nTestUtils'); + return { + initReactI18next: { + type: '3rdParty', + init: vi.fn(), }, - }), -})); + useTranslation: () => ({ + t: createTestI18nT('flow-chat'), + }), + }; +}); vi.mock('@/component-library', () => ({ Button: ({ @@ -287,7 +287,7 @@ describeWithJsdom('DeepReviewActionBar', () => { }); const fixAndReviewButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Fix and re-review')); + .find((button) => button.textContent?.includes('Fix & re-review')); expect(fixAndReviewButton).toBeTruthy(); @@ -746,7 +746,7 @@ describeWithJsdom('DeepReviewActionBar', () => { }); const fixAndReviewButton = Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Fix and re-review')); + .find((button) => button.textContent?.includes('Fix & re-review')); expect(fixAndReviewButton).toBeTruthy(); await act(async () => { @@ -754,7 +754,7 @@ describeWithJsdom('DeepReviewActionBar', () => { await Promise.resolve(); }); - expect(container.textContent).toContain('Fixing and preparing re-review...'); + expect(container.textContent).toContain('Fixing & preparing re-review...'); }); it('requires explicit decision confirmation before executing selected decision remediation', async () => { diff --git a/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx b/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx index b14bfb519..df868dfc0 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx +++ b/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx @@ -480,12 +480,11 @@ export const SessionFilesBadge: React.FC = ({ if (fileStats.size === 0) return ''; const head = t('sessionFilesBadge.filesSummaryCount', { count: fileStats.size, - defaultValue: '{{count}} files', }); const deltas: string[] = []; if (totalStats.totalAdditions > 0) deltas.push(`+${totalStats.totalAdditions}`); if (totalStats.totalDeletions > 0) deltas.push(`-${totalStats.totalDeletions}`); - const cue = t('sessionFilesBadge.expandChangeListCue', { defaultValue: 'Expand list' }); + const cue = t('sessionFilesBadge.expandChangeListCue'); return deltas.length > 0 ? `${head} · ${deltas.join(' ')} · ${cue}` : `${head} · ${cue}`; }, [fileStats.size, totalStats.totalAdditions, totalStats.totalDeletions, t]); @@ -493,14 +492,11 @@ export const SessionFilesBadge: React.FC = ({ if (fileStats.size === 0) return ''; const head = t('sessionFilesBadge.filesSummaryCount', { count: fileStats.size, - defaultValue: '{{count}} files', }); const deltas: string[] = []; if (totalStats.totalAdditions > 0) deltas.push(`+${totalStats.totalAdditions}`); if (totalStats.totalDeletions > 0) deltas.push(`-${totalStats.totalDeletions}`); - const cue = t('sessionFilesBadge.expandChangeListAriaCue', { - defaultValue: 'Expand to show files', - }); + const cue = t('sessionFilesBadge.expandChangeListAriaCue'); return deltas.length > 0 ? `${head}, ${deltas.join(' ')}, ${cue}` : `${head}, ${cue}`; }, [fileStats.size, totalStats.totalAdditions, totalStats.totalDeletions, t]); @@ -600,9 +596,7 @@ export const SessionFilesBadge: React.FC = ({ if (reviewableFilePaths.length === 0) { notificationService.warning( - t('sessionFilesBadge.review.noEligibleFiles', { - defaultValue: 'No reviewable files remain after excluded files were filtered out.', - }), + t('sessionFilesBadge.review.noEligibleFiles'), { duration: 3500 } ); return; @@ -613,8 +607,6 @@ export const SessionFilesBadge: React.FC = ({ t('sessionFilesBadge.review.filteredNotice', { included: reviewableFilePaths.length, skipped: skippedCount, - defaultValue: - 'Review will analyze {{included}} files and skip {{skipped}} excluded files such as lock, generated, or binary assets.', }), { duration: 3500 } ); @@ -625,16 +617,12 @@ export const SessionFilesBadge: React.FC = ({ ? t('sessionFilesBadge.review.displayMessageFiltered', { files: fileList, skipped: skippedCount, - defaultValue: - 'Review filtered files:\n{{files}}\n\nSkipped {{skipped}} excluded files.', }) : t('sessionFilesBadge.review.displayMessage', { files: fileList }); const reviewMessage = skippedCount > 0 ? t('sessionFilesBadge.review.promptFiltered', { files: fileList, skipped: skippedCount, - defaultValue: - 'Please review the following modified files in this session:\n\n{{files}}\n\nDo not review the {{skipped}} skipped files because they matched the excluded lock, generated, or binary file rules.', }) : t('sessionFilesBadge.review.prompt', { files: fileList }); @@ -642,9 +630,7 @@ export const SessionFilesBadge: React.FC = ({ try { const { FlowChatManager } = await import('../../services/FlowChatManager'); const flowChatManager = FlowChatManager.getInstance(); - const reviewThreadTitle = t('sessionFilesBadge.review.threadTitle', { - defaultValue: 'Code review', - }); + const reviewThreadTitle = t('sessionFilesBadge.review.threadTitle'); const created = await createBtwChildSession({ parentSessionId: sessionId, workspacePath: currentWorkspace?.rootPath, @@ -704,9 +690,7 @@ export const SessionFilesBadge: React.FC = ({ if (reviewableFilePaths.length === 0) { notificationService.warning( - t('sessionFilesBadge.review.noEligibleFiles', { - defaultValue: 'No reviewable files remain after excluded files were filtered out.', - }), + t('sessionFilesBadge.review.noEligibleFiles'), { duration: 3500 } ); return; @@ -717,12 +701,9 @@ export const SessionFilesBadge: React.FC = ({ ? t('sessionFilesBadge.deepReview.displayMessageFiltered', { files: fileList, skipped: skippedCount, - defaultValue: - 'Deep review filtered files:\n{{files}}\n\nSkipped {{skipped}} excluded files.', }) : t('sessionFilesBadge.deepReview.displayMessage', { files: fileList, - defaultValue: 'Deep review modified files:\n{{files}}', }); try { @@ -746,8 +727,6 @@ export const SessionFilesBadge: React.FC = ({ t('sessionFilesBadge.review.filteredNotice', { included: reviewableFilePaths.length, skipped: skippedCount, - defaultValue: - 'Review will analyze {{included}} files and skip {{skipped}} excluded files such as lock, generated, or binary assets.', }), { duration: 3500 } ); @@ -765,9 +744,7 @@ export const SessionFilesBadge: React.FC = ({ prompt, displayMessage, runManifest, - childSessionName: t('sessionFilesBadge.deepReview.threadTitle', { - defaultValue: 'Deep review', - }), + childSessionName: t('sessionFilesBadge.deepReview.threadTitle'), requestedFiles: reviewableFilePaths, }); @@ -813,10 +790,8 @@ export const SessionFilesBadge: React.FC = ({ const activeReviewMode = launchingReviewMode ?? (reviewActivity?.isBlocking ? reviewActivity.kind : null) ?? null; const reviewButtonTitle = activeReviewMode - ? t('sessionFilesBadge.reviewRunningHint', { - defaultValue: 'Wait for the current review to finish or stop it from the review page.', - }) - : t('sessionFilesBadge.actionsMenuHint', { defaultValue: 'Quick actions' }); + ? t('sessionFilesBadge.reviewRunningHint') + : t('sessionFilesBadge.actionsMenuHint'); // Hide when there is no session or parent disabled. Actions menu (reviews + quick actions) // renders first; file-change summary appears after we have stats. @@ -861,8 +836,8 @@ export const SessionFilesBadge: React.FC = ({ > {activeReviewMode - ? t('sessionFilesBadge.actionsButtonRunning', { defaultValue: 'Busy…' }) - : t('sessionFilesBadge.actionsButton', { defaultValue: 'Actions' })} + ? t('sessionFilesBadge.actionsButtonRunning') + : t('sessionFilesBadge.actionsButton')} {!activeReviewMode ? ( = ({ title={fileChangeToggleHint} aria-label={ isExpanded - ? t('sessionFilesBadge.collapseFileDiffList', { defaultValue: 'Collapse file change list' }) + ? t('sessionFilesBadge.collapseFileDiffList') : fileChangeToggleAriaCollapsed } aria-expanded={isExpanded} @@ -976,7 +951,6 @@ export const SessionFilesBadge: React.FC = ({ {t('sessionFilesBadge.filesSummaryCount', { count: fileStats.size, - defaultValue: '{{count}} files', })} {(totalStats.totalAdditions > 0 || totalStats.totalDeletions > 0) && ( diff --git a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx index 2f6862cda..122d75e62 100644 --- a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx +++ b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx @@ -4,7 +4,6 @@ */ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; import { Copy, Check, RotateCcw, Loader2, ArrowDownToLine, X, CircleUser, Pencil } from 'lucide-react'; import type { DialogTurn, FlowUserSteeringItem } from '../../types/flow-chat'; import { flowChatManager } from '../../services/FlowChatManager'; @@ -13,6 +12,7 @@ import { useActiveSession } from '../../store/modernFlowChatStore'; import { flowChatStore } from '../../store/FlowChatStore'; import { useMessageEditStore } from '../../store/messageEditStore'; import { snapshotAPI } from '@/infrastructure/api'; +import { useI18n } from '@/infrastructure/i18n'; import { notificationService } from '@/shared/notification-system'; import { globalEventBus } from '@/infrastructure/event-bus'; import { shouldIgnoreCardToggleClick } from '@/shared/utils/textSelection'; @@ -40,7 +40,7 @@ interface UserMessageItemProps { export const UserMessageItem = React.memo( ({ message, turnId, steeringStatus }) => { - const { t } = useTranslation('flow-chat'); + const { t, formatDate } = useI18n('flow-chat'); const { config, sessionId, @@ -380,7 +380,10 @@ export const UserMessageItem = React.memo( > {config?.showTimestamps && (
- {new Date(message.timestamp).toLocaleTimeString()} + {formatDate(new Date(message.timestamp), { + hour: '2-digit', + minute: '2-digit', + })}
)} {isEditing ? ( diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx index b41759007..b85c946f5 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.test.tsx @@ -3,17 +3,14 @@ import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import { CapacityQueueNotice } from './CapacityQueueNotice'; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (_key: string, options?: Record & { defaultValue?: string }) => { - if (_key === 'deepReviewActionBar.capacityQueue.reasons.launchBatchBlocked') { - return 'previous launch batch still running'; - } - const template = options?.defaultValue ?? _key; - return template.replace(/{{(\w+)}}/g, (_match, token: string) => String(options?.[token] ?? _match)); - }, - }), -})); +vi.mock('react-i18next', async () => { + const { createTestI18nT } = await import('@/test/i18nTestUtils'); + return { + useTranslation: () => ({ + t: createTestI18nT('flow-chat'), + }), + }; +}); vi.mock('@/component-library', () => ({ Button: ({ diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx index 0ef788037..9975ce483 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx @@ -127,31 +127,17 @@ export const CapacityQueueNotice: React.FC = ({ && capacityQueueState.maxQueueWaitSeconds !== undefined && capacityQueueState.queueElapsedMs > capacityQueueState.maxQueueWaitSeconds * 1000; const capacityQueueTitle = capacityQueueState.status === 'paused_by_user' - ? t('deepReviewActionBar.capacityQueue.pausedTitle', { - defaultValue: 'Queue paused', - }) + ? t('deepReviewActionBar.capacityQueue.pausedTitle') : capacityQueueWaitMode === 'active_reviewer' - ? t('deepReviewActionBar.capacityQueue.activeReviewerTitle', { - defaultValue: 'Waiting for running reviewers', - }) + ? t('deepReviewActionBar.capacityQueue.activeReviewerTitle') : capacityQueueWaitMode === 'provider_capacity' - ? t('deepReviewActionBar.capacityQueue.providerTitle', { - defaultValue: 'Waiting for model capacity', - }) - : t('deepReviewActionBar.capacityQueue.title', { - defaultValue: 'Reviewers waiting for capacity', - }); + ? t('deepReviewActionBar.capacityQueue.providerTitle') + : t('deepReviewActionBar.capacityQueue.title'); const capacityQueueDetail = capacityQueueWaitMode === 'active_reviewer' - ? t('deepReviewActionBar.capacityQueue.activeReviewerDetail', { - defaultValue: 'Queued reviewers start when a running reviewer frees capacity. Queue wait does not count against reviewer runtime.', - }) + ? t('deepReviewActionBar.capacityQueue.activeReviewerDetail') : capacityQueueWaitMode === 'provider_capacity' - ? t('deepReviewActionBar.capacityQueue.providerDetail', { - defaultValue: 'BitFun is waiting for temporary model capacity. This wait does not count against reviewer runtime.', - }) - : t('deepReviewActionBar.capacityQueue.detail', { - defaultValue: 'Queue wait does not count against reviewer runtime.', - }); + ? t('deepReviewActionBar.capacityQueue.providerDetail') + : t('deepReviewActionBar.capacityQueue.detail'); const waitingReviewers = capacityQueueState.waitingReviewers ?? []; const showCapacityQueueMeta = Boolean( capacityQueueReasonLabel @@ -176,7 +162,6 @@ export const CapacityQueueNotice: React.FC = ({ {t('deepReviewActionBar.capacityQueue.reason', { reason: capacityQueueReasonLabel, - defaultValue: `Reason: ${capacityQueueReasonLabel}`, })} )} @@ -186,11 +171,9 @@ export const CapacityQueueNotice: React.FC = ({ ? t('deepReviewActionBar.capacityQueue.elapsedWithMax', { elapsed: capacityQueueElapsedLabel, max: capacityQueueMaxWaitLabel, - defaultValue: `Waited ${capacityQueueElapsedLabel} of ${capacityQueueMaxWaitLabel}`, }) : t('deepReviewActionBar.capacityQueue.elapsed', { elapsed: capacityQueueElapsedLabel, - defaultValue: `Waited ${capacityQueueElapsedLabel}`, })} )} @@ -198,7 +181,6 @@ export const CapacityQueueNotice: React.FC = ({ {t('deepReviewActionBar.capacityQueue.activeReviewerCount', { count: activeReviewerCount, - defaultValue: `Running reviewers: ${activeReviewerCount}`, })} )} @@ -211,24 +193,18 @@ export const CapacityQueueNotice: React.FC = ({ )} {isLongLaunchBatchWait && ( - {t('deepReviewActionBar.capacityQueue.longLaunchBatchWaitDetail', { - defaultValue: 'This reviewer has waited longer than the configured queue window because an earlier reviewer batch is still running. You can keep waiting, pause the queue, cancel queued reviewers, or open Review settings.', - })} + {t('deepReviewActionBar.capacityQueue.longLaunchBatchWaitDetail')} )} {capacityQueueState.sessionConcurrencyHigh && ( - {t('deepReviewActionBar.capacityQueue.sessionBusy', { - defaultValue: 'Your active session is busy. Pause Deep Review or continue later.', - })} + {t('deepReviewActionBar.capacityQueue.sessionBusy')} )} {waitingReviewers.length > 0 && (
- {t('deepReviewActionBar.capacityQueue.waitingReviewersTitle', { - defaultValue: 'Waiting reviewers', - })} + {t('deepReviewActionBar.capacityQueue.waitingReviewersTitle')}
{waitingReviewers.map((reviewer) => { @@ -237,12 +213,8 @@ export const CapacityQueueNotice: React.FC = ({ ? formatElapsedTime(reviewer.queueElapsedMs) : null; const statusLabel = reviewer.status === 'paused_by_user' - ? t('deepReviewActionBar.capacityQueue.reviewerStatusPaused', { - defaultValue: 'Paused', - }) - : t('deepReviewActionBar.capacityQueue.reviewerStatusQueued', { - defaultValue: 'Waiting', - }); + ? t('deepReviewActionBar.capacityQueue.reviewerStatusPaused') + : t('deepReviewActionBar.capacityQueue.reviewerStatusQueued'); return ( = ({ {reviewer.optional && ( <> {' / '} - {t('deepReviewActionBar.capacityQueue.optionalReviewer', { - defaultValue: 'Optional', - })} + {t('deepReviewActionBar.capacityQueue.optionalReviewer')} )} {reviewerElapsed && ( @@ -266,7 +236,6 @@ export const CapacityQueueNotice: React.FC = ({ {' / '} {t('deepReviewActionBar.capacityQueue.elapsed', { elapsed: reviewerElapsed, - defaultValue: `Waited ${reviewerElapsed}`, })} )} @@ -279,9 +248,7 @@ export const CapacityQueueNotice: React.FC = ({ )} {!supportsInlineQueueControls && ( - {t('deepReviewActionBar.capacityQueue.stopHint', { - defaultValue: 'Use Stop to interrupt this review queue.', - })} + {t('deepReviewActionBar.capacityQueue.stopHint')} )}
@@ -296,9 +263,7 @@ export const CapacityQueueNotice: React.FC = ({ onClick={() => void onContinueQueue()} > - {t('deepReviewActionBar.capacityQueue.continueQueue', { - defaultValue: 'Continue queue', - })} + {t('deepReviewActionBar.capacityQueue.continueQueue')} ) : ( )} {(capacityQueueState.optionalReviewerCount ?? 0) > 0 && ( @@ -319,9 +282,7 @@ export const CapacityQueueNotice: React.FC = ({ onClick={() => void onSkipOptionalQueuedReviewers()} > - {t('deepReviewActionBar.capacityQueue.skipOptionalQueued', { - defaultValue: 'Skip optional extras', - })} + {t('deepReviewActionBar.capacityQueue.skipOptionalQueued')} )} )} @@ -340,9 +299,7 @@ export const CapacityQueueNotice: React.FC = ({ size="small" onClick={() => void onOpenReviewSettings()} > - {t('deepReviewActionBar.capacityQueue.openReviewSettings', { - defaultValue: 'Open Review settings', - })} + {t('deepReviewActionBar.capacityQueue.openReviewSettings')}
diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx index fc1324247..80511661e 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx @@ -199,9 +199,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId const toolIds = buildCapacityQueueControlToolIds(capacityQueueState, action); if (!childSessionId || !capacityQueueState.dialogTurnId || toolIds.length === 0) { - notificationService.error(t('deepReviewActionBar.capacityQueue.controlFailed', { - defaultValue: 'Queue control is unavailable because this reviewer is missing session, turn, or tool identifiers. Use Stop to interrupt the review, or wait for the queue state to refresh.', - })); + notificationService.error(t('deepReviewActionBar.capacityQueue.controlFailed')); return; } @@ -223,13 +221,11 @@ export const ReviewActionBar: React.FC = ({ childSessionId failed: failedResults.length, total: toolIds.length, reason, - defaultValue: `Queue control partly applied; ${failedResults.length} of ${toolIds.length} reviewers failed: ${reason}. Wait for the queue state to refresh, then retry or use Stop if it is stuck.`, })); return; } notificationService.error(t('deepReviewActionBar.capacityQueue.controlFailedWithReason', { reason, - defaultValue: 'Queue control failed: {{reason}}. Try again, or use Stop to interrupt the review if the queue is stuck.', })); return; } @@ -273,7 +269,6 @@ export const ReviewActionBar: React.FC = ({ childSessionId return t('deepReviewActionBar.progressResumePreserved', { preserved: progressSummary.completed, total: progressSummary.total, - defaultValue: `${progressSummary.completed}/${progressSummary.total} preserved, continuing remaining review`, }); } return t('deepReviewActionBar.progressHandled', { @@ -331,9 +326,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId if (elapsed > 3 * 60 * 1000 && !longRunningNotified) { setLongRunningNotified(true); notificationService.info( - t('deepReviewActionBar.longRunningHint', { - defaultValue: 'Review is still running. This may take a few more minutes.', - }), + t('deepReviewActionBar.longRunningHint'), { duration: 5000 }, ); } @@ -494,7 +487,6 @@ export const ReviewActionBar: React.FC = ({ childSessionId childSessionId, t('deepReviewActionBar.retryIncompleteRequestDisplay', { count: retryableSlices.length, - defaultValue: `Retry ${retryableSlices.length} incomplete Deep Review slice(s)`, }), 'DeepReview', 'agentic', @@ -504,9 +496,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId log.error('Failed to start DeepReview retry slices', { childSessionId, error }); const message = error instanceof Error ? error.message - : t('deepReviewActionBar.retryIncompleteFailed', { - defaultValue: 'Unable to retry incomplete Deep Review slices.', - }); + : t('deepReviewActionBar.retryIncompleteFailed'); notificationService.error(message, { duration: 5000 }); } finally { store.setActiveAction(null, undefined, childSessionId); @@ -537,16 +527,10 @@ export const ReviewActionBar: React.FC = ({ childSessionId if (currentInput.trim()) { const confirmed = await confirmWarning( - t('deepReviewActionBar.replaceInputConfirmTitle', { - defaultValue: 'Replace current input?', - }), - t('deepReviewActionBar.replaceInputConfirmMessage', { - defaultValue: 'The chat input already has text. Filling this plan will replace the current draft.', - }), + t('deepReviewActionBar.replaceInputConfirmTitle'), + t('deepReviewActionBar.replaceInputConfirmMessage'), { - confirmText: t('deepReviewActionBar.replaceInputConfirmAction', { - defaultValue: 'Replace input', - }), + confirmText: t('deepReviewActionBar.replaceInputConfirmAction'), }, ); if (!confirmed) return; @@ -569,16 +553,10 @@ export const ReviewActionBar: React.FC = ({ childSessionId if (!interruption.canResume) { const confirmed = await confirmWarning( - t('deepReviewActionBar.resumeBlockedConfirmTitle', { - defaultValue: 'Continue review?', - }), - t('deepReviewActionBar.resumeBlockedConfirmMessage', { - defaultValue: 'The error that interrupted the review has not been resolved. Continuing may fail again. Do you want to proceed?', - }), + t('deepReviewActionBar.resumeBlockedConfirmTitle'), + t('deepReviewActionBar.resumeBlockedConfirmMessage'), { - confirmText: t('deepReviewActionBar.resumeBlockedConfirmAction', { - defaultValue: 'Continue anyway', - }), + confirmText: t('deepReviewActionBar.resumeBlockedConfirmAction'), }, ); if (!confirmed) return; @@ -589,14 +567,10 @@ export const ReviewActionBar: React.FC = ({ childSessionId store.updatePhase('resume_running', undefined, childSessionId ?? undefined); store.minimize(childSessionId ?? undefined); try { - await continueDeepReviewSession(interruption, t('deepReviewActionBar.resumeRequestDisplay', { - defaultValue: 'Continue interrupted Deep Review', - }), { force: !interruption.canResume }); + await continueDeepReviewSession(interruption, t('deepReviewActionBar.resumeRequestDisplay'), { force: !interruption.canResume }); } catch (error) { log.error('Failed to continue interrupted Deep Review', { childSessionId, error }); - const message = t('deepReviewActionBar.resumeFailedMessage', { - defaultValue: 'Unable to continue Deep Review. Check the model settings or try again later.', - }); + const message = t('deepReviewActionBar.resumeFailedMessage'); store.updatePhase('resume_failed', message, childSessionId ?? undefined); store.restore(childSessionId ?? undefined); notificationService.error(message, { duration: 5000 }); @@ -628,16 +602,12 @@ export const ReviewActionBar: React.FC = ({ childSessionId setShowPartialResults(true); } else if (type === 'reduce_reviewers') { notificationService.info( - t('deepReviewActionBar.degradation.reduceReviewersPending', { - defaultValue: 'Reduced reviewer mode will be supported in a future update.', - }), + t('deepReviewActionBar.degradation.reduceReviewersPending'), { duration: 3000 }, ); } else if (type === 'compress_context') { notificationService.info( - t('deepReviewActionBar.degradation.compressContextPending', { - defaultValue: 'Context compression will be supported in a future update.', - }), + t('deepReviewActionBar.degradation.compressContextPending'), { duration: 3000 }, ); } @@ -651,13 +621,9 @@ export const ReviewActionBar: React.FC = ({ childSessionId try { await navigator.clipboard.writeText(diagnostics); - notificationService.success(t('deepReviewActionBar.diagnosticsCopied', { - defaultValue: 'Diagnostics copied', - }), { duration: 2500 }); + notificationService.success(t('deepReviewActionBar.diagnosticsCopied'), { duration: 2500 }); } catch { - notificationService.error(t('deepReviewActionBar.diagnosticsCopyFailed', { - defaultValue: 'Failed to copy diagnostics', - }), { duration: 2500 }); + notificationService.error(t('deepReviewActionBar.diagnosticsCopyFailed'), { duration: 2500 }); } }, [interruption, t]); @@ -667,25 +633,21 @@ export const ReviewActionBar: React.FC = ({ childSessionId if (phase === 'review_interrupted') { return t('deepReviewActionBar.reviewInterruptedWithReason', { reason: categoryLabel, - defaultValue: `Deep review interrupted: ${categoryLabel}`, }); } if (phase === 'resume_blocked') { return t('deepReviewActionBar.resumeBlockedWithReason', { reason: categoryLabel, - defaultValue: `Cannot continue: ${categoryLabel}`, }); } if (phase === 'resume_failed') { return t('deepReviewActionBar.resumeFailedWithReason', { reason: categoryLabel, - defaultValue: `Continue failed: ${categoryLabel}`, }); } if (phase === 'review_error') { return t('deepReviewActionBar.reviewErrorWithReason', { reason: categoryLabel, - defaultValue: `Review error: ${categoryLabel}`, }); } } @@ -701,49 +663,27 @@ export const ReviewActionBar: React.FC = ({ childSessionId }); case 'fix_running': if (lastSubmittedAction === 'fix-review') { - return t('deepReviewActionBar.fixAndReviewRunning', { - defaultValue: 'Fixing and preparing re-review...', - }); + return t('deepReviewActionBar.fixAndReviewRunning'); } - return t('deepReviewActionBar.fixRunning', { - defaultValue: 'Fixing in progress...', - }); + return t('deepReviewActionBar.fixRunning'); case 'fix_completed': - return t('deepReviewActionBar.fixCompleted', { - defaultValue: 'Fix completed', - }); + return t('deepReviewActionBar.fixCompleted'); case 'fix_failed': - return t('deepReviewActionBar.fixFailed', { - defaultValue: 'Fix failed', - }); + return t('deepReviewActionBar.fixFailed'); case 'fix_timeout': - return t('deepReviewActionBar.fixTimeout', { - defaultValue: 'Fix timed out', - }); + return t('deepReviewActionBar.fixTimeout'); case 'review_waiting_capacity': - return t('deepReviewActionBar.reviewWaitingCapacity', { - defaultValue: 'Review queue waiting', - }); + return t('deepReviewActionBar.reviewWaitingCapacity'); case 'review_interrupted': - return t('deepReviewActionBar.reviewInterrupted', { - defaultValue: 'Deep review interrupted', - }); + return t('deepReviewActionBar.reviewInterrupted'); case 'resume_blocked': - return t('deepReviewActionBar.resumeBlocked', { - defaultValue: 'Action required before continuing', - }); + return t('deepReviewActionBar.resumeBlocked'); case 'resume_running': - return t('deepReviewActionBar.resumeRunning', { - defaultValue: 'Continuing review...', - }); + return t('deepReviewActionBar.resumeRunning'); case 'resume_failed': - return t('deepReviewActionBar.resumeFailed', { - defaultValue: 'Continue failed', - }); + return t('deepReviewActionBar.resumeFailed'); case 'review_error': - return t('deepReviewActionBar.reviewError', { - defaultValue: 'Review error', - }); + return t('deepReviewActionBar.reviewError'); default: return ''; } @@ -765,7 +705,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId phaseIconClass={phaseConfig.iconClass} phaseTitle={phaseTitle} errorMessage={errorMessage} - minimizeLabel={t('deepReviewActionBar.minimize', { defaultValue: 'Minimize' })} + minimizeLabel={t('deepReviewActionBar.minimize')} onMinimize={handleMinimize} /> @@ -779,7 +719,6 @@ export const ReviewActionBar: React.FC = ({ childSessionId {t('deepReviewActionBar.elapsedTime', { time: formatElapsedTime(elapsedMs), - defaultValue: `Running for ${formatElapsedTime(elapsedMs)}`, })} )} @@ -840,9 +779,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId {showInterruptionDetails && interruption?.errorDetail?.category === 'context_overflow' && (
- {t('deepReviewActionBar.contextOverflowTitle', { - defaultValue: 'Context limit reached. Choose how to proceed:', - })} + {t('deepReviewActionBar.contextOverflowTitle')} {degradationOptions.map((option) => ( {showCustomInput && (