diff --git a/AGENTS.md b/AGENTS.md index b130df399..6c1cc1887 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,9 +98,11 @@ For the full script list, see [`package.json`](package.json). - Web UI loads only bootstrap namespaces eagerly; use `useI18n(namespace)` for route or feature copy and keep direct `i18nService.t(...)` calls in bootstrap namespaces. +- Use shared i18n formatting helpers for user-visible dates, times, and + numbers instead of direct `Intl.*` or `toLocale*` calls. - `pnpm run i18n:audit` enforces key/placeholder parity, direct static key - existence, dynamic key governance, no-growth i18n governance baselines, and - the no-hardcoded-CJK source budget. + existence, dynamic key governance, no-growth i18n governance baselines, + locale-format no-growth baselines, and the no-hardcoded-CJK source budget. ### Logging diff --git a/BitFun-Installer/src/i18n/locales/zh-TW.json b/BitFun-Installer/src/i18n/locales/zh-TW.json index 7e68229c0..21296d385 100644 --- a/BitFun-Installer/src/i18n/locales/zh-TW.json +++ b/BitFun-Installer/src/i18n/locales/zh-TW.json @@ -47,7 +47,7 @@ "modelName": "模型名稱(如 deepseek-v4-flash)", "skip": "稍後配置", "nextTheme": "下一步:主題", - "description": "配置和管理 AI 模型提供商", + "description": "設定和管理 AI 模型提供商", "providerLabel": "選擇模型提供商", "selectProvider": "或選擇預設提供商", "customProvider": "自定義配置", @@ -71,7 +71,7 @@ "description": "MiniMax 系列模型", "urlOptions": { "default": "Anthropic格式-默認", - "openai": "OpenAI兼容格式" + "openai": "OpenAI 相容格式" } }, "moonshot": { @@ -127,13 +127,13 @@ "testSuccess": "測試成功", "testFailed": "測試失敗", "fillApiKeyBeforeFetch": "請先填寫 API Key 再獲取模型列表", - "fetchingModels": "正在拉取模型列表...", + "fetchingModels": "正在擷取模型清單...", "fetchFailedFallback": "拉取模型列表失敗,已回退到常用預設模型", "fetchEmptyFallback": "供應商未返回可用模型,已回退到常用預設模型", "usingPresetModels": "當前顯示的是常用預設模型", "addCustomModel": "添加自定義模型", "form": { - "baseUrl": "API地址", + "baseUrl": "API 位址", "apiKey": "API密鑰", "apiKeyPlaceholder": "輸入您的 API Key", "provider": "請求格式", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3100a6772..10c41371c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,9 +88,11 @@ Captured data is logged as structured JSON under the `bitfun::devtools` target. Web UI locale catalogs into mobile-web, installer, backend, or static pages. - Static self-contained pages may use generated page-scoped shared-term files instead of copying stable labels. +- User-visible dates, times, and numbers should use shared i18n formatting + helpers instead of direct `Intl.*` or `toLocale*` calls. - `pnpm run i18n:audit` enforces key/placeholder parity, direct static key - existence, dynamic key governance, no-growth i18n governance baselines, and - the no-hardcoded-CJK source budget. + existence, dynamic key governance, no-growth i18n governance baselines, + locale-format no-growth baselines, and the no-hardcoded-CJK source budget. ### Platform-agnostic core diff --git a/docs/architecture/i18n.md b/docs/architecture/i18n.md index 6a1dd73a2..cad58e510 100644 --- a/docs/architecture/i18n.md +++ b/docs/architecture/i18n.md @@ -183,6 +183,8 @@ The audit layer should distinguish these cases explicitly: - Stable concept duplicates: surface values that duplicate an existing `shared.*` term and should be migrated only when the local wording has the same product meaning. +- Direct locale-format candidates: user-visible date, time, number, or currency + formatting that bypasses the surface i18n formatting helper. Deletion is safe only for confirmed unused keys. Dynamic-key candidates need an explicit allowlist or a code-level mapping comment before cleanup. Localization @@ -220,6 +222,12 @@ Do not add checked-in execution plans for these governance improvements. Keep durable architecture and development rules in this document and `docs/development/i18n.md`; keep one-off rollout plans outside version control. +`scripts/i18n-literal-fallback-baseline.json` 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. + ## Backend And Frontend Language Contract All surfaces must exchange canonical app locale ids: diff --git a/docs/development/i18n.md b/docs/development/i18n.md index cc45aec4a..5adc8c0db 100644 --- a/docs/development/i18n.md +++ b/docs/development/i18n.md @@ -16,6 +16,8 @@ - Do not import `src/web-ui/src/locales` from `src/mobile-web`, `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. - 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. @@ -148,6 +150,12 @@ resources. Dynamic fallbacks are acceptable only when the fallback is real runtime data, 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. + Repeated values are not automatically wrong. Generic atoms such as `common:actions.cancel` should be reused when the meaning is identical, but feature-specific labels, button text, and status words may stay local when their @@ -167,9 +175,10 @@ static Web UI translation keys from `i18nService.t('namespace:key')`, `i18nService.getT()('namespace:key')`, and namespace-aware `useI18n(namespace)` / `useTranslation(namespace)` calls, direct `t(key, "literal fallback")` arguments, object-form literal `defaultValue` budget -growth, and CJK source candidates outside approved resource owners. Keep -user-facing copy in locale/resource files rather than raising hardcoded-copy or -literal-fallback baselines. +growth, direct locale-format budget growth, and CJK source candidates outside +approved resource owners. Keep user-facing copy and formatting decisions in +locale/resource or i18n helper files rather than raising hardcoded-copy, +literal-fallback, or locale-format baselines. Use `pnpm run i18n:audit -- --report-json ` when reviewing cleanup or shared-term work. The JSON report separates confirmed unused keys, dynamic-key @@ -260,4 +269,5 @@ Every i18n PR should state: - Whether generated files were updated. - Which i18n verification commands passed. - Whether any surface intentionally does not support a new locale. -- Whether a hardcoded-copy budget changed, and why that increase is acceptable. +- Whether any hardcoded-copy, literal-fallback, or locale-format budget changed, + and why any increase is acceptable. diff --git a/scripts/i18n-audit.mjs b/scripts/i18n-audit.mjs index e8c1bd734..2818edb28 100644 --- a/scripts/i18n-audit.mjs +++ b/scripts/i18n-audit.mjs @@ -8,6 +8,7 @@ const root = process.cwd(); const contractPath = path.join(root, 'src', 'shared', 'i18n', 'contract', 'locales.json'); const hardcodedBaselinePath = path.join(root, 'scripts', 'i18n-hardcoded-baseline.json'); const literalFallbackBaselinePath = path.join(root, 'scripts', 'i18n-literal-fallback-baseline.json'); +const localeFormatBaselinePath = path.join(root, 'scripts', 'i18n-locale-format-baseline.json'); const dynamicKeyAllowlistPath = path.join(root, 'scripts', 'i18n-dynamic-key-allowlist.json'); const l10nIdenticalAllowlistPath = path.join(root, 'scripts', 'i18n-l10n-identical-allowlist.json'); const governanceBaselinePath = path.join(root, 'scripts', 'i18n-governance-baseline.json'); @@ -48,6 +49,8 @@ const reportCategories = [ 'dynamicKeyCandidates', 'sharedTermDuplicates', 'l10nQualityCandidates', + 'literalDefaultValueFallbacks', + 'localeFormatCandidates', ]; const governanceReport = { version: 1, @@ -64,6 +67,8 @@ const governanceReport = { dynamicKeyCandidates: [], sharedTermDuplicates: [], l10nQualityCandidates: [], + literalDefaultValueFallbacks: [], + localeFormatCandidates: [], }; function reportError(message) { @@ -281,6 +286,14 @@ function finalizeGovernanceReport() { byNamespace: countEntriesBy(governanceReport.l10nQualityCandidates, 'namespace', { emptyLabel: '' }), bySurface: countEntriesBy(governanceReport.l10nQualityCandidates, 'surface'), }, + literalDefaultValueFallbacks: { + byFile: countEntriesBy(governanceReport.literalDefaultValueFallbacks, 'file'), + byNamespace: countEntriesBy(governanceReport.literalDefaultValueFallbacks, 'namespace', { emptyLabel: '' }), + }, + localeFormatCandidates: { + byFile: countEntriesBy(governanceReport.localeFormatCandidates, 'file'), + bySurface: countEntriesBy(governanceReport.localeFormatCandidates, 'surface'), + }, }; } @@ -1820,9 +1833,11 @@ function collectWebUiLiteralFallbackFindings() { if (propertyNameToString(ts, property.name) !== 'defaultValue') continue; if (!isLiteralFallbackInitializer(ts, property.initializer)) continue; + const [namespace, ...keyParts] = call.key.split(':'); findings.push({ file: call.file, location: call.location, + namespace: keyParts.length > 0 ? namespace : null, key: call.key, }); } @@ -1844,6 +1859,14 @@ function auditWebUiLiteralFallbackBudget() { const findingsByFile = new Map(); for (const finding of collectWebUiLiteralFallbackFindings()) { + governanceReport.literalDefaultValueFallbacks.push({ + surface: 'web-ui', + namespace: finding.namespace, + key: finding.key, + file: finding.file, + location: finding.location, + reason: 'literal-i18next-defaultValue', + }); findingsByFile.set(finding.file, [...(findingsByFile.get(finding.file) ?? []), finding]); } @@ -1933,6 +1956,130 @@ function countCjkSourceLines(scanRoot, predicate) { return findings; } +function shouldSkipLocaleFormatSourceScan(file) { + const normalized = toPosixPath(path.relative(root, file)); + return ( + normalized === 'src/web-ui/src/infrastructure/i18n/core/I18nService.ts' || + normalized.endsWith('/generatedLocaleContract.ts') || + normalized.endsWith('.test.ts') || + normalized.endsWith('.test.tsx') || + normalized.endsWith('.spec.ts') || + normalized.endsWith('.spec.tsx') + ); +} + +function collectLocaleFormatUsageFindings() { + const formatPattern = /\b(?:new\s+)?Intl\.(?:DateTimeFormat|NumberFormat|RelativeTimeFormat)\s*\(|\.\s*toLocale(?:String|DateString|TimeString)\s*\(/g; + const specs = [ + { + surface: 'web-ui', + root: webSourceDir, + predicate: (file) => ( + (file.endsWith('.ts') || file.endsWith('.tsx')) && + !shouldSkipSourceScan(file) && + !shouldSkipLocaleFormatSourceScan(file) + ), + }, + { + surface: 'mobile-web', + root: mobileWebSourceDir, + predicate: (file) => ( + (file.endsWith('.ts') || file.endsWith('.tsx')) && + !shouldSkipMobileWebSourceScan(file) && + !shouldSkipLocaleFormatSourceScan(file) + ), + }, + { + surface: 'installer', + root: installerSourceDir, + predicate: (file) => ( + (file.endsWith('.ts') || file.endsWith('.tsx')) && + !shouldSkipInstallerSourceScan(file) && + !shouldSkipLocaleFormatSourceScan(file) + ), + }, + { + surface: 'core-miniapp', + root: path.join(root, 'src', 'crates', 'core', 'src', 'miniapp', 'builtin', 'assets'), + predicate: (file) => file.endsWith('.js'), + }, + ]; + const findings = []; + + for (const spec of specs) { + for (const file of listFiles(spec.root, spec.predicate)) { + const relativeFile = toPosixPath(path.relative(root, file)); + const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); + lines.forEach((line, index) => { + formatPattern.lastIndex = 0; + let match; + while ((match = formatPattern.exec(line)) != null) { + findings.push({ + surface: spec.surface, + file: relativeFile, + location: `${relativeFile}:${index + 1}`, + expression: match[0].trim(), + snippet: line.trim().slice(0, 200), + reason: 'direct-locale-format-call', + }); + } + }); + } + } + + return findings.sort(sortByReportIdentity); +} + +function auditLocaleFormatUsageBudget() { + if (!fs.existsSync(localeFormatBaselinePath)) { + reportError('Missing scripts/i18n-locale-format-baseline.json'); + return; + } + + const baseline = readJsonFile(localeFormatBaselinePath); + if (baseline.version !== 1) { + reportError('scripts/i18n-locale-format-baseline.json must use version 1'); + } + if (!Array.isArray(baseline.budgets)) { + reportError('scripts/i18n-locale-format-baseline.json must define a budgets array'); + return; + } + + const budgetByFile = new Map((baseline.budgets ?? []).map((budget) => [budget.path, budget])); + const findingsByFile = new Map(); + + for (const finding of collectLocaleFormatUsageFindings()) { + governanceReport.localeFormatCandidates.push(finding); + findingsByFile.set(finding.file, [...(findingsByFile.get(finding.file) ?? []), finding]); + } + + for (const [file, findings] of findingsByFile.entries()) { + const budget = budgetByFile.get(file); + if (!budget) { + reportError( + `${file} has ${findings.length} direct locale formatting call(s) but is missing from scripts/i18n-locale-format-baseline.json. First entries: ${ + findings.slice(0, 8).map((finding) => `${finding.location} ${finding.expression}`).join(', ') + }`, + ); + continue; + } + + if (typeof budget.maxLocaleFormatCalls !== 'number') { + reportError(`${file} has an invalid locale format baseline entry`); + } else if (findings.length > budget.maxLocaleFormatCalls) { + reportError(`${file} has ${findings.length} direct locale formatting call(s), budget is ${budget.maxLocaleFormatCalls}`); + } else if (findings.length < budget.maxLocaleFormatCalls) { + reportError(`${file} has ${findings.length} direct locale formatting call(s), below baseline ${budget.maxLocaleFormatCalls}; lower scripts/i18n-locale-format-baseline.json.`); + } + } + + for (const [file, budget] of budgetByFile.entries()) { + if (budget.maxLocaleFormatCalls > 0 && !findingsByFile.has(file)) { + reportError(`${file} no longer has direct locale formatting call(s); remove it from scripts/i18n-locale-format-baseline.json.`); + } + } +} + function auditHardcodedSourceBudgets() { const baseline = readJsonFile(hardcodedBaselinePath); const budgetById = new Map((baseline.budgets ?? []).map((budget) => [budget.id, budget.maxCjkLines])); @@ -1997,6 +2144,7 @@ auditInstallerPlaceholderParity(); auditCoreFluentParity(); auditRelayStaticHomepageResources(); auditSourceText(); +auditLocaleFormatUsageBudget(); auditHardcodedSourceBudgets(); auditI18nGovernanceReport(namespaces); writeGovernanceReport(); diff --git a/scripts/i18n-contract.test.mjs b/scripts/i18n-contract.test.mjs index aa803dccd..1d63a3c41 100644 --- a/scripts/i18n-contract.test.mjs +++ b/scripts/i18n-contract.test.mjs @@ -318,6 +318,71 @@ test('i18n audit gates object-form literal fallbacks with an explicit budget', ( assert.match(auditSource, /defaultValue/, 'audit should inspect i18next defaultValue options'); }); +test('i18n audit reports literal fallback and locale formatting candidate baselines', { concurrency: false }, () => { + const auditSource = readText('scripts/i18n-audit.mjs'); + const localeFormatBaselineSource = readText('scripts/i18n-locale-format-baseline.json'); + const localeFormatBaseline = JSON.parse(localeFormatBaselineSource); + + assert.match(auditSource, /literalDefaultValueFallbacks/, 'governance report should include literal fallback candidates'); + assert.match(auditSource, /i18n-locale-format-baseline\.json/, 'audit should read the direct locale format baseline'); + assert.match(auditSource, /auditLocaleFormatUsageBudget/, 'direct locale formatting should have a no-growth gate'); + assert.ok( + localeFormatBaseline.budgets.every((entry) => ( + typeof entry.path === 'string' && + typeof entry.maxLocaleFormatCalls === 'number' + )), + 'locale format baseline should use exact per-file budgets', + ); + + const reportPath = 'scripts/.tmp-i18n-locale-format-report.json'; + const absoluteReportPath = path.join(root, reportPath); + fs.rmSync(absoluteReportPath, { force: true }); + + try { + const result = runI18nAudit(['--report-json', reportPath]); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + const report = readJson(reportPath); + assert.equal( + report.summary.byCategory.localeFormatCandidates.bySurface['web-ui'], + report.localeFormatCandidates.filter((entry) => entry.surface === 'web-ui').length, + 'report should summarize direct locale formatting candidates by surface', + ); + assert.ok( + report.summary.byCategory.literalDefaultValueFallbacks.byFile, + 'report should summarize literal fallbacks by file', + ); + } finally { + fs.rmSync(absoluteReportPath, { force: true }); + } + + const baselinePath = 'scripts/i18n-locale-format-baseline.json'; + const baseline = readJson(baselinePath); + baseline.budgets.push({ + path: 'src/web-ui/src/__removed_locale_format_fixture__.ts', + maxLocaleFormatCalls: 1, + }); + + withTemporaryTextFile(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`, () => { + const result = runI18nAudit(); + assert.notEqual(result.status, 0, 'stale direct locale format baselines must fail audit'); + assert.match( + `${result.stdout}\n${result.stderr}`, + /no longer has direct locale formatting call/, + 'audit output should explain the stale locale format budget', + ); + }); + + withTemporaryTextFile('src/web-ui/src/__locale_format_fixture__.ts', 'export const value = Intl.NumberFormat().format(1234);\n', () => { + const result = runI18nAudit(); + assert.notEqual(result.status, 0, 'direct Intl format calls without new must fail audit'); + assert.match( + `${result.stdout}\n${result.stderr}`, + /__locale_format_fixture__\.ts has 1 direct locale formatting call/, + 'audit output should report direct Intl format calls without new', + ); + }); +}); + test('i18n audit can emit a machine-readable governance report', { concurrency: false }, () => { const reportPath = 'scripts/.tmp-i18n-governance-report.json'; const absoluteReportPath = path.join(root, reportPath); @@ -335,6 +400,8 @@ test('i18n audit can emit a machine-readable governance report', { concurrency: assert.ok(Array.isArray(report.dynamicKeyCandidates), 'report should include dynamic key candidates'); assert.ok(Array.isArray(report.sharedTermDuplicates), 'report should include shared-term duplicate candidates'); assert.ok(Array.isArray(report.l10nQualityCandidates), 'report should include l10n quality candidates'); + assert.ok(Array.isArray(report.literalDefaultValueFallbacks), 'report should include literal defaultValue fallback candidates'); + assert.ok(Array.isArray(report.localeFormatCandidates), 'report should include direct locale formatting candidates'); assert.deepEqual( report.summary.counts, { @@ -342,6 +409,8 @@ test('i18n audit can emit a machine-readable governance report', { concurrency: dynamicKeyCandidates: report.dynamicKeyCandidates.length, sharedTermDuplicates: report.sharedTermDuplicates.length, l10nQualityCandidates: report.l10nQualityCandidates.length, + literalDefaultValueFallbacks: report.literalDefaultValueFallbacks.length, + localeFormatCandidates: report.localeFormatCandidates.length, }, 'report counts should match finding arrays', ); @@ -394,6 +463,14 @@ test('i18n audit can emit a machine-readable governance report', { concurrency: )), 'unchanged zh-TW copy should be reported as a localization quality candidate', ); + assert.ok( + report.literalDefaultValueFallbacks.every((entry) => entry.file && entry.key && entry.location), + 'literal fallback candidates should include file, key, and location for cleanup reviews', + ); + assert.ok( + report.localeFormatCandidates.every((entry) => entry.surface && entry.file && entry.location), + 'locale format candidates should include surface, file, and location for cleanup reviews', + ); assert.equal( report.l10nQualityCandidates.some((entry) => entry.resourceKey === 'shared:modes.agentic'), false, @@ -488,6 +565,7 @@ test('web-ui uses shared terms for stable navigation and feature labels', { conc 'flow-chat:toolCards.sessionControl.workspace', 'flow-chat:toolCards.sessionMessage.workspace', 'flow-chat:welcome.workspace', + 'scenes/skills:suite.modes.claw', 'settings:title', 'settings:configCenter.title', 'settings:workspace.title', @@ -508,6 +586,7 @@ test('web-ui uses shared terms for stable navigation and feature labels', { conc 'toolCards.sessionControl.workspace', 'toolCards.sessionMessage.workspace', 'welcome.workspace', + 'suite.modes.claw', 'configCenter.title', 'workspace.title', ]; diff --git a/scripts/i18n-governance-baseline.json b/scripts/i18n-governance-baseline.json index 7d6040ef6..1a4a6716b 100644 --- a/scripts/i18n-governance-baseline.json +++ b/scripts/i18n-governance-baseline.json @@ -6,16 +6,16 @@ "maxTotal": 0 }, "sharedTermDuplicates": { - "maxTotal": 206, + "maxTotal": 201, "bySurface": { "core": 15, "installer": 0, - "mobile-web": 8, + "mobile-web": 6, "relay-static-homepage": 0, - "web-ui": 183 + "web-ui": 180 }, "bySharedKey": { - "agents.claw": 6, + "agents.claw": 3, "agents.code": 4, "agents.cowork": 2, "agents.default": 3, @@ -37,21 +37,21 @@ "statuses.running": 13, "tools.edit": 33, "tools.explore": 2, - "tools.search": 14, + "tools.search": 12, "tools.shell": 8 } }, "l10nQualityCandidates": { - "maxTotal": 1093, + "maxTotal": 1087, "bySurface": { "core": 36, - "installer": 28, - "mobile-web": 21, + "installer": 24, + "mobile-web": 19, "relay-static-homepage": 1, "web-ui": 1007 }, "byNamespace": { - "": 86, + "": 80, "common": 158, "components": 87, "errors": 7, diff --git a/scripts/i18n-literal-fallback-baseline.json b/scripts/i18n-literal-fallback-baseline.json index b905349ff..ddb6596d4 100644 --- a/scripts/i18n-literal-fallback-baseline.json +++ b/scripts/i18n-literal-fallback-baseline.json @@ -2,12 +2,6 @@ "version": 1, "description": "Temporary no-growth allowlist for existing Web UI i18next defaultValue literal fallbacks. Lower this file when moving fallback copy into locale resources.", "budgets": [ - { - "path": "src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx", - "literalDefaultValues": { - "common:nav.sessions.loading": 1 - } - }, { "path": "src/web-ui/src/app/scenes/agents/AgentsScene.tsx", "literalDefaultValues": { @@ -66,12 +60,6 @@ "settings/default-model:selection.primary": 1 } }, - { - "path": "src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx", - "literalDefaultValues": { - "flow-chat:input.placeholder": 1 - } - }, { "path": "src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx", "literalDefaultValues": { @@ -403,66 +391,6 @@ "flow-chat:btw.threadLabel": 1 } }, - { - "path": "src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx", - "literalDefaultValues": { - "flow-chat:toolCards.codeReview.export.copyFailed": 1, - "flow-chat:toolCards.codeReview.export.copyMarkdown": 2, - "flow-chat:toolCards.codeReview.export.copySuccess": 1, - "flow-chat:toolCards.codeReview.export.editorTitle": 1, - "flow-chat:toolCards.codeReview.export.fileNamePrefix": 1, - "flow-chat:toolCards.codeReview.export.openFailed": 1, - "flow-chat:toolCards.codeReview.export.openMarkdown": 3, - "flow-chat:toolCards.codeReview.export.saveDialogTitle": 1, - "flow-chat:toolCards.codeReview.export.saveFailed": 1, - "flow-chat:toolCards.codeReview.export.saveMarkdown": 2, - "flow-chat:toolCards.codeReview.export.saveSuccess": 1, - "flow-chat:toolCards.codeReview.groups.architecture": 1, - "flow-chat:toolCards.codeReview.groups.maintainability": 1, - "flow-chat:toolCards.codeReview.groups.must_fix": 1, - "flow-chat:toolCards.codeReview.groups.needs_decision": 1, - "flow-chat:toolCards.codeReview.groups.other": 1, - "flow-chat:toolCards.codeReview.groups.performance": 1, - "flow-chat:toolCards.codeReview.groups.security": 1, - "flow-chat:toolCards.codeReview.groups.should_improve": 1, - "flow-chat:toolCards.codeReview.groups.tests": 1, - "flow-chat:toolCards.codeReview.groups.user_experience": 1, - "flow-chat:toolCards.codeReview.groups.verification": 1, - "flow-chat:toolCards.codeReview.recommendedAction": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.cache_hit.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.cache_miss.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.compression_preserved.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.concurrency_limited.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.context_pressure.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.partial_reviewer.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.reduced_scope.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.retry_guidance.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.skipped_reviewers.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.token_budget_limited.label": 1, - "flow-chat:toolCards.codeReview.reliabilityStatus.user_decision.label": 1, - "flow-chat:toolCards.codeReview.report.findings": 1, - "flow-chat:toolCards.codeReview.report.noIssues": 1, - "flow-chat:toolCards.codeReview.report.noItems": 1, - "flow-chat:toolCards.codeReview.report.packet": 1, - "flow-chat:toolCards.codeReview.report.partialOutput": 1, - "flow-chat:toolCards.codeReview.report.reliabilitySignals": 1, - "flow-chat:toolCards.codeReview.report.reviewDecision": 1, - "flow-chat:toolCards.codeReview.report.source": 1, - "flow-chat:toolCards.codeReview.report.status": 1, - "flow-chat:toolCards.codeReview.report.titleDeep": 1, - "flow-chat:toolCards.codeReview.report.titleStandard": 1, - "flow-chat:toolCards.codeReview.report.validation": 1, - "flow-chat:toolCards.codeReview.reviewScope": 1, - "flow-chat:toolCards.codeReview.riskLevel": 1, - "flow-chat:toolCards.codeReview.sections.coverage": 1, - "flow-chat:toolCards.codeReview.sections.issues": 1, - "flow-chat:toolCards.codeReview.sections.remediation": 1, - "flow-chat:toolCards.codeReview.sections.strengths": 1, - "flow-chat:toolCards.codeReview.sections.summary": 1, - "flow-chat:toolCards.codeReview.sections.team": 1, - "flow-chat:toolCards.codeReview.suggestion": 1 - } - }, { "path": "src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx", "literalDefaultValues": { diff --git a/scripts/i18n-locale-format-baseline.json b/scripts/i18n-locale-format-baseline.json new file mode 100644 index 000000000..06dadcb6e --- /dev/null +++ b/scripts/i18n-locale-format-baseline.json @@ -0,0 +1,66 @@ +{ + "version": 1, + "description": "Temporary no-growth baseline for direct locale formatting calls outside the shared i18n formatting APIs. Lower this file when callers move to i18n-owned format helpers.", + "budgets": [ + { + "path": "src/crates/core/src/miniapp/builtin/assets/coding-selfie/ui.js", + "maxLocaleFormatCalls": 1 + }, + { + "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/mobile-web/src/i18n/messages.ts b/src/mobile-web/src/i18n/messages.ts index 9850e913a..85dfa16c1 100644 --- a/src/mobile-web/src/i18n/messages.ts +++ b/src/mobile-web/src/i18n/messages.ts @@ -162,7 +162,6 @@ export const messages: Record = { grep: 'Grep', delete: 'Delete', task: 'Task', - search: 'Search', web: 'Web', }, }, @@ -321,7 +320,6 @@ export const messages: Record = { grep: 'Grep', delete: '删除', task: '任务', - search: '搜索', web: '网络', }, }, @@ -461,7 +459,7 @@ export const messages: Record = { readToolsDone: '{summary}', readToolsRunning: '{summary}(已完成 {doneCount})', fileLoading: '加載中...', - fileUnavailable: '文件不可用', + fileUnavailable: '檔案不可用', fileDownloading: '下載中...', fileDownloaded: '已下載', clickToDownload: '點擊下載', @@ -480,7 +478,6 @@ export const messages: Record = { grep: 'Grep', delete: '刪除', task: '任務', - search: '搜索', web: '網絡', }, } diff --git a/src/mobile-web/src/pages/ChatPage.tsx b/src/mobile-web/src/pages/ChatPage.tsx index 299e66e10..48e21ab7c 100644 --- a/src/mobile-web/src/pages/ChatPage.tsx +++ b/src/mobile-web/src/pages/ChatPage.tsx @@ -629,7 +629,7 @@ const TOOL_TYPE_MAP: Record = { create_file: 'shared.tools.write', delete_file: 'tools.delete', Task: 'tools.task', - search: 'tools.search', + search: 'shared.tools.search', edit_file: 'shared.tools.edit', web_search: 'tools.web', TodoWrite: 'shared.tools.todo', diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index a3f1b00ae..1187a8d59 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -607,7 +607,7 @@ const SessionsSection: React.FC = ({
- {t('nav.sessions.loading', { defaultValue: 'Loading sessions...' })} + {t('nav.sessions.loading')}
); diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx index 34b7ec33d..76fd2bb36 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx +++ b/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx @@ -89,7 +89,7 @@ const AssistantQuickInput: React.FC = ({ const placeholder = assistantName ? t('input.assistantPlaceholder', { name: assistantName }) - : t('input.placeholder', { defaultValue: 'Send a message…' }); + : t('input.placeholder'); return (
diff --git a/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx b/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx index f656690d6..6def77a3e 100644 --- a/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx +++ b/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx @@ -27,7 +27,7 @@ const SKILL_GROUP_ORDER: Record = { const SUITE_MODES = [ { id: 'agentic', labelKey: 'suite.modes.agentic', descKey: 'suite.modeDescriptions.agentic' }, { id: 'Cowork', labelKey: 'suite.modes.cowork', descKey: 'suite.modeDescriptions.cowork' }, - { id: 'Claw', labelKey: 'suite.modes.claw', descKey: 'suite.modeDescriptions.claw' }, + { id: 'Claw', labelKey: 'shared:agents.claw', descKey: 'suite.modeDescriptions.claw' }, { id: 'Team', labelKey: 'suite.modes.team', descKey: 'suite.modeDescriptions.team' }, ] as const; diff --git a/src/web-ui/src/flow_chat/components/TokenUsageIndicator.tsx b/src/web-ui/src/flow_chat/components/TokenUsageIndicator.tsx index cfd57ed8a..97d0e87e0 100644 --- a/src/web-ui/src/flow_chat/components/TokenUsageIndicator.tsx +++ b/src/web-ui/src/flow_chat/components/TokenUsageIndicator.tsx @@ -4,6 +4,7 @@ */ import React, { useMemo } from 'react'; +import { useI18n } from '@/infrastructure/i18n'; import './TokenUsageIndicator.scss'; export interface TokenUsageIndicatorProps { @@ -17,15 +18,12 @@ export const TokenUsageIndicator: React.FC = ({ maxTokens, className = '' }) => { + const { formatNumber } = useI18n(); const percentage = useMemo(() => { if (!maxTokens || maxTokens <= 0) return 0; return Math.min(Math.round((currentTokens / maxTokens) * 100), 100); }, [currentTokens, maxTokens]); - const formatNumber = (num: number): string => { - return num.toLocaleString('en-US'); - }; - const getStatusClass = (percent: number): string => { if (percent >= 90) return 'critical'; if (percent >= 70) return 'warning'; diff --git a/src/web-ui/src/flow_chat/components/usage/SessionUsageComponents.test.tsx b/src/web-ui/src/flow_chat/components/usage/SessionUsageComponents.test.tsx index 31e21bf58..b06289824 100644 --- a/src/web-ui/src/flow_chat/components/usage/SessionUsageComponents.test.tsx +++ b/src/web-ui/src/flow_chat/components/usage/SessionUsageComponents.test.tsx @@ -41,7 +41,8 @@ vi.mock('@/shared/utils/tabUtils', () => ({ createDiffEditorTab: tabUtilsMocks.createDiffEditorTab, })); -vi.mock('react-i18next', () => ({ +vi.mock('react-i18next', async (importOriginal) => ({ + ...(await importOriginal()), useTranslation: () => ({ t: (key: string, options?: Record) => { const labels: Record = { diff --git a/src/web-ui/src/flow_chat/components/usage/usageReportUtils.ts b/src/web-ui/src/flow_chat/components/usage/usageReportUtils.ts index 73024dda7..4ef7f3190 100644 --- a/src/web-ui/src/flow_chat/components/usage/usageReportUtils.ts +++ b/src/web-ui/src/flow_chat/components/usage/usageReportUtils.ts @@ -1,4 +1,5 @@ import type { SessionUsageReport } from '@/infrastructure/api/service-api/SessionAPI'; +import { i18nService } from '@/infrastructure/i18n'; type Translator = (key: string, options?: Record) => string; type ModelIdentitySource = SessionUsageReport['models'][number]['modelIdSource']; @@ -42,7 +43,7 @@ export function formatUsageNumber(value: number | undefined, t: Translator): str if (typeof value !== 'number' || !Number.isFinite(value)) { return t('usage.unavailable'); } - return new Intl.NumberFormat().format(value); + return i18nService.formatNumber(value); } export function formatUsageDuration(value: number | undefined, t: Translator): string { @@ -77,12 +78,12 @@ export function formatUsageTimestamp(value: number | undefined, t: Translator): if (typeof value !== 'number' || !Number.isFinite(value)) { return t('usage.unavailable'); } - return new Intl.DateTimeFormat(undefined, { + return i18nService.formatDate(new Date(value), { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', - }).format(new Date(value)); + }); } export function formatUsagePercent(value: number | undefined, t: Translator): string { diff --git a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx index eebd57409..b861301d0 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.test.tsx @@ -19,7 +19,14 @@ vi.mock('lucide-react', () => ({ vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (_key: string, options?: { defaultValue?: string }) => options?.defaultValue ?? _key, + t: (key: string) => { + const labels: Record = { + 'toolCards.codeReview.export.copyMarkdown': 'Copy Markdown', + 'toolCards.codeReview.export.openMarkdown': 'Open as Markdown', + 'toolCards.codeReview.export.saveMarkdown': 'Save Markdown', + }; + return labels[key] ?? key; + }, }), })); diff --git a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx index 7126577b7..3b2cf84ad 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx @@ -56,81 +56,59 @@ export const CodeReviewReportExportActions: React.FC new Set(actions), [actions]); const markdownLabels = useMemo>(() => ({ - titleStandard: t('toolCards.codeReview.report.titleStandard', { defaultValue: 'Code Review Report' }), - titleDeep: t('toolCards.codeReview.report.titleDeep', { defaultValue: 'Deep Review Report' }), - executiveSummary: t('toolCards.codeReview.sections.summary', { defaultValue: 'Executive Summary' }), - reviewDecision: t('toolCards.codeReview.report.reviewDecision', { defaultValue: 'Review Decision' }), + titleStandard: t('toolCards.codeReview.report.titleStandard'), + titleDeep: t('toolCards.codeReview.report.titleDeep'), + executiveSummary: t('toolCards.codeReview.sections.summary'), + reviewDecision: t('toolCards.codeReview.report.reviewDecision'), runManifest: t('toolCards.codeReview.sections.runManifest'), - riskLevel: t('toolCards.codeReview.riskLevel', { defaultValue: 'Risk Level' }), - recommendedAction: t('toolCards.codeReview.recommendedAction', { defaultValue: 'Recommended Action' }), - scope: t('toolCards.codeReview.reviewScope', { defaultValue: 'Scope' }), + riskLevel: t('toolCards.codeReview.riskLevel'), + recommendedAction: t('toolCards.codeReview.recommendedAction'), + scope: t('toolCards.codeReview.reviewScope'), target: t('toolCards.codeReview.runManifest.target'), budget: t('toolCards.codeReview.runManifest.budget'), estimatedCalls: t('toolCards.codeReview.runManifest.estimatedCalls'), activeReviewers: t('toolCards.codeReview.runManifest.activeGroupTitle'), skippedReviewers: t('toolCards.codeReview.runManifest.skippedGroupTitle'), - issues: t('toolCards.codeReview.sections.issues', { defaultValue: 'Issues' }), - noIssues: t('toolCards.codeReview.report.noIssues', { defaultValue: 'No validated issues.' }), - remediationPlan: t('toolCards.codeReview.sections.remediation', { defaultValue: 'Remediation Plan' }), - strengths: t('toolCards.codeReview.sections.strengths', { defaultValue: 'Strengths' }), - reviewTeam: t('toolCards.codeReview.sections.team', { defaultValue: 'Code Review Team' }), - reliabilitySignals: t('toolCards.codeReview.report.reliabilitySignals', { defaultValue: 'Review Reliability' }), - coverageNotes: t('toolCards.codeReview.sections.coverage', { defaultValue: 'Coverage Notes' }), - status: t('toolCards.codeReview.report.status', { defaultValue: 'Status' }), - packet: t('toolCards.codeReview.report.packet', { defaultValue: 'Packet' }), - partialOutput: t('toolCards.codeReview.report.partialOutput', { defaultValue: 'Partial output' }), - findings: t('toolCards.codeReview.report.findings', { defaultValue: 'Findings' }), - validation: t('toolCards.codeReview.report.validation', { defaultValue: 'Validation' }), - suggestion: t('toolCards.codeReview.suggestion', { defaultValue: 'Suggestion' }), - source: t('toolCards.codeReview.report.source', { defaultValue: 'Source' }), - noItems: t('toolCards.codeReview.report.noItems', { defaultValue: 'None.' }), + issues: t('toolCards.codeReview.sections.issues'), + noIssues: t('toolCards.codeReview.report.noIssues'), + remediationPlan: t('toolCards.codeReview.sections.remediation'), + strengths: t('toolCards.codeReview.sections.strengths'), + reviewTeam: t('toolCards.codeReview.sections.team'), + reliabilitySignals: t('toolCards.codeReview.report.reliabilitySignals'), + coverageNotes: t('toolCards.codeReview.sections.coverage'), + status: t('toolCards.codeReview.report.status'), + packet: t('toolCards.codeReview.report.packet'), + partialOutput: t('toolCards.codeReview.report.partialOutput'), + findings: t('toolCards.codeReview.report.findings'), + validation: t('toolCards.codeReview.report.validation'), + suggestion: t('toolCards.codeReview.suggestion'), + source: t('toolCards.codeReview.report.source'), + noItems: t('toolCards.codeReview.report.noItems'), groupTitles: { - must_fix: t('toolCards.codeReview.groups.must_fix', { defaultValue: 'Must fix' }), - should_improve: t('toolCards.codeReview.groups.should_improve', { defaultValue: 'Should improve' }), - needs_decision: t('toolCards.codeReview.groups.needs_decision', { defaultValue: 'Needs decision' }), - verification: t('toolCards.codeReview.groups.verification', { defaultValue: 'Verification' }), - architecture: t('toolCards.codeReview.groups.architecture', { defaultValue: 'Architecture' }), - maintainability: t('toolCards.codeReview.groups.maintainability', { defaultValue: 'Maintainability' }), - tests: t('toolCards.codeReview.groups.tests', { defaultValue: 'Tests' }), - security: t('toolCards.codeReview.groups.security', { defaultValue: 'Security' }), - performance: t('toolCards.codeReview.groups.performance', { defaultValue: 'Performance' }), - user_experience: t('toolCards.codeReview.groups.user_experience', { defaultValue: 'User experience' }), - other: t('toolCards.codeReview.groups.other', { defaultValue: 'Other' }), + must_fix: t('toolCards.codeReview.groups.must_fix'), + should_improve: t('toolCards.codeReview.groups.should_improve'), + needs_decision: t('toolCards.codeReview.groups.needs_decision'), + verification: t('toolCards.codeReview.groups.verification'), + architecture: t('toolCards.codeReview.groups.architecture'), + maintainability: t('toolCards.codeReview.groups.maintainability'), + tests: t('toolCards.codeReview.groups.tests'), + security: t('toolCards.codeReview.groups.security'), + performance: t('toolCards.codeReview.groups.performance'), + user_experience: t('toolCards.codeReview.groups.user_experience'), + other: t('toolCards.codeReview.groups.other'), }, reliabilityNoticeLabels: { - context_pressure: t('toolCards.codeReview.reliabilityStatus.context_pressure.label', { - defaultValue: 'Context pressure rising', - }), - compression_preserved: t('toolCards.codeReview.reliabilityStatus.compression_preserved.label', { - defaultValue: 'Compression preserved key facts', - }), - cache_hit: t('toolCards.codeReview.reliabilityStatus.cache_hit.label', { - defaultValue: 'Incremental cache reused reviewer output', - }), - cache_miss: t('toolCards.codeReview.reliabilityStatus.cache_miss.label', { - defaultValue: 'Incremental cache missed or refreshed', - }), - concurrency_limited: t('toolCards.codeReview.reliabilityStatus.concurrency_limited.label', { - defaultValue: 'Reviewer launch was concurrency-limited', - }), - partial_reviewer: t('toolCards.codeReview.reliabilityStatus.partial_reviewer.label', { - defaultValue: 'Reviewer returned partial result', - }), - reduced_scope: t('toolCards.codeReview.reliabilityStatus.reduced_scope.label', { - defaultValue: 'Reduced-depth coverage', - }), - retry_guidance: t('toolCards.codeReview.reliabilityStatus.retry_guidance.label', { - defaultValue: 'Retry guidance emitted', - }), - skipped_reviewers: t('toolCards.codeReview.reliabilityStatus.skipped_reviewers.label', { - defaultValue: 'Skipped reviewers', - }), - token_budget_limited: t('toolCards.codeReview.reliabilityStatus.token_budget_limited.label', { - defaultValue: 'Token budget limited reviewer coverage', - }), - user_decision: t('toolCards.codeReview.reliabilityStatus.user_decision.label', { - defaultValue: 'User decision needed', - }), + context_pressure: t('toolCards.codeReview.reliabilityStatus.context_pressure.label'), + compression_preserved: t('toolCards.codeReview.reliabilityStatus.compression_preserved.label'), + cache_hit: t('toolCards.codeReview.reliabilityStatus.cache_hit.label'), + cache_miss: t('toolCards.codeReview.reliabilityStatus.cache_miss.label'), + concurrency_limited: t('toolCards.codeReview.reliabilityStatus.concurrency_limited.label'), + partial_reviewer: t('toolCards.codeReview.reliabilityStatus.partial_reviewer.label'), + reduced_scope: t('toolCards.codeReview.reliabilityStatus.reduced_scope.label'), + retry_guidance: t('toolCards.codeReview.reliabilityStatus.retry_guidance.label'), + skipped_reviewers: t('toolCards.codeReview.reliabilityStatus.skipped_reviewers.label'), + token_budget_limited: t('toolCards.codeReview.reliabilityStatus.token_budget_limited.label'), + user_decision: t('toolCards.codeReview.reliabilityStatus.user_decision.label'), }, }), [t]); @@ -144,9 +122,7 @@ export const CodeReviewReportExportActions: React.FC { - const prefix = t('toolCards.codeReview.export.fileNamePrefix', { - defaultValue: 'BitFun-Code-Review', - }); + const prefix = t('toolCards.codeReview.export.fileNamePrefix'); return `${prefix}_${timestampForFileName()}.md`; }, [t]); @@ -156,13 +132,9 @@ export const CodeReviewReportExportActions: React.FC setCopied(false), 1600); - notificationService.success(t('toolCards.codeReview.export.copySuccess', { - defaultValue: 'Review report copied as Markdown.', - })); + notificationService.success(t('toolCards.codeReview.export.copySuccess')); } catch { - notificationService.error(t('toolCards.codeReview.export.copyFailed', { - defaultValue: 'Failed to copy review report.', - })); + notificationService.error(t('toolCards.codeReview.export.copyFailed')); } }, [markdown, t]); @@ -170,16 +142,14 @@ export const CodeReviewReportExportActions: React.FC - {t('toolCards.codeReview.export.openMarkdown', { defaultValue: 'Open as Markdown' })} + {t('toolCards.codeReview.export.openMarkdown')} )}
@@ -246,37 +210,37 @@ export const CodeReviewReportExportActions: React.FC event.stopPropagation()}> {visibleActions.has('copy') && ( - + )} {visibleActions.has('open') && ( - + )} {visibleActions.has('save') && ( - + diff --git a/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx index f8f0a09e0..97955a16e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'; import { CubeLoading } from '../../component-library'; import type { FlowToolItem } from '../types/flow-chat'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; +import { i18nService } from '@/infrastructure/i18n'; import './ContextCompressionDisplay.scss'; interface ContextCompressionDisplayProps { @@ -76,6 +77,7 @@ export const ContextCompressionDisplay: React.FC const savedTokens = data.tokensBefore && data.tokensAfter ? data.tokensBefore - data.tokensAfter : undefined; + const formatNumber = (value: number): string => i18nService.formatNumber(value); const isLoading = data.status === 'preparing' || data.status === 'streaming' || data.status === 'running'; @@ -112,14 +114,14 @@ export const ContextCompressionDisplay: React.FC <> {t('toolCards.contextCompression.tokenChange', { - before: data.tokensBefore.toLocaleString(), - after: data.tokensAfter.toLocaleString(), + before: formatNumber(data.tokensBefore), + after: formatNumber(data.tokensAfter), })} {savedTokens !== undefined && data.compressionRatio !== undefined && ( {t('toolCards.contextCompression.savingsTag', { - saved: savedTokens.toLocaleString(), + saved: formatNumber(savedTokens), ratio: (data.compressionRatio * 100).toFixed(0), })} diff --git a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx index 7936f56cf..32de3cf5c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx @@ -54,6 +54,7 @@ import { displayFileToolGuidanceMessage, isFileToolGuidanceMessage, } from './fileToolGuidance'; +import { i18nService } from '@/infrastructure/i18n'; import './FileOperationToolCard.scss'; const log = createLogger('FileOperationToolCard'); @@ -210,7 +211,7 @@ export const FileOperationToolCard: React.FC = ({ const writeContentStatusText = useMemo(() => { if (toolItem.toolName !== 'Write' || writeContentCharCount <= 0) return null; - const formattedCount = writeContentCharCount.toLocaleString(); + const formattedCount = i18nService.formatNumber(writeContentCharCount); if (status === 'completed') { return `${formattedCount} chars written`; } diff --git a/src/web-ui/src/locales/en-US/scenes/skills.json b/src/web-ui/src/locales/en-US/scenes/skills.json index fde537346..eb40b6edb 100644 --- a/src/web-ui/src/locales/en-US/scenes/skills.json +++ b/src/web-ui/src/locales/en-US/scenes/skills.json @@ -118,7 +118,6 @@ "modes": { "agentic": "agentic", "cowork": "Cowork", - "claw": "Claw", "team": "Team" }, "modeDescriptions": { diff --git a/src/web-ui/src/locales/zh-CN/scenes/skills.json b/src/web-ui/src/locales/zh-CN/scenes/skills.json index 698759efc..45a9e3e82 100644 --- a/src/web-ui/src/locales/zh-CN/scenes/skills.json +++ b/src/web-ui/src/locales/zh-CN/scenes/skills.json @@ -118,7 +118,6 @@ "modes": { "agentic": "agentic", "cowork": "Cowork", - "claw": "Claw", "team": "Team" }, "modeDescriptions": { diff --git a/src/web-ui/src/locales/zh-TW/scenes/skills.json b/src/web-ui/src/locales/zh-TW/scenes/skills.json index b69f16a29..35643b2e4 100644 --- a/src/web-ui/src/locales/zh-TW/scenes/skills.json +++ b/src/web-ui/src/locales/zh-TW/scenes/skills.json @@ -118,7 +118,6 @@ "modes": { "agentic": "agentic", "cowork": "Cowork", - "claw": "Claw", "team": "Team" }, "modeDescriptions": { diff --git a/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx b/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx index c21eb9fe2..c0e7ff7d0 100644 --- a/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx +++ b/src/web-ui/src/tools/workspace/components/WorkspaceManager.tsx @@ -225,7 +225,11 @@ const WorkspaceManager: React.FC = ({
Lines: - {currentWorkspace.statistics.totalLines?.toLocaleString()} + + {currentWorkspace.statistics.totalLines == null + ? '' + : i18nService.formatNumber(currentWorkspace.statistics.totalLines)} +
Total Size: