From b349d2f7b3f16d33621635525039ddfc9e6f9186 Mon Sep 17 00:00:00 2001 From: limityan Date: Fri, 29 May 2026 19:30:59 +0800 Subject: [PATCH] refactor: harden i18n resource audit --- .github/workflows/ci.yml | 6 + AGENTS-CN.md | 2 + AGENTS.md | 2 + CONTRIBUTING.md | 2 + CONTRIBUTING_CN.md | 2 + docs/architecture/i18n.md | 7 + docs/development/i18n.md | 12 +- scripts/i18n-audit.mjs | 375 +++++++++++++++- scripts/i18n-contract.test.mjs | 34 ++ scripts/i18n-hardcoded-baseline.json | 6 +- .../relay-server/static/homepage/i18n.json | 53 +++ .../relay-server/static/homepage/index.html | 50 +-- src/mobile-web/src/App.tsx | 6 +- src/mobile-web/src/i18n/messages.ts | 8 +- src/mobile-web/src/pages/ChatPage.tsx | 47 +- .../src/services/RemoteSessionManager.ts | 6 +- .../src/app/scenes/settings/SettingsNav.tsx | 27 +- .../src/app/scenes/settings/settingsConfig.ts | 11 - .../flow_chat/components/FlowTextBlock.tsx | 32 +- .../components/modern/ProcessingIndicator.tsx | 10 +- .../flow_chat/constants/processingHints.ts | 420 ------------------ .../flow-chat-manager/SessionModule.ts | 2 +- .../services/AgentCompanionPetService.ts | 6 +- .../agentCompanionBuiltinPetMetadata.json | 6 + .../i18n/presets/namespaceRegistry.ts | 1 + src/web-ui/src/locales/en-US/common.json | 1 + src/web-ui/src/locales/en-US/flow-chat.json | 1 + .../en-US/flow-chat/processing-hints.json | 192 ++++++++ src/web-ui/src/locales/en-US/settings.json | 8 + src/web-ui/src/locales/zh-CN/common.json | 1 + src/web-ui/src/locales/zh-CN/flow-chat.json | 1 + .../zh-CN/flow-chat/processing-hints.json | 192 ++++++++ src/web-ui/src/locales/zh-CN/settings.json | 8 + src/web-ui/src/locales/zh-TW/common.json | 1 + src/web-ui/src/locales/zh-TW/flow-chat.json | 1 + .../zh-TW/flow-chat/processing-hints.json | 192 ++++++++ src/web-ui/src/locales/zh-TW/settings.json | 8 + .../src/shared/ai-errors/aiErrorPresenter.ts | 8 +- .../ai-errors/providerMessageMatchers.json | 10 + src/web-ui/src/shared/utils/tabUtils.ts | 2 +- .../tools/editor/components/CodeEditor.tsx | 3 +- .../components/largeFileExpansionLabels.json | 5 + 42 files changed, 1250 insertions(+), 517 deletions(-) create mode 100644 src/apps/relay-server/static/homepage/i18n.json delete mode 100644 src/web-ui/src/flow_chat/constants/processingHints.ts create mode 100644 src/web-ui/src/infrastructure/config/services/agentCompanionBuiltinPetMetadata.json create mode 100644 src/web-ui/src/locales/en-US/flow-chat/processing-hints.json create mode 100644 src/web-ui/src/locales/zh-CN/flow-chat/processing-hints.json create mode 100644 src/web-ui/src/locales/zh-TW/flow-chat/processing-hints.json create mode 100644 src/web-ui/src/shared/ai-errors/providerMessageMatchers.json create mode 100644 src/web-ui/src/tools/editor/components/largeFileExpansionLabels.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e78477554..14eda9ef5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,12 @@ jobs: - name: Validate GitHub config run: pnpm run check:github-config + - name: Validate i18n contract + run: pnpm run i18n:contract:test + + - name: Audit i18n resources + run: pnpm run i18n:audit + - name: Lint web UI run: pnpm run lint:web diff --git a/AGENTS-CN.md b/AGENTS-CN.md index 295ecb6dd..4b711baf7 100644 --- a/AGENTS-CN.md +++ b/AGENTS-CN.md @@ -93,6 +93,8 @@ pnpm run desktop:build:nsis:fast # Windows 安装器,release-fast profile - 不要把 Web UI locale 资源导入 `src/mobile-web`、`BitFun-Installer` 等较小形态。 - Web UI 只急切加载 bootstrap namespace;路由或功能文案使用 `useI18n(namespace)`,直接 `i18nService.t(...)` 只用于 bootstrap namespace。 +- `pnpm run i18n:audit` 会检查 key / 占位符一致性、直接静态 key 是否存在,以及 + source 中不再新增硬编码 CJK 文案。 ### 日志 diff --git a/AGENTS.md b/AGENTS.md index cb303500a..a73ddcfa0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,6 +96,8 @@ 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. +- `pnpm run i18n:audit` enforces key/placeholder parity, direct static key + existence, and the no-hardcoded-CJK source budget. ### Logging diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 327035495..0cf873ff4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,6 +86,8 @@ Captured data is logged as structured JSON under the `bitfun::devtools` target. workflow copy in the owning surface. - Web UI route or feature copy should use `useI18n(namespace)`. Do not import Web UI locale catalogs into mobile-web, installer, backend, or static pages. +- `pnpm run i18n:audit` enforces key/placeholder parity, direct static key + existence, and the no-hardcoded-CJK source budget. ### Platform-agnostic core diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index 4cf1cd362..d022c9a75 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -84,6 +84,8 @@ pnpm run e2e:test `pnpm run i18n:generate`。 - 跨形态稳定标签放在 `src/shared/i18n/resources/shared`;流程文案留在所属形态资源中。 - Web UI 路由或功能文案使用 `useI18n(namespace)`。不要把 Web UI locale 资源导入 mobile-web、installer、backend 或静态页面。 +- `pnpm run i18n:audit` 会检查 key / 占位符一致性、直接静态 key 是否存在,以及 + source 中不再新增硬编码 CJK 文案。 ### 平台无关核心 diff --git a/docs/architecture/i18n.md b/docs/architecture/i18n.md index 531b7073f..db4284a51 100644 --- a/docs/architecture/i18n.md +++ b/docs/architecture/i18n.md @@ -78,11 +78,18 @@ to their current locale data. locale parsing, canonicalization, fallback order, and shared terms. - Relay static homepage is a self-contained static surface. It is tracked in the contract and audit baseline, but it does not import runtime locale catalogs. + Keep its static copy in its own resource file, such as + `src/apps/relay-server/static/homepage/i18n.json`, with English HTML fallback + text for first paint and fetch-failure resilience. Direct Web UI `i18nService.t('namespace:key')` calls are allowed only for bootstrap namespaces. Route, scene, and feature UI should use `useI18n(namespace)` so the namespace can be loaded on demand. +Large or rarely used Web UI copy should live in a feature namespace outside +`WEB_UI_BOOTSTRAP_NAMESPACES`; components load it with `useI18n(namespace)` or +`useTranslation(namespace)` at the point of use. + ## 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 199d295fd..8cf30c49f 100644 --- a/docs/development/i18n.md +++ b/docs/development/i18n.md @@ -104,9 +104,11 @@ to the relevant locale file instead. Literal fallback strings make audits and translation completeness harder to enforce. `pnpm run i18n:audit` fails on missing or extra keys in Web UI, mobile-web, and -installer locale resources. It also fails if the checked-in CJK source candidate -budgets grow. Existing budget warnings are grandfathered; new user-facing copy -should be extracted instead of increasing the baseline. +installer locale resources. It also fails on placeholder mismatches, unknown +static Web UI `i18nService.t('namespace:key')` literals, direct `t(key, +"literal fallback")` arguments, and CJK source candidates outside approved +resource owners. Keep user-facing copy in locale/resource files rather than +raising the hardcoded-copy baseline. ## Loading Size @@ -118,6 +120,10 @@ Web UI eagerly loads only bootstrap namespaces and lazy-loads all other namespaces. Mobile Web, Installer, Backend, and static relay pages must not import Web UI locale catalogs to reuse copy. +For large feature copy, create a dedicated lazy Web UI namespace instead of +putting it in a bootstrap namespace. Static self-contained pages should keep +their own small resource files and retain English fallback HTML. + For bundle-sensitive changes, compare generated asset sizes before and after the change and include the result in the PR. diff --git a/scripts/i18n-audit.mjs b/scripts/i18n-audit.mjs index 013028982..1b8df0353 100644 --- a/scripts/i18n-audit.mjs +++ b/scripts/i18n-audit.mjs @@ -24,7 +24,9 @@ const mobileWebSourceDir = path.join(root, 'src', 'mobile-web', 'src'); const mobileWebMessagesPath = path.join(mobileWebSourceDir, 'i18n', 'messages.ts'); const installerSourceDir = path.join(root, 'BitFun-Installer', 'src'); const installerLocalesDir = path.join(installerSourceDir, 'i18n', 'locales'); +const coreLocalesDir = path.join(root, 'src', 'crates', 'core', 'locales'); const relayHomepageDir = path.join(root, 'src', 'apps', 'relay-server', 'static', 'homepage'); +const relayHomepageI18nPath = path.join(relayHomepageDir, 'i18n.json'); const supportedLocales = fs .readdirSync(webLocalesDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) @@ -115,6 +117,54 @@ function flattenKeys(value, prefix = '') { return keys.sort(); } +function flattenStringEntries(value, prefix = '') { + if (typeof value === 'string') { + return prefix ? [[prefix, value]] : []; + } + if (Array.isArray(value)) { + const text = value.filter((item) => typeof item === 'string').join('\n'); + return prefix ? [[prefix, text]] : []; + } + if (value == null || typeof value !== 'object') { + return prefix ? [[prefix, '']] : []; + } + + return Object.entries(value) + .flatMap(([key, child]) => flattenStringEntries(child, prefix ? `${prefix}.${key}` : key)) + .sort(([left], [right]) => left.localeCompare(right)); +} + +function sortedUnique(values) { + return Array.from(new Set(values)).sort(); +} + +function extractI18nextPlaceholders(value) { + const matches = String(value).matchAll(/\{\{\s*-?\s*([A-Za-z_][\w]*)\s*\}\}/g); + return sortedUnique(Array.from(matches, (match) => match[1])); +} + +function extractMobilePlaceholders(value) { + const matches = String(value).matchAll(/\{\s*([A-Za-z_][\w]*)\s*\}/g); + return sortedUnique(Array.from(matches, (match) => match[1])); +} + +function extractFluentPlaceholders(value) { + const matches = String(value).matchAll(/\$\s*([A-Za-z_][\w-]*)/g); + return sortedUnique(Array.from(matches, (match) => match[1])); +} + +function sameSet(left, right) { + if (left.length !== right.length) return false; + return left.every((item, index) => item === right[index]); +} + +function reportPlaceholderParity(surface, locale, key, expected, actual) { + if (sameSet(expected, actual)) return; + reportError( + `${surface} ${locale} key "${key}" placeholder mismatch: expected [${expected.join(', ')}], got [${actual.join(', ')}]`, + ); +} + function readJsonKeys(locale, namespace) { const file = namespace === 'shared' ? path.join(sharedTermsDir, locale, 'terms.json') @@ -127,6 +177,18 @@ function readJsonKeys(locale, namespace) { } } +function readJsonEntries(locale, namespace) { + const file = namespace === 'shared' + ? path.join(sharedTermsDir, locale, 'terms.json') + : path.join(webLocalesDir, locale, `${namespace}.json`); + try { + return new Map(flattenStringEntries(readJsonFile(file))); + } catch (error) { + reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`); + return new Map(); + } +} + function readInstallerJsonKeys(uiLocale) { const file = path.join(installerLocalesDir, `${uiLocale}.json`); try { @@ -137,6 +199,16 @@ function readInstallerJsonKeys(uiLocale) { } } +function readInstallerJsonEntries(uiLocale) { + const file = path.join(installerLocalesDir, `${uiLocale}.json`); + try { + return new Map(flattenStringEntries(readJsonFile(file))); + } catch (error) { + reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`); + return new Map(); + } +} + function propertyNameToString(ts, name) { if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { return name.text; @@ -173,7 +245,33 @@ function flattenTsObjectKeys(ts, objectLiteral, prefix = '') { return keys.sort(); } -function readMobileMessageKeysByLocale() { +function flattenTsObjectEntries(ts, objectLiteral, prefix = '') { + const entries = []; + for (const property of objectLiteral.properties) { + if (!ts.isPropertyAssignment(property)) continue; + + const key = propertyNameToString(ts, property.name); + if (!key) continue; + if (!prefix && key === 'shared') continue; + + const nextPrefix = prefix ? `${prefix}.${key}` : key; + const initializer = unwrapTsExpression(ts, property.initializer); + + if (ts.isObjectLiteralExpression(initializer)) { + entries.push(...flattenTsObjectEntries(ts, initializer, nextPrefix)); + } else if ( + ts.isStringLiteral(initializer) || + ts.isNoSubstitutionTemplateLiteral(initializer) + ) { + entries.push([nextPrefix, initializer.text]); + } else { + entries.push([nextPrefix, '']); + } + } + return entries.sort(([left], [right]) => left.localeCompare(right)); +} + +function readMobileMessagesByLocale() { let ts; try { ts = require('typescript'); @@ -210,7 +308,7 @@ function readMobileMessageKeysByLocale() { continue; } - output.set(locale, flattenTsObjectKeys(ts, value)); + output.set(locale, new Map(flattenTsObjectEntries(ts, value))); } } ts.forEachChild(node, visit); @@ -220,6 +318,13 @@ function readMobileMessageKeysByLocale() { return output; } +function readMobileMessageKeysByLocale() { + return new Map( + Array.from(readMobileMessagesByLocale().entries()) + .map(([locale, entries]) => [locale, Array.from(entries.keys()).sort()]), + ); +} + function diffSets(left, right) { const rightSet = new Set(right); return left.filter((item) => !rightSet.has(item)); @@ -386,6 +491,27 @@ function auditKeyParity(namespaces) { } } +function auditWebI18nextPlaceholderParity(namespaces) { + for (const namespace of namespaces) { + const baselineEntries = readJsonEntries(baselineLocale, namespace); + const baselinePlaceholders = new Map( + Array.from(baselineEntries.entries()).map(([key, value]) => [ + key, + extractI18nextPlaceholders(value), + ]), + ); + + for (const locale of supportedLocales.filter((item) => item !== baselineLocale)) { + const localeEntries = readJsonEntries(locale, namespace); + for (const [key, expected] of baselinePlaceholders.entries()) { + if (!localeEntries.has(key)) continue; + const actual = extractI18nextPlaceholders(localeEntries.get(key)); + reportPlaceholderParity(`web-ui ${namespace}`, locale, key, expected, actual); + } + } + } +} + function auditMobileWebMessageParity() { const messagesByLocale = readMobileMessageKeysByLocale(); const baselineKeys = messagesByLocale.get('en-US'); @@ -408,6 +534,31 @@ function auditMobileWebMessageParity() { } } +function auditMobileWebPlaceholderParity() { + const messagesByLocale = readMobileMessagesByLocale(); + const baselineEntries = messagesByLocale.get('en-US'); + if (!baselineEntries) { + reportError('mobile-web messages are missing the en-US baseline locale'); + return; + } + + const baselinePlaceholders = new Map( + Array.from(baselineEntries.entries()).map(([key, value]) => [ + key, + extractMobilePlaceholders(value), + ]), + ); + + for (const [locale, entries] of messagesByLocale.entries()) { + if (locale === 'en-US') continue; + for (const [key, expected] of baselinePlaceholders.entries()) { + if (!entries.has(key)) continue; + const actual = extractMobilePlaceholders(entries.get(key)); + reportPlaceholderParity('mobile-web', locale, key, expected, actual); + } + } +} + function auditInstallerKeyParity() { const baselineKeys = readInstallerJsonKeys('en'); for (const uiLocale of ['zh', 'zh-TW']) { @@ -424,6 +575,162 @@ function auditInstallerKeyParity() { } } +function auditInstallerPlaceholderParity() { + const baselineEntries = readInstallerJsonEntries('en'); + const baselinePlaceholders = new Map( + Array.from(baselineEntries.entries()).map(([key, value]) => [ + key, + extractI18nextPlaceholders(value), + ]), + ); + + for (const uiLocale of ['zh', 'zh-TW']) { + const entries = readInstallerJsonEntries(uiLocale); + for (const [key, expected] of baselinePlaceholders.entries()) { + if (!entries.has(key)) continue; + const actual = extractI18nextPlaceholders(entries.get(key)); + reportPlaceholderParity('installer', uiLocale, key, expected, actual); + } + } +} + +function readFluentMessages(localeId) { + const file = path.join(coreLocalesDir, `${localeId}.ftl`); + const messages = new Map(); + let currentKey = null; + let currentLines = []; + + function flushCurrent() { + if (currentKey) { + messages.set(currentKey, currentLines.join('\n')); + } + currentKey = null; + currentLines = []; + } + + try { + const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); + for (const line of lines) { + const match = line.match(/^([A-Za-z][\w-]*)\s*=\s*(.*)$/); + if (match) { + flushCurrent(); + currentKey = match[1]; + currentLines = [match[2]]; + continue; + } + if (currentKey && (/^\s+/.test(line) || line.trim().startsWith('*[') || line.trim().startsWith('['))) { + currentLines.push(line); + } + } + flushCurrent(); + } catch (error) { + reportError(`Failed to parse ${toPosixPath(path.relative(root, file))}: ${error.message}`); + } + + return messages; +} + +function auditCoreFluentParity() { + const coreLocales = localeContract.surfaceOrders?.core ?? []; + const baselineCoreLocale = coreLocales.includes('en-US') ? 'en-US' : coreLocales[0]; + const baselineEntries = readFluentMessages(baselineCoreLocale); + const baselineKeys = Array.from(baselineEntries.keys()).sort(); + const baselinePlaceholders = new Map( + Array.from(baselineEntries.entries()).map(([key, value]) => [ + key, + extractFluentPlaceholders(value), + ]), + ); + + for (const locale of coreLocales.filter((item) => item !== baselineCoreLocale)) { + const entries = readFluentMessages(locale); + const keys = Array.from(entries.keys()).sort(); + for (const key of diffSets(baselineKeys, keys)) { + reportError(`core ${locale}.ftl is missing key "${key}"`); + } + for (const key of diffSets(keys, baselineKeys)) { + reportError(`core ${locale}.ftl has extra key "${key}"`); + } + for (const [key, expected] of baselinePlaceholders.entries()) { + if (!entries.has(key)) continue; + const actual = extractFluentPlaceholders(entries.get(key)); + reportPlaceholderParity('core Fluent', locale, key, expected, actual); + } + } +} + +function readRelayHomepageMessages() { + let resource; + try { + resource = readJsonFile(relayHomepageI18nPath); + } catch (error) { + reportError(`Failed to parse ${toPosixPath(path.relative(root, relayHomepageI18nPath))}: ${error.message}`); + return { localeIds: [], entriesByLocale: new Map() }; + } + + const entriesByLocale = new Map(); + for (const [locale, messages] of Object.entries(resource)) { + entriesByLocale.set(locale, new Map(flattenStringEntries(messages))); + } + + return { + localeIds: Object.keys(resource).sort(), + entriesByLocale, + }; +} + +function collectRelayHomepageDataKeys() { + const htmlPath = path.join(relayHomepageDir, 'index.html'); + const html = fs.readFileSync(htmlPath, 'utf8'); + return sortedUnique(Array.from(html.matchAll(/\bdata-i18n="([^"]+)"/g), (match) => match[1])); +} + +function auditRelayStaticHomepageResources() { + const expectedLocaleIds = (localeContract.locales ?? []).map((locale) => locale.id).sort(); + const { localeIds, entriesByLocale } = readRelayHomepageMessages(); + const baselineLocaleId = expectedLocaleIds.includes('en-US') ? 'en-US' : expectedLocaleIds[0]; + const baselineEntries = entriesByLocale.get(baselineLocaleId) ?? new Map(); + const baselineKeys = Array.from(baselineEntries.keys()).sort(); + const dataKeys = collectRelayHomepageDataKeys(); + + for (const locale of diffSets(expectedLocaleIds, localeIds)) { + reportError(`relay static homepage i18n.json is missing locale "${locale}"`); + } + for (const locale of diffSets(localeIds, expectedLocaleIds)) { + reportError(`relay static homepage i18n.json has non-canonical locale "${locale}"`); + } + for (const key of diffSets(dataKeys, baselineKeys)) { + reportError(`relay static homepage index.html references missing i18n key "${key}"`); + } + for (const key of diffSets(baselineKeys, dataKeys)) { + reportError(`relay static homepage i18n.json has unused baseline key "${key}"`); + } + + const baselinePlaceholders = new Map( + Array.from(baselineEntries.entries()).map(([key, value]) => [ + key, + extractI18nextPlaceholders(value), + ]), + ); + + for (const locale of expectedLocaleIds.filter((item) => item !== baselineLocaleId)) { + const entries = entriesByLocale.get(locale); + if (!entries) continue; + const keys = Array.from(entries.keys()).sort(); + for (const key of diffSets(baselineKeys, keys)) { + reportError(`relay static homepage ${locale} messages are missing key "${key}"`); + } + for (const key of diffSets(keys, baselineKeys)) { + reportError(`relay static homepage ${locale} messages have extra key "${key}"`); + } + for (const [key, expected] of baselinePlaceholders.entries()) { + if (!entries.has(key)) continue; + const actual = extractI18nextPlaceholders(entries.get(key)); + reportPlaceholderParity('relay static homepage', locale, key, expected, actual); + } + } +} + function shouldSkipSourceScan(file) { const normalized = toPosixPath(path.relative(root, file)); return ( @@ -482,7 +789,63 @@ function auditSourceText() { } if (fallbackFindings.length > 0) { - reportWarning(`Found ${fallbackFindings.length} t(key, "literal fallback") candidate(s). First entries: ${fallbackFindings.slice(0, 12).join(', ')}`); + reportError(`Found ${fallbackFindings.length} t(key, "literal fallback") candidate(s). First entries: ${fallbackFindings.slice(0, 12).join(', ')}`); + } +} + +function lineNumberAt(text, index) { + return text.slice(0, index).split(/\r?\n/).length; +} + +function collectWebUiStaticTranslationKeys() { + const sourceFiles = listFiles( + webSourceDir, + (file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !shouldSkipSourceScan(file), + ); + const output = []; + const patterns = [ + /i18nService\.t\(\s*(['"`])([^'"`$]+?)\1/g, + /i18nService\.getT\(\)\(\s*(['"`])([^'"`$]+?)\1/g, + ]; + + for (const file of sourceFiles) { + const text = fs.readFileSync(file, 'utf8'); + for (const pattern of patterns) { + for (const match of text.matchAll(pattern)) { + const key = match[2]; + if (!key.includes(':')) continue; + output.push({ + key, + location: `${toPosixPath(path.relative(root, file))}:${lineNumberAt(text, match.index ?? 0)}`, + }); + } + } + } + + return output; +} + +function buildWebUiKeySet(namespaces) { + const keys = new Set(); + for (const namespace of namespaces) { + for (const key of readJsonKeys(baselineLocale, namespace)) { + keys.add(`${namespace}:${key}`); + } + } + return keys; +} + +function auditWebUiStaticTranslationKeys(namespaces) { + const knownKeys = buildWebUiKeySet(namespaces); + const missing = collectWebUiStaticTranslationKeys() + .filter(({ key }) => !knownKeys.has(key)); + + if (missing.length > 0) { + reportError( + `Found ${missing.length} unknown static Web UI i18n key(s). First entries: ${ + missing.slice(0, 12).map(({ key, location }) => `${location} ${key}`).join(', ') + }`, + ); } } @@ -555,8 +918,14 @@ auditMobileWebBoundary(); const namespaces = auditNamespaceCoverage(); auditKeyParity(namespaces); +auditWebI18nextPlaceholderParity(namespaces); +auditWebUiStaticTranslationKeys(namespaces); auditMobileWebMessageParity(); +auditMobileWebPlaceholderParity(); auditInstallerKeyParity(); +auditInstallerPlaceholderParity(); +auditCoreFluentParity(); +auditRelayStaticHomepageResources(); auditSourceText(); auditHardcodedSourceBudgets(); diff --git a/scripts/i18n-contract.test.mjs b/scripts/i18n-contract.test.mjs index 392a6936a..d2d4456d5 100644 --- a/scripts/i18n-contract.test.mjs +++ b/scripts/i18n-contract.test.mjs @@ -225,6 +225,40 @@ test('i18n audit treats locale key parity as an error', () => { assert.match(auditSource, /auditInstallerKeyParity/, 'installer locale keys should be covered by i18n:audit'); }); +test('CI runs i18n contract and audit guards before frontend builds', () => { + const ciSource = readText('.github/workflows/ci.yml'); + const contractIndex = ciSource.indexOf('pnpm run i18n:contract:test'); + const auditIndex = ciSource.indexOf('pnpm run i18n:audit'); + const buildIndex = ciSource.indexOf('pnpm run build:web'); + + assert.notEqual(contractIndex, -1, 'CI should run pnpm run i18n:contract:test'); + assert.notEqual(auditIndex, -1, 'CI should run pnpm run i18n:audit'); + assert.ok(contractIndex < buildIndex, 'i18n contract checks should run before web build'); + assert.ok(auditIndex < buildIndex, 'i18n audit should run before web build'); +}); + +test('i18n audit enforces interpolation parameter parity across resource formats', () => { + const auditSource = readText('scripts/i18n-audit.mjs'); + + assert.match(auditSource, /auditWebI18nextPlaceholderParity/, 'Web UI JSON placeholders should be audited'); + assert.match(auditSource, /auditMobileWebPlaceholderParity/, 'mobile-web placeholders should be audited'); + assert.match(auditSource, /auditInstallerPlaceholderParity/, 'installer placeholders should be audited'); + assert.match(auditSource, /auditCoreFluentParity/, 'core Fluent keys and placeholders should be audited'); + assert.match(auditSource, /auditRelayStaticHomepageResources/, 'relay static homepage resources should be audited'); + assert.match(auditSource, /extractI18nextPlaceholders/, 'i18next placeholder extraction should be explicit'); + assert.match(auditSource, /extractMobilePlaceholders/, 'mobile placeholder extraction should be explicit'); + assert.match(auditSource, /extractFluentPlaceholders/, 'Fluent placeholder extraction should be explicit'); +}); + +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] ?? ''; + + assert.match(sourceTextAudit, /reportError/, 'literal t(key, "fallback") candidates should fail audit'); + assert.match(auditSource, /auditWebUiStaticTranslationKeys/, 'static Web UI translation keys should be checked'); + assert.match(auditSource, /collectWebUiStaticTranslationKeys/, 'static key collection should be explicit'); +}); + test('installer Rust locale contract is generated from the installer surface order', () => { const generatorSource = readText('scripts/generate-i18n-contract.mjs'); const installerRustGenerator = generatorSource.match(/function generateInstallerRustLocaleContract\(contract\) \{[\s\S]*?\n\}/)?.[0] ?? ''; diff --git a/scripts/i18n-hardcoded-baseline.json b/scripts/i18n-hardcoded-baseline.json index 07ea3d120..8f3c7d67e 100644 --- a/scripts/i18n-hardcoded-baseline.json +++ b/scripts/i18n-hardcoded-baseline.json @@ -3,11 +3,11 @@ "budgets": [ { "id": "web-ui-source", - "maxCjkLines": 198 + "maxCjkLines": 0 }, { "id": "mobile-web-source", - "maxCjkLines": 8 + "maxCjkLines": 0 }, { "id": "installer-source", @@ -15,7 +15,7 @@ }, { "id": "relay-static-homepage", - "maxCjkLines": 17 + "maxCjkLines": 0 } ] } diff --git a/src/apps/relay-server/static/homepage/i18n.json b/src/apps/relay-server/static/homepage/i18n.json new file mode 100644 index 000000000..431d462b8 --- /dev/null +++ b/src/apps/relay-server/static/homepage/i18n.json @@ -0,0 +1,53 @@ +{ + "zh-CN": { + "languageToggle": "语言:简体", + "status": "服务运行中", + "desc": "桌面端与移动端之间的中继服务,提供配对与消息转发能力。", + "f1t": "建立连接", + "f1d": "承接配对请求,建立远程会话。", + "f2t": "消息转发", + "f2d": "桥接 WebSocket 与 HTTP 双向通信。", + "f3t": "自托管友好", + "f3d": "私有部署、自定义域名与反向代理。", + "flowDesktop": "桌面端", + "flowDesktopSub": "BitFun Agent", + "flowRelay": "中继服务", + "flowRelaySub": "配对 & 转发", + "flowMobile": "移动端", + "flowMobileSub": "远程控制" + }, + "zh-TW": { + "languageToggle": "語言:繁體", + "status": "服務運行中", + "desc": "桌面端與移動端之間的中繼服務,提供配對與消息轉發能力。", + "f1t": "建立連接", + "f1d": "承接配對請求,建立遠程會話。", + "f2t": "消息轉發", + "f2d": "橋接 WebSocket 與 HTTP 雙向通信。", + "f3t": "自託管友好", + "f3d": "私有部署、自定義域名與反向代理。", + "flowDesktop": "桌面端", + "flowDesktopSub": "BitFun Agent", + "flowRelay": "中繼服務", + "flowRelaySub": "配對 & 轉發", + "flowMobile": "移動端", + "flowMobileSub": "遠程控制" + }, + "en-US": { + "languageToggle": "Language: EN", + "status": "Service Running", + "desc": "Relay service between desktop and mobile, enabling pairing and message forwarding.", + "f1t": "Connect", + "f1d": "Handle pairing requests, create remote sessions.", + "f2t": "Relay", + "f2d": "Bridge WebSocket and HTTP bidirectionally.", + "f3t": "Self-Host", + "f3d": "Private deploy, custom domains and reverse proxy.", + "flowDesktop": "Desktop", + "flowDesktopSub": "BitFun Agent", + "flowRelay": "Relay Server", + "flowRelaySub": "Pair and Forward", + "flowMobile": "Mobile", + "flowMobileSub": "Remote Control" + } +} diff --git a/src/apps/relay-server/static/homepage/index.html b/src/apps/relay-server/static/homepage/index.html index 1a5b656f4..a0ab330a3 100644 --- a/src/apps/relay-server/static/homepage/index.html +++ b/src/apps/relay-server/static/homepage/index.html @@ -1,5 +1,5 @@ - + @@ -156,10 +156,10 @@ BitFun Relay Server @@ -168,7 +168,7 @@

BitFun Relay Server

-

桌面端与移动端之间的中继服务,提供配对与消息转发能力。

+

Relay service between desktop and mobile, enabling pairing and message forwarding.

WebSocket
@@ -194,16 +194,16 @@

BitFun Relay Server

- 中继服务 - 配对 & 转发 + Relay Server + Pair and Forward
HTTP
- 移动端 - 远程控制 + Mobile + Remote Control
@@ -211,23 +211,23 @@

BitFun Relay Server

-

建立连接

+

Connect

-

承接配对请求,建立远程会话。

+

Handle pairing requests, create remote sessions.

-

消息转发

+

Relay

-

桥接 WebSocket 与 HTTP 双向通信。

+

Bridge WebSocket and HTTP bidirectionally.

-

自托管友好

+

Self-Host

-

私有部署、自定义域名与反向代理。

+

Private deploy, custom domains and reverse proxy.

@@ -236,18 +236,14 @@

自托管友好

diff --git a/src/mobile-web/src/App.tsx b/src/mobile-web/src/App.tsx index 92674d931..6aa278361 100644 --- a/src/mobile-web/src/App.tsx +++ b/src/mobile-web/src/App.tsx @@ -81,7 +81,7 @@ const AppContent: React.FC = () => { useEffect(() => () => clearTimeout(timerRef.current), []); - // 全局链接点击处理 - 确保所有外部链接在新标签页打开 + // Open external links in a new tab from anywhere in the app. useEffect(() => { const handleLinkClick = (e: MouseEvent) => { const target = e.target as HTMLElement; @@ -89,7 +89,7 @@ const AppContent: React.FC = () => { if (link && link.href) { const href = link.href; - // 检查是否是外部链接 (http/https 且不是当前域名) + // Treat all http(s) links as external for the mobile web shell. if (href.startsWith('http://') || href.startsWith('https://')) { e.preventDefault(); e.stopPropagation(); @@ -98,7 +98,7 @@ const AppContent: React.FC = () => { } }; - // 添加全局点击监听 + // Capture link clicks before nested content handles them. document.addEventListener('click', handleLinkClick, true); return () => { diff --git a/src/mobile-web/src/i18n/messages.ts b/src/mobile-web/src/i18n/messages.ts index 27de53f75..803faa409 100644 --- a/src/mobile-web/src/i18n/messages.ts +++ b/src/mobile-web/src/i18n/messages.ts @@ -327,7 +327,7 @@ export const messages: Record = { modelPrimary: 'Primary 模型', modelFast: 'Fast 模型', modelNotConfigured: '模型未配置', - askQuestionCount: '{count} 个问题', + askQuestionCount: '{count} 个问题{suffix}', waiting: '等待中', modeAgentic: 'Agentic', modePlan: 'Plan', @@ -335,7 +335,7 @@ export const messages: Record = { thinking: '思考中...', allTasksCompleted: '所有任务已完成', task: '任务', - toolCalls: '{count} 次工具调用', + toolCalls: '{count} 次工具调用{suffix}', done: '已完成 {count}', running: '运行中 {count}', thoughtCharacters: '思考 {count} 个字符', @@ -509,7 +509,7 @@ export const messages: Record = { modelPrimary: 'Primary 模型', modelFast: 'Fast 模型', modelNotConfigured: '模型未配置', - askQuestionCount: '{count} 個問題', + askQuestionCount: '{count} 個問題{suffix}', waiting: '等待中', modeAgentic: 'Agentic', modePlan: 'Plan', @@ -517,7 +517,7 @@ export const messages: Record = { thinking: '思考中...', allTasksCompleted: '所有任務已完成', task: '任務', - toolCalls: '{count} 次工具調用', + toolCalls: '{count} 次工具調用{suffix}', done: '已完成 {count}', running: '運行中 {count}', thoughtCharacters: '思考 {count} 個字符', diff --git a/src/mobile-web/src/pages/ChatPage.tsx b/src/mobile-web/src/pages/ChatPage.tsx index 1705ba843..34f7ab0e1 100644 --- a/src/mobile-web/src/pages/ChatPage.tsx +++ b/src/mobile-web/src/pages/ChatPage.tsx @@ -4,6 +4,7 @@ import remarkGfm from 'remark-gfm'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useI18n } from '../i18n'; +import { messages } from '../i18n/messages'; import { RemoteSessionManager, SessionPoller, @@ -33,6 +34,10 @@ function formatDuration(ms: number): string { return `${(ms / 1000).toFixed(1)}s`; } +function getEnglishPluralSuffix(language: string, count: number): string { + return language === 'en-US' && count !== 1 ? 's' : ''; +} + function truncateMiddle(str: string, maxLen: number): string { if (!str || str.length <= maxLen) return str; const keep = maxLen - 3; @@ -151,7 +156,7 @@ function normalizeFileLikeHref(rawHref: string): string { } } - // Normalize URI-like Windows absolute paths such as `/C:/Users/...`. + // Normalize URI-like Windows absolute paths with a leading slash before the drive letter. if (/^\/[A-Za-z]:[\\/]/.test(filePath)) { filePath = filePath.slice(1); } @@ -474,7 +479,7 @@ const MarkdownContent: React.FC = ({ content, onFileDownlo // Fallback: render as plain text for computer:// links without handler, // or as a regular link for http(s) links. if (typeof href === 'string') { - // 所有外部链接都在新标签页打开 + // Open all external links in a new tab. const isExternalLink = href.startsWith('http://') || href.startsWith('https://'); if (isExternalLink) { return ( @@ -745,7 +750,7 @@ const TaskToolCard: React.FC<{ subItems?: ChatMessageItem[]; onCancelTool?: (toolId: string) => void; }> = ({ tool, now, subItems = [], onCancelTool }) => { - const { t } = useI18n(); + const { t, language } = useI18n(); const scrollRef = useRef(null); const prevCountRef = useRef(0); const [stepsExpanded, setStepsExpanded] = useState(false); @@ -818,7 +823,7 @@ const TaskToolCard: React.FC<{ <>
setStepsExpanded(e => !e)}> - {t('chat.toolCalls', { count: subTools.length, suffix: subTools.length === 1 ? '' : 's' })} + {t('chat.toolCalls', { count: subTools.length, suffix: getEnglishPluralSuffix(language, subTools.length) })} {t('chat.done', { count: subToolsDone })} @@ -1081,7 +1086,7 @@ const ToolList: React.FC<{ now: number; onCancelTool?: (toolId: string) => void; }> = ({ tools, now, onCancelTool }) => { - const { t } = useI18n(); + const { t, language } = useI18n(); const scrollRef = useRef(null); const prevCountRef = useRef(0); const [expanded, setExpanded] = useState(false); @@ -1111,7 +1116,7 @@ const ToolList: React.FC<{ return (
setExpanded(e => !e)}> - {t('chat.toolCalls', { count: tools.length, suffix: tools.length === 1 ? '' : 's' })} + {t('chat.toolCalls', { count: tools.length, suffix: getEnglishPluralSuffix(language, tools.length) })} {doneCount > 0 && {t('chat.done', { count: doneCount })}} {runningCount > 0 && {t('chat.running', { count: runningCount })}} @@ -1224,13 +1229,35 @@ const isPendingAskUserQuestion = (tool?: RemoteToolStatus | null) => { return !['completed', 'failed', 'cancelled', 'rejected'].includes(tool.status); }; -const isOtherQuestionOption = (label?: string) => { +function getMessageByPath(source: unknown, path: string): string | null { + const segments = path.split('.'); + let current: unknown = source; + + for (const segment of segments) { + if (!current || typeof current !== 'object' || !(segment in current)) { + return null; + } + current = (current as Record)[segment]; + } + + return typeof current === 'string' ? current : null; +} + +const OTHER_QUESTION_OPTION_LABELS = new Set([ + 'other', + ...Object.values(messages) + .map((localeMessages) => getMessageByPath(localeMessages, 'common.other')) + .filter((label): label is string => !!label) + .map((label) => label.trim().toLowerCase()), +]); + +const isOtherQuestionOption = (label: string | undefined) => { const normalized = (label || '').trim().toLowerCase(); - return normalized === 'other' || normalized === '其他'; + return OTHER_QUESTION_OPTION_LABELS.has(normalized); }; const AskQuestionCard: React.FC = ({ tool, onAnswer }) => { - const { t } = useI18n(); + const { t, language } = useI18n(); const questions: any[] = tool.tool_input?.questions || []; const [selected, setSelected] = useState>({}); const [customTexts, setCustomTexts] = useState>({}); @@ -1295,7 +1322,7 @@ const AskQuestionCard: React.FC = ({ tool, onAnswer }) => return (
- {t('chat.askQuestionCount', { count: questions.length, suffix: questions.length > 1 ? 's' : '' })} + {t('chat.askQuestionCount', { count: questions.length, suffix: getEnglishPluralSuffix(language, questions.length) })} {!submitted && !submitting && ( {t('chat.waiting')} )} diff --git a/src/mobile-web/src/services/RemoteSessionManager.ts b/src/mobile-web/src/services/RemoteSessionManager.ts index 1e760dab2..34de29144 100644 --- a/src/mobile-web/src/services/RemoteSessionManager.ts +++ b/src/mobile-web/src/services/RemoteSessionManager.ts @@ -514,7 +514,7 @@ export class SessionPoller { private getInterval(): number { if (document.visibilityState !== 'visible') return 5000; - // 如果在宽限期内(turn 刚刚结束),继续保持快速轮询 + // Keep fast polling during the short grace period after a turn ends. const now = Date.now(); if (this.turnJustEndedAt != null && (now - this.turnJustEndedAt) < this.TURN_JUST_ENDED_GRACE_PERIOD_MS) { return 1000; @@ -560,12 +560,12 @@ export class SessionPoller { const isActiveNow = resp.active_turn != null && resp.active_turn.status === 'active'; this.hasActiveTurn = isActiveNow; - // 检测到 active_turn 刚刚结束,设置宽限期 + // Start the grace period when active_turn just ended. if (wasActive && !isActiveNow) { this.turnJustEndedAt = Date.now(); } - // 如果有新消息或者 active_turn 仍然活跃,重置宽限期 + // Clear the grace period once new messages arrive. if (resp.new_messages && resp.new_messages.length > 0) { this.turnJustEndedAt = null; } diff --git a/src/web-ui/src/app/scenes/settings/SettingsNav.tsx b/src/web-ui/src/app/scenes/settings/SettingsNav.tsx index 27b25e455..b5a9f6751 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsNav.tsx +++ b/src/web-ui/src/app/scenes/settings/SettingsNav.tsx @@ -28,6 +28,7 @@ import { SETTINGS_TAB_SEARCH_CONTENT } from './settingsTabSearchContent'; import './SettingsNav.scss'; const SEARCH_DEBOUNCE_MS = 150; +type SettingsT = (key: string, options?: Record) => unknown; export interface SettingsSearchRow { tabId: ConfigTab; @@ -53,21 +54,37 @@ function resolveTabPageContentHaystack(i18n: I18nApi, tabId: ConfigTab): string return parts.join(' '); } +function translateString(t: SettingsT, key: string, defaultValue: string): string { + const value = t(key, { defaultValue }); + return typeof value === 'string' ? value : defaultValue; +} + +function readSearchAliases(t: SettingsT, tabId: ConfigTab): string[] { + const aliases = t(`configCenter.searchAliases.${tabId}`, { + defaultValue: [], + returnObjects: true, + }); + return Array.isArray(aliases) + ? aliases.filter((alias): alias is string => typeof alias === 'string') + : []; +} + function buildSettingsSearchIndex( - t: (key: string, options?: Record) => string, + t: SettingsT, i18n: I18nApi ): SettingsSearchRow[] { const rows: SettingsSearchRow[] = []; for (const cat of SETTINGS_CATEGORIES) { - const categoryLabel = t(cat.nameKey, { defaultValue: cat.id }); + const categoryLabel = translateString(t, cat.nameKey, cat.id); for (const tabDef of cat.tabs) { - const tabLabel = t(tabDef.labelKey, { defaultValue: tabDef.id }); + const tabLabel = translateString(t, tabDef.labelKey, tabDef.id); const description = tabDef.descriptionKey - ? t(tabDef.descriptionKey, { defaultValue: '' }) + ? translateString(t, tabDef.descriptionKey, '') : ''; const kw = (tabDef.keywords ?? []).join(' '); + const aliases = readSearchAliases(t, tabDef.id).join(' '); const pageContent = resolveTabPageContentHaystack(i18n, tabDef.id); - const haystack = [categoryLabel, tabLabel, description, kw, tabDef.id, pageContent] + const haystack = [categoryLabel, tabLabel, description, kw, aliases, tabDef.id, pageContent] .join(' ') .toLowerCase(); rows.push({ diff --git a/src/web-ui/src/app/scenes/settings/settingsConfig.ts b/src/web-ui/src/app/scenes/settings/settingsConfig.ts index b3f810bb2..f1ad68eb1 100644 --- a/src/web-ui/src/app/scenes/settings/settingsConfig.ts +++ b/src/web-ui/src/app/scenes/settings/settingsConfig.ts @@ -107,8 +107,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ 'sessions', 'restore', 'unarchive', - '\u5f52\u6863', - '\u4f1a\u8bdd', ], }, { @@ -121,8 +119,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ 'keybinding', 'hotkey', 'shortcut key', - '\u5feb\u6377\u952e', - '\u952e\u4f4d', ], }, ], @@ -143,8 +139,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ 'pixel', 'pet', 'partner', - '\u4f19\u4f34', - '\u4e2a\u6027\u5316', ], }, { @@ -167,7 +161,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ 'search', 'flashgrep', 'index', - '\u6743\u9650', ], }, { @@ -182,8 +175,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ 'pull request', 'post-coding', 'shortcut', - '快捷动作', - '提交', ], }, { @@ -198,8 +189,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ 'subagent', 'readonly', 'audit', - '\u5ba1\u6838', - '\u4ee3\u7801\u5ba1\u6838', ], }, { diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx index 941083aba..7d70090e0 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx @@ -12,7 +12,6 @@ import { DotMatrixLoader } from '@/component-library'; import type { FlowTextItem } from '../types/flow-chat'; import { useFlowChatContext } from './modern/FlowChatContext'; import { useTypewriter } from '../hooks/useTypewriter'; -import { processingHintsZh, processingHintsEn } from '../constants/processingHints'; import './FlowTextBlock.scss'; // Idle timeout (ms) after content stops growing. @@ -24,6 +23,25 @@ interface FlowTextBlockProps { replayStreamingOnMount?: boolean; } +const RuntimeStatusBlock: React.FC> = ({ textItem, className = '' }) => { + const { t } = useTranslation('flow-chat/processing-hints'); + const rawHints = t('items', { returnObjects: true }); + const hints = Array.isArray(rawHints) + ? rawHints.filter((item): item is string => typeof item === 'string') + : []; + const hintIndex = hints.length > 0 + ? Math.abs(textItem.id.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0)) % hints.length + : 0; + const hint = hints[hintIndex] ?? ''; + + return ( +
+ + {hint && {hint}} +
+ ); +}; + /** * Use React.memo to avoid unnecessary re-renders. * Re-render only when key textItem fields change. @@ -34,7 +52,6 @@ export const FlowTextBlock = React.memo(({ replayStreamingOnMount = true }) => { const { onFileViewRequest, onTabOpen, onHttpLinkClick, onOpenVisualization } = useFlowChatContext(); - const { i18n } = useTranslation(); // Normalize content to a string. const content = typeof textItem.content === 'string' @@ -84,16 +101,7 @@ export const FlowTextBlock = React.memo(({ isContentGrowing; if (textItem.runtimeStatus) { - const hints = i18n.language.startsWith('zh') ? processingHintsZh : processingHintsEn; - const hintIndex = Math.abs(textItem.id.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0)) % hints.length; - const hint = hints[hintIndex]; - - return ( -
- - {hint} -
- ); + return ; } return ( diff --git a/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.tsx b/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.tsx index a5715e345..eb79194fd 100644 --- a/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DotMatrixLoader } from '@/component-library'; -import { processingHintsZh, processingHintsEn } from '../../constants/processingHints'; import './ProcessingIndicator.scss'; interface ProcessingIndicatorProps { @@ -18,8 +17,11 @@ interface ProcessingIndicatorProps { } export const ProcessingIndicator: React.FC = ({ visible, reserveSpace = false }) => { - const { i18n } = useTranslation(); - const hints = i18n.language.startsWith('zh') ? processingHintsZh : processingHintsEn; + const { t } = useTranslation('flow-chat/processing-hints'); + const rawHints = t('items', { returnObjects: true }); + const hints = Array.isArray(rawHints) + ? rawHints.filter((item): item is string => typeof item === 'string') + : []; const [showHint, setShowHint] = useState(false); const [hintIndex, setHintIndex] = useState(0); @@ -28,7 +30,7 @@ export const ProcessingIndicator: React.FC = ({ visibl const rotateTimerRef = useRef | null>(null); useEffect(() => { - if (visible) { + if (visible && hints.length > 0) { const initialIndex = Math.floor(Math.random() * hints.length); setHintIndex(initialIndex); diff --git a/src/web-ui/src/flow_chat/constants/processingHints.ts b/src/web-ui/src/flow_chat/constants/processingHints.ts deleted file mode 100644 index b3fd4372e..000000000 --- a/src/web-ui/src/flow_chat/constants/processingHints.ts +++ /dev/null @@ -1,420 +0,0 @@ -/** - * Fun processing hint messages shown while the AI is working. - * Designed to ease user anxiety with light-hearted copy. - */ - -export const processingHintsZh: string[] = [ - // --- Tech / ML metaphors --- - "思维回路全速运转...", - "Token 们奋力排列中...", - "矩阵乘法进行中...", - "Transformer 全力变形...", - "Embeddings 寻找彼此...", - "最后一次梯度下降...", - "注意力机制已锁定...", - "思维发动机轰鸣启动...", - "思维路线图展开中...", - "激活函数全员上岗...", - "推理链跑起来了...", - "十四亿参数找答案...", - "上下文窗口挤一挤...", - "最合适的词正在出现...", - "正在 Debug 思维链...", - "推导链节节延伸...", - "在语言的岔路口选方向...", - "思路全线开动...", - "向量空间里迷了路...", - "量子态还没塌缩...", - "正在过滤噪音...", - "从千万想法里提炼精华...", - "灵感之流汇聚成河...", - "数百万次思考之后...", - "所有脑力为你服务...", - "思绪整装,即将出发...", - - // --- Personification & humor --- - "给神经网络倒咖啡...", - "模型正在思考人生...", - "神经元群情激昂...", - "反向传播感悟中...", - "正在召开内部评审会...", - "多个思路正在 PK...", - "神经元:已就位...", - "神经网络的午夜加班...", - "正在假装镇定地高速运转...", - "暗中使力中...", - "绞尽脑汁为你服务...", - "正在对话宇宙...", - "向宇宙借创意...", - - // --- Progress --- - "答案在路上了...", - "拼图最后一块到位...", - "思路合流中...", - "逻辑线头找到了...", - "差不多了,收尾中...", - "还差一点点...", - "就差临门一脚...", - "搞定了百分之九十九...", - "最终赢家即将揭晓...", - "答案正在着陆跑道...", - "最后校对一遍...", - "正在打磨最后措辞...", - "正在精简表达...", - "填充最后几块拼图...", - "高质量输出即将发射...", - "系好安全带,即将着陆...", - "即将顿悟...", - "答案就在嘴边...", - - // --- Imagination --- - "翻阅无数可能中...", - "召唤创意,稍候...", - "脑洞已打开...", - "在十万八千条思路里取最优...", - "把你的问题投影到高维空间...", - "好问题!让我选一个薛定谔的答案...", - "在知识图谱里找路...", - "知识正在快速结晶...", - "把抽象想法具体化...", - "思维导图疯狂生长...", - "在概念森林砍出一条路...", - "想法从潜空间投影...", - "将混乱整理成诗...", - "折叠所有可能成一答...", - "举一反三进行中...", - "联想力爆棚中...", - "正在破解难题密码...", - "智慧的光正在聚焦...", - - // --- Light & confident --- - "字斟句酌,只给精华...", - "想想,再想想...", - "已经快了(真的)...", - "快了!(不是在敷衍)", - "这道题,我会!", - "收到!全力处理中...", - "就在这里,别急...", - "全情投入,心无旁骛...", - "私下已想了七八种方案...", - "比想象中更努力...", - "认真的,从没这么认真过...", - "比光速还快(误)...", - "才华正在集中爆发...", - "正在憋大招...", - "超强输出,即将到来...", - "最强答案正在成形...", - - // --- Concrete metaphors --- - "把散落的灵感拼起来...", - "用知识蒸馏一杯好茶...", - "灵感在深处发酵...", - "知识浪潮涌向输出端...", - "脑图正在扩展中...", - "思维潮水汇聚中...", - "思维火焰正旺...", - "知识在快速结晶...", - "脑内灯泡闪烁中...", - "思路打结,解开中...", - "思维信号穿梭神经丛...", - "秒速百公里地思考...", - "知识储备调用中...", - "知识能量正在充能...", - "智慧内核全速转...", - - // --- Care & focus --- - "脑子已经全力转起来了...", - "正在为你量身定制...", - "把最好的留到最后...", - "用心对待每一个字...", - "全神贯注于你的问题...", - "注意力全给你了...", - "这一刻,全给你...", - "正在起草一份诚意回答...", - "灵感正在悄悄聚拢...", - "倾尽所有给你答案...", - "绝不摆烂,全力以赴...", - "努力营业中...", - - // --- Short beats --- - "混沌转逻辑中...", - "思考齿轮咬合中...", - "解码深邃想法中...", - "合并所有子思路...", - "抢救迷失的语义...", - "拆解成千个小问题...", - "千个念头取最好...", - "把想法和文字对齐...", - "整理千头万绪中...", - "确认答案合理性中...", - "检查一遍,再检查...", - "把模糊变清晰中...", - "思维正在对焦...", - "多线程思维全力汇总...", - "把逻辑再拧紧一点...", - "让逻辑自己说话...", - "在语言丛林里寻宝...", - - // --- More fun --- - "全速冲刺,答案在望...", - "思维扩容,装下更多...", - "为回答精心调味...", - "给逻辑链上润滑油...", - "把回答装进包裹打结中...", - "逻辑拼图已完成...", - "有条不紊,稳步推进...", - "万事就绪,答案出发...", - "脑波达到最高频...", - "思路刚打通,马上来...", - "解开语言的最后一层...", - "用推理缝合碎片...", - "召开紧急大脑会议...", - "整理出最佳路径...", - "正在审阅自己的思维...", - "正在进行最后冲刺...", - "努力中,绝不放弃...", - "正在做些不可思议的事...", - "思考容量拉满...", - "我在,我想,我输出...", - "后台小剧场精彩上演...", - "正在奋笔疾书...", - "在参数空间漫步...", - "正在将想法变为现实...", - "知识全力驰援中...", - "删删改改中...", - "像素精灵正在组字...", - "比特们正在排队...", - "比特流已启动...", - "思维启动加速度...", - "正在给回答注入灵魂...", - "准备好了吗?快了...", - "就差最后一步...", - "输出即将解冻...", - "神经丛林探险中...", - "答案雏形已在手...", - "思路正在合流...", - "尽力而为,必有所获...", - "正在快乐工作中...", - "大脑正在全速燃烧...", - "答案即将成形...", - "思维的潮水汇聚中...", - "正在把复杂变简单...", - "脑中千个念头筛选中...", - "正在为你开天辟地...", - "知识海洋里捞最优解...", - "万亿可能取其一...", - "答案正在破壳而出...", - "大脑高速公路全线畅通...", - "最佳路径已锁定...", - "思维齐步走,答案来了...", - "大招正在蓄力中...", - "答案即将横空出世...", -]; - -export const processingHintsEn: string[] = [ - // --- Tech / ML metaphors --- - "Neural circuits running at full speed...", - "Tokens lining up in perfect order...", - "Matrix multiplications in progress...", - "Transformer: transforming at full capacity...", - "Embeddings searching for their soulmates...", - "One final gradient descent to go...", - "Attention heads: activated and locked...", - "Thought engine roaring to life...", - "Mental roadmap unfolding...", - "Activation functions reporting for duty...", - "Reasoning chain is running...", - "Spelunking through fourteen billion parameters...", - "Squeezing into the context window...", - "The perfect word is surfacing...", - "Debugging my own chain of thought...", - "Reasoning links extending one by one...", - "Picking a path through the language labyrinth...", - "All thought lanes open and moving...", - "Got lost in vector space, back soon...", - "Quantum state hasn't collapsed yet...", - "Filtering out the noise...", - "Distilling the best from a million thoughts...", - "Rivers of inspiration converging...", - "After millions of mental iterations...", - "All brainpower dedicated to you...", - "Thoughts packed and ready to go...", - - // --- Personification & humor --- - "Feeding the model its morning coffee...", - "Pondering the meaning of tokens...", - "Neurons firing in passionate debate...", - "Backpropagating through life choices...", - "Holding an internal peer review...", - "Multiple ideas in a heated competition...", - "Neurons: reporting for duty...", - "Neural network pulling an all-nighter...", - "Pretending to be calm while going full speed...", - "Quietly working magic in the background...", - "Wracking every synapse for your sake...", - "Consulting the cosmos...", - "Borrowing creativity from the universe...", - - // --- Progress --- - "Answer is on its way...", - "Last puzzle piece sliding into place...", - "Thoughts merging into one stream...", - "Found the thread of logic...", - "Almost done, wrapping up...", - "Just a tiny bit more...", - "One final push to go...", - "Ninety-nine percent done...", - "The winner is about to be revealed...", - "Answer on final approach for landing...", - "One last proofread...", - "Polishing the last few words...", - "Trimming the fat from the response...", - "Filling in the last few puzzle pieces...", - "High-quality output launching soon...", - "Fasten your seatbelt — landing shortly...", - "Epiphany incoming...", - "Answer is right on the tip of my tongue...", - - // --- Imagination --- - "Rummaging through the multiverse of ideas...", - "Summoning creativity, please hold...", - "Brain hole: open...", - "Picking the best from ten thousand thoughts...", - "Projecting your question into high-dimensional space...", - "Great question! Picking from Schrödinger's answers...", - "Navigating the knowledge graph...", - "Knowledge crystallizing rapidly...", - "Turning abstract ideas into concrete ones...", - "Mind map expanding like wildfire...", - "Hacking a path through the concept forest...", - "Ideas projecting from latent space...", - "Turning chaos into something like poetry...", - "Folding all possible futures into one answer...", - "Analogical reasoning in progress...", - "Associative power: overclocked...", - "Cracking the code of a tough problem...", - "Focusing the beam of intelligence...", - - // --- Light & confident --- - "Hand-picking only the best words...", - "Let me think... still thinking...", - "I'm fast, I promise...", - "Almost there! (Not stalling, really)", - "I know this one!", - "Roger that! Processing at full capacity...", - "Right here, no rush...", - "Fully in the zone right now...", - "Already tried seven different approaches internally...", - "Working harder than you'd expect...", - "Never been this focused in my life...", - "Faster than light (not really)...", - "Talent concentrated and detonating...", - "Charging up the big move...", - "Supercharged output incoming...", - "The best answer is taking shape...", - - // --- Concrete metaphors --- - "Gathering the scattered sparks of inspiration...", - "Distilling knowledge into a fine brew...", - "Ideas fermenting in the neural depths...", - "Tidal waves of knowledge rushing to the output...", - "Mind map expanding at max speed...", - "Waves of thought converging...", - "Thinking flame burning bright...", - "Knowledge solidifying into form...", - "Lightbulb flickering in the back of my mind...", - "Untangling the knots in my reasoning...", - "Thought signals racing through the neural jungle...", - "Thinking at hundreds of miles per second...", - "Pulling from deep knowledge reserves...", - "Knowledge energy charging up...", - "Intelligence core running at max rpm...", - - // --- Care & focus --- - "Brain already spinning at full tilt...", - "Tailoring this response just for you...", - "Saving the best for last...", - "Crafting every word with care...", - "Fully focused on your question...", - "All attention given to you...", - "This moment is entirely for you...", - "Drafting a sincere and thoughtful reply...", - "Inspiration quietly gathering...", - "Giving everything I've got for your answer...", - "Zero slacking, full effort...", - "Diligently serving your request...", - - // --- Short beats --- - "Converting chaos into logic...", - "Gears of thought clicking into place...", - "Decoding your profound question...", - "Merging all sub-ideas...", - "Rescuing lost semantic meaning...", - "Breaking it into a thousand micro-problems...", - "Picking the best of a thousand thoughts...", - "Lining up thoughts with words...", - "Sorting through a tangle of threads...", - "Verifying the answer makes sense...", - "Checking once, then checking again...", - "Turning fuzzy into clear...", - "Thoughts coming into focus...", - "All threads converging at full speed...", - "Tightening the logic just a little more...", - "Letting logic speak for itself...", - "Treasure-hunting through the language forest...", - - // --- More fun --- - "Sprinting to the finish line...", - "Expanding the thought tank...", - "Adding just the right pinch of creativity...", - "Oiling the chain of logic...", - "Tying a bow on your response package...", - "Logic puzzle: assembled...", - "Moving steadily, step by step...", - "Everything ready — answer departing now...", - "Brainwave frequency at all-time high...", - "Thread of thought just clicked — almost there...", - "Unwrapping the final layer of language...", - "Stitching the fragments together with reasoning...", - "Calling an emergency brain session...", - "Plotting the optimal path...", - "Reviewing my own chain of thought...", - "Making the final sprint...", - "Pushing through, won't give up...", - "Doing something kind of amazing right now...", - "Thinking capacity: maxed out...", - "I think, therefore I output...", - "A tiny drama unfolding backstage...", - "Writing at full speed...", - "Wandering the parameter space...", - "Turning ideas into reality...", - "All knowledge rallying to help...", - "Editing and re-editing...", - "Pixel elves composing text...", - "Bits falling into line...", - "Bit stream: engaged...", - "Thought engine accelerating...", - "Infusing the response with soul...", - "Ready for it? Almost there...", - "One last step to go...", - "Output thawing out...", - "Exploring the neural jungle...", - "Answer prototype taking form...", - "Thoughts flowing into one channel...", - "Doing my very best, as always...", - "Happily working away...", - "Brain burning at full speed...", - "Answer taking shape...", - "Waves of thought converging on the answer...", - "Turning complexity into simplicity...", - "Filtering thousands of thoughts right now...", - "Creating something from nothing, just for you...", - "Fishing the best answer from an ocean of knowledge...", - "One in a trillion possibilities, chosen for you...", - "Answer hatching right now...", - "The brain highway is clear and running fast...", - "Optimal path: locked in...", - "Thoughts marching in step — answer incoming...", - "Done in one second (approximately)...", - "Casting a chaos-to-clarity spell...", -]; diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts index e9d74e71b..953f6c2c8 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts @@ -747,7 +747,7 @@ export async function ensureBackendSession( const raw = typeof e?.message === 'string' ? e.message : String(e); const hint = raw.includes('Session metadata not found') || raw.includes('Not found') - ? '在后端找不到该会话数据。若刚重新连接过 SSH 远程工作区,请关闭并重新打开该远程项目,或新建会话后再试。' + ? i18nService.t('flow-chat:historyState.remoteSessionMissing') : raw; throw new Error(hint); } diff --git a/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts b/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts index eb6c907aa..f3d0e205d 100644 --- a/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts +++ b/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts @@ -3,9 +3,11 @@ import { readFile } from '@tauri-apps/plugin-fs'; import type { AgentCompanionPetSelection } from './AIExperienceConfigService'; import { isTauriRuntime } from '@/infrastructure/runtime'; import { createLogger } from '@/shared/utils/logger'; +import builtinPetMetadata from './agentCompanionBuiltinPetMetadata.json'; const log = createLogger('AgentCompanionPetService'); const BUILTIN_PET_BASE = '/agent-companion-pets'; +const BUILTIN_PET_DISPLAY_NAMES = builtinPetMetadata.displayNames; export const DEFAULT_AGENT_COMPANION_PET: AgentCompanionPetSelection = { id: 'panda-pix', @@ -89,7 +91,7 @@ const BUILTIN_PETS: AgentCompanionPetSelection[] = [ }, { id: 'gugugaga', - displayName: '咕咕嘎嘎', + displayName: BUILTIN_PET_DISPLAY_NAMES.gugugaga, description: 'A cheerful chibi girl in a black penguin suit with a simple silver collar pendant.', source: 'preset', packagePath: `${BUILTIN_PET_BASE}/gugugaga`, @@ -117,7 +119,7 @@ const BUILTIN_PETS: AgentCompanionPetSelection[] = [ }, { id: 'jiyi', - displayName: '吉伊', + displayName: BUILTIN_PET_DISPLAY_NAMES.jiyi, description: 'A round white chibi bear with dark chocolate outlines, pink cheeks, tiny limbs, curled ears, and a small pink bear pouch.', source: 'preset', diff --git a/src/web-ui/src/infrastructure/config/services/agentCompanionBuiltinPetMetadata.json b/src/web-ui/src/infrastructure/config/services/agentCompanionBuiltinPetMetadata.json new file mode 100644 index 000000000..b4408c78f --- /dev/null +++ b/src/web-ui/src/infrastructure/config/services/agentCompanionBuiltinPetMetadata.json @@ -0,0 +1,6 @@ +{ + "displayNames": { + "gugugaga": "咕咕嘎嘎", + "jiyi": "吉伊" + } +} diff --git a/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts b/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts index e13ff6f43..7f1f820d9 100644 --- a/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts +++ b/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts @@ -9,6 +9,7 @@ export const ALL_NAMESPACES = [ 'components', 'errors', 'flow-chat', + 'flow-chat/processing-hints', 'notifications', 'panels/files', 'panels/git', diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 5f6b7a866..72c4a0942 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -858,6 +858,7 @@ "fixPreview": "Fix Preview", "gitDiff": "Git Diff", "gitSettings": "Git Settings", + "pullRequests": "Pull Requests", "taskPlanner": "Task Planner", "fileBrowser": "File Browser", "editor": "Editor", diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index e1f2ba51d..8e33c9442 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -44,6 +44,7 @@ "loadingDescription": "Preparing the conversation history.", "failedTitle": "Session history did not load", "failedDescription": "Retry loading the saved conversation.", + "remoteSessionMissing": "The backend session data was not found. If you just reconnected an SSH remote workspace, close and reopen the remote project, or create a new session and try again.", "retry": "Retry" }, "usage": { diff --git a/src/web-ui/src/locales/en-US/flow-chat/processing-hints.json b/src/web-ui/src/locales/en-US/flow-chat/processing-hints.json new file mode 100644 index 000000000..610131aae --- /dev/null +++ b/src/web-ui/src/locales/en-US/flow-chat/processing-hints.json @@ -0,0 +1,192 @@ +{ + "items": [ + "Neural circuits running at full speed...", + "Tokens lining up in perfect order...", + "Matrix multiplications in progress...", + "Transformer: transforming at full capacity...", + "Embeddings searching for their soulmates...", + "One final gradient descent to go...", + "Attention heads: activated and locked...", + "Thought engine roaring to life...", + "Mental roadmap unfolding...", + "Activation functions reporting for duty...", + "Reasoning chain is running...", + "Spelunking through fourteen billion parameters...", + "Squeezing into the context window...", + "The perfect word is surfacing...", + "Debugging my own chain of thought...", + "Reasoning links extending one by one...", + "Picking a path through the language labyrinth...", + "All thought lanes open and moving...", + "Got lost in vector space, back soon...", + "Quantum state hasn't collapsed yet...", + "Filtering out the noise...", + "Distilling the best from a million thoughts...", + "Rivers of inspiration converging...", + "After millions of mental iterations...", + "All brainpower dedicated to you...", + "Thoughts packed and ready to go...", + "Feeding the model its morning coffee...", + "Pondering the meaning of tokens...", + "Neurons firing in passionate debate...", + "Backpropagating through life choices...", + "Holding an internal peer review...", + "Multiple ideas in a heated competition...", + "Neurons: reporting for duty...", + "Neural network pulling an all-nighter...", + "Pretending to be calm while going full speed...", + "Quietly working magic in the background...", + "Wracking every synapse for your sake...", + "Consulting the cosmos...", + "Borrowing creativity from the universe...", + "Answer is on its way...", + "Last puzzle piece sliding into place...", + "Thoughts merging into one stream...", + "Found the thread of logic...", + "Almost done, wrapping up...", + "Just a tiny bit more...", + "One final push to go...", + "Ninety-nine percent done...", + "The winner is about to be revealed...", + "Answer on final approach for landing...", + "One last proofread...", + "Polishing the last few words...", + "Trimming the fat from the response...", + "Filling in the last few puzzle pieces...", + "High-quality output launching soon...", + "Fasten your seatbelt — landing shortly...", + "Epiphany incoming...", + "Answer is right on the tip of my tongue...", + "Rummaging through the multiverse of ideas...", + "Summoning creativity, please hold...", + "Brain hole: open...", + "Picking the best from ten thousand thoughts...", + "Projecting your question into high-dimensional space...", + "Great question! Picking from Schrödinger's answers...", + "Navigating the knowledge graph...", + "Knowledge crystallizing rapidly...", + "Turning abstract ideas into concrete ones...", + "Mind map expanding like wildfire...", + "Hacking a path through the concept forest...", + "Ideas projecting from latent space...", + "Turning chaos into something like poetry...", + "Folding all possible futures into one answer...", + "Analogical reasoning in progress...", + "Associative power: overclocked...", + "Cracking the code of a tough problem...", + "Focusing the beam of intelligence...", + "Hand-picking only the best words...", + "Let me think... still thinking...", + "I'm fast, I promise...", + "Almost there! (Not stalling, really)", + "I know this one!", + "Roger that! Processing at full capacity...", + "Right here, no rush...", + "Fully in the zone right now...", + "Already tried seven different approaches internally...", + "Working harder than you'd expect...", + "Never been this focused in my life...", + "Faster than light (not really)...", + "Talent concentrated and detonating...", + "Charging up the big move...", + "Supercharged output incoming...", + "The best answer is taking shape...", + "Gathering the scattered sparks of inspiration...", + "Distilling knowledge into a fine brew...", + "Ideas fermenting in the neural depths...", + "Tidal waves of knowledge rushing to the output...", + "Mind map expanding at max speed...", + "Waves of thought converging...", + "Thinking flame burning bright...", + "Knowledge solidifying into form...", + "Lightbulb flickering in the back of my mind...", + "Untangling the knots in my reasoning...", + "Thought signals racing through the neural jungle...", + "Thinking at hundreds of miles per second...", + "Pulling from deep knowledge reserves...", + "Knowledge energy charging up...", + "Intelligence core running at max rpm...", + "Brain already spinning at full tilt...", + "Tailoring this response just for you...", + "Saving the best for last...", + "Crafting every word with care...", + "Fully focused on your question...", + "All attention given to you...", + "This moment is entirely for you...", + "Drafting a sincere and thoughtful reply...", + "Inspiration quietly gathering...", + "Giving everything I've got for your answer...", + "Zero slacking, full effort...", + "Diligently serving your request...", + "Converting chaos into logic...", + "Gears of thought clicking into place...", + "Decoding your profound question...", + "Merging all sub-ideas...", + "Rescuing lost semantic meaning...", + "Breaking it into a thousand micro-problems...", + "Picking the best of a thousand thoughts...", + "Lining up thoughts with words...", + "Sorting through a tangle of threads...", + "Verifying the answer makes sense...", + "Checking once, then checking again...", + "Turning fuzzy into clear...", + "Thoughts coming into focus...", + "All threads converging at full speed...", + "Tightening the logic just a little more...", + "Letting logic speak for itself...", + "Treasure-hunting through the language forest...", + "Sprinting to the finish line...", + "Expanding the thought tank...", + "Adding just the right pinch of creativity...", + "Oiling the chain of logic...", + "Tying a bow on your response package...", + "Logic puzzle: assembled...", + "Moving steadily, step by step...", + "Everything ready — answer departing now...", + "Brainwave frequency at all-time high...", + "Thread of thought just clicked — almost there...", + "Unwrapping the final layer of language...", + "Stitching the fragments together with reasoning...", + "Calling an emergency brain session...", + "Plotting the optimal path...", + "Reviewing my own chain of thought...", + "Making the final sprint...", + "Pushing through, won't give up...", + "Doing something kind of amazing right now...", + "Thinking capacity: maxed out...", + "I think, therefore I output...", + "A tiny drama unfolding backstage...", + "Writing at full speed...", + "Wandering the parameter space...", + "Turning ideas into reality...", + "All knowledge rallying to help...", + "Editing and re-editing...", + "Pixel elves composing text...", + "Bits falling into line...", + "Bit stream: engaged...", + "Thought engine accelerating...", + "Infusing the response with soul...", + "Ready for it? Almost there...", + "One last step to go...", + "Output thawing out...", + "Exploring the neural jungle...", + "Answer prototype taking form...", + "Thoughts flowing into one channel...", + "Doing my very best, as always...", + "Happily working away...", + "Brain burning at full speed...", + "Answer taking shape...", + "Waves of thought converging on the answer...", + "Turning complexity into simplicity...", + "Filtering thousands of thoughts right now...", + "Creating something from nothing, just for you...", + "Fishing the best answer from an ocean of knowledge...", + "One in a trillion possibilities, chosen for you...", + "Answer hatching right now...", + "The brain highway is clear and running fast...", + "Optimal path: locked in...", + "Thoughts marching in step — answer incoming...", + "Done in one second (approximately)...", + "Casting a chaos-to-clarity spell..." + ] +} diff --git a/src/web-ui/src/locales/en-US/settings.json b/src/web-ui/src/locales/en-US/settings.json index 3257a1d8b..40d996d38 100644 --- a/src/web-ui/src/locales/en-US/settings.json +++ b/src/web-ui/src/locales/en-US/settings.json @@ -7,6 +7,14 @@ "searchNoResults": "No matching settings", "searchClear": "Clear search", "beta": "Beta", + "searchAliases": { + "archivedSessions": [], + "keyboard": [], + "sessionPersonalization": [], + "sessionPermissions": [], + "quickActions": [], + "review": [] + }, "tabDescriptions": { "basics": "Logging, terminal shell, notifications, and launch at login.", "appearance": "Language, theme, and UI font size.", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 612e2fc67..293e7174e 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -858,6 +858,7 @@ "fixPreview": "修复预览", "gitDiff": "Git Diff", "gitSettings": "Git 设置", + "pullRequests": "Pull Requests", "taskPlanner": "任务规划器", "fileBrowser": "文件浏览器", "editor": "编辑器", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 8b6c0ee15..c647fa18a 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -44,6 +44,7 @@ "loadingDescription": "正在准备该会话的历史内容。", "failedTitle": "历史会话加载失败", "failedDescription": "可以重新加载已保存的会话内容。", + "remoteSessionMissing": "在后端找不到该会话数据。若刚重新连接过 SSH 远程工作区,请关闭并重新打开该远程项目,或新建会话后再试。", "retry": "重试" }, "usage": { diff --git a/src/web-ui/src/locales/zh-CN/flow-chat/processing-hints.json b/src/web-ui/src/locales/zh-CN/flow-chat/processing-hints.json new file mode 100644 index 000000000..13cee1a38 --- /dev/null +++ b/src/web-ui/src/locales/zh-CN/flow-chat/processing-hints.json @@ -0,0 +1,192 @@ +{ + "items": [ + "思维回路全速运转...", + "Token 们奋力排列中...", + "矩阵乘法进行中...", + "Transformer 全力变形...", + "Embeddings 寻找彼此...", + "最后一次梯度下降...", + "注意力机制已锁定...", + "思维发动机轰鸣启动...", + "思维路线图展开中...", + "激活函数全员上岗...", + "推理链跑起来了...", + "十四亿参数找答案...", + "上下文窗口挤一挤...", + "最合适的词正在出现...", + "正在 Debug 思维链...", + "推导链节节延伸...", + "在语言的岔路口选方向...", + "思路全线开动...", + "向量空间里迷了路...", + "量子态还没塌缩...", + "正在过滤噪音...", + "从千万想法里提炼精华...", + "灵感之流汇聚成河...", + "数百万次思考之后...", + "所有脑力为你服务...", + "思绪整装,即将出发...", + "给神经网络倒咖啡...", + "模型正在思考人生...", + "神经元群情激昂...", + "反向传播感悟中...", + "正在召开内部评审会...", + "多个思路正在 PK...", + "神经元:已就位...", + "神经网络的午夜加班...", + "正在假装镇定地高速运转...", + "暗中使力中...", + "绞尽脑汁为你服务...", + "正在对话宇宙...", + "向宇宙借创意...", + "答案在路上了...", + "拼图最后一块到位...", + "思路合流中...", + "逻辑线头找到了...", + "差不多了,收尾中...", + "还差一点点...", + "就差临门一脚...", + "搞定了百分之九十九...", + "最终赢家即将揭晓...", + "答案正在着陆跑道...", + "最后校对一遍...", + "正在打磨最后措辞...", + "正在精简表达...", + "填充最后几块拼图...", + "高质量输出即将发射...", + "系好安全带,即将着陆...", + "即将顿悟...", + "答案就在嘴边...", + "翻阅无数可能中...", + "召唤创意,稍候...", + "脑洞已打开...", + "在十万八千条思路里取最优...", + "把你的问题投影到高维空间...", + "好问题!让我选一个薛定谔的答案...", + "在知识图谱里找路...", + "知识正在快速结晶...", + "把抽象想法具体化...", + "思维导图疯狂生长...", + "在概念森林砍出一条路...", + "想法从潜空间投影...", + "将混乱整理成诗...", + "折叠所有可能成一答...", + "举一反三进行中...", + "联想力爆棚中...", + "正在破解难题密码...", + "智慧的光正在聚焦...", + "字斟句酌,只给精华...", + "想想,再想想...", + "已经快了(真的)...", + "快了!(不是在敷衍)", + "这道题,我会!", + "收到!全力处理中...", + "就在这里,别急...", + "全情投入,心无旁骛...", + "私下已想了七八种方案...", + "比想象中更努力...", + "认真的,从没这么认真过...", + "比光速还快(误)...", + "才华正在集中爆发...", + "正在憋大招...", + "超强输出,即将到来...", + "最强答案正在成形...", + "把散落的灵感拼起来...", + "用知识蒸馏一杯好茶...", + "灵感在深处发酵...", + "知识浪潮涌向输出端...", + "脑图正在扩展中...", + "思维潮水汇聚中...", + "思维火焰正旺...", + "知识在快速结晶...", + "脑内灯泡闪烁中...", + "思路打结,解开中...", + "思维信号穿梭神经丛...", + "秒速百公里地思考...", + "知识储备调用中...", + "知识能量正在充能...", + "智慧内核全速转...", + "脑子已经全力转起来了...", + "正在为你量身定制...", + "把最好的留到最后...", + "用心对待每一个字...", + "全神贯注于你的问题...", + "注意力全给你了...", + "这一刻,全给你...", + "正在起草一份诚意回答...", + "灵感正在悄悄聚拢...", + "倾尽所有给你答案...", + "绝不摆烂,全力以赴...", + "努力营业中...", + "混沌转逻辑中...", + "思考齿轮咬合中...", + "解码深邃想法中...", + "合并所有子思路...", + "抢救迷失的语义...", + "拆解成千个小问题...", + "千个念头取最好...", + "把想法和文字对齐...", + "整理千头万绪中...", + "确认答案合理性中...", + "检查一遍,再检查...", + "把模糊变清晰中...", + "思维正在对焦...", + "多线程思维全力汇总...", + "把逻辑再拧紧一点...", + "让逻辑自己说话...", + "在语言丛林里寻宝...", + "全速冲刺,答案在望...", + "思维扩容,装下更多...", + "为回答精心调味...", + "给逻辑链上润滑油...", + "把回答装进包裹打结中...", + "逻辑拼图已完成...", + "有条不紊,稳步推进...", + "万事就绪,答案出发...", + "脑波达到最高频...", + "思路刚打通,马上来...", + "解开语言的最后一层...", + "用推理缝合碎片...", + "召开紧急大脑会议...", + "整理出最佳路径...", + "正在审阅自己的思维...", + "正在进行最后冲刺...", + "努力中,绝不放弃...", + "正在做些不可思议的事...", + "思考容量拉满...", + "我在,我想,我输出...", + "后台小剧场精彩上演...", + "正在奋笔疾书...", + "在参数空间漫步...", + "正在将想法变为现实...", + "知识全力驰援中...", + "删删改改中...", + "像素精灵正在组字...", + "比特们正在排队...", + "比特流已启动...", + "思维启动加速度...", + "正在给回答注入灵魂...", + "准备好了吗?快了...", + "就差最后一步...", + "输出即将解冻...", + "神经丛林探险中...", + "答案雏形已在手...", + "思路正在合流...", + "尽力而为,必有所获...", + "正在快乐工作中...", + "大脑正在全速燃烧...", + "答案即将成形...", + "思维的潮水汇聚中...", + "正在把复杂变简单...", + "脑中千个念头筛选中...", + "正在为你开天辟地...", + "知识海洋里捞最优解...", + "万亿可能取其一...", + "答案正在破壳而出...", + "大脑高速公路全线畅通...", + "最佳路径已锁定...", + "思维齐步走,答案来了...", + "大招正在蓄力中...", + "答案即将横空出世..." + ] +} diff --git a/src/web-ui/src/locales/zh-CN/settings.json b/src/web-ui/src/locales/zh-CN/settings.json index 8aa42ec93..7f5d8a7ec 100644 --- a/src/web-ui/src/locales/zh-CN/settings.json +++ b/src/web-ui/src/locales/zh-CN/settings.json @@ -7,6 +7,14 @@ "searchNoResults": "没有匹配的配置", "searchClear": "清除搜索", "beta": "Beta", + "searchAliases": { + "archivedSessions": ["归档", "会话"], + "keyboard": ["快捷键", "键位"], + "sessionPersonalization": ["伙伴", "个性化"], + "sessionPermissions": ["权限"], + "quickActions": ["快捷动作", "提交"], + "review": ["审核", "代码审核"] + }, "tabDescriptions": { "basics": "日志、终端 Shell、通知与开机启动。", "appearance": "语言、主题与界面字体大小。", diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 0adf1a3f9..b41615e29 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -858,6 +858,7 @@ "fixPreview": "修復預覽", "gitDiff": "Git Diff", "gitSettings": "Git 設置", + "pullRequests": "Pull Requests", "taskPlanner": "任務規劃器", "fileBrowser": "文件瀏覽器", "editor": "編輯器", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 926ac937b..eca8be933 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -44,6 +44,7 @@ "loadingDescription": "正在準備該會話的歷史內容。", "failedTitle": "歷史會話載入失敗", "failedDescription": "可以重新載入已儲存的會話內容。", + "remoteSessionMissing": "在後端找不到該會話資料。若剛重新連接過 SSH 遠端工作區,請關閉並重新打開該遠端項目,或新建會話後再試。", "retry": "重試" }, "usage": { diff --git a/src/web-ui/src/locales/zh-TW/flow-chat/processing-hints.json b/src/web-ui/src/locales/zh-TW/flow-chat/processing-hints.json new file mode 100644 index 000000000..13cee1a38 --- /dev/null +++ b/src/web-ui/src/locales/zh-TW/flow-chat/processing-hints.json @@ -0,0 +1,192 @@ +{ + "items": [ + "思维回路全速运转...", + "Token 们奋力排列中...", + "矩阵乘法进行中...", + "Transformer 全力变形...", + "Embeddings 寻找彼此...", + "最后一次梯度下降...", + "注意力机制已锁定...", + "思维发动机轰鸣启动...", + "思维路线图展开中...", + "激活函数全员上岗...", + "推理链跑起来了...", + "十四亿参数找答案...", + "上下文窗口挤一挤...", + "最合适的词正在出现...", + "正在 Debug 思维链...", + "推导链节节延伸...", + "在语言的岔路口选方向...", + "思路全线开动...", + "向量空间里迷了路...", + "量子态还没塌缩...", + "正在过滤噪音...", + "从千万想法里提炼精华...", + "灵感之流汇聚成河...", + "数百万次思考之后...", + "所有脑力为你服务...", + "思绪整装,即将出发...", + "给神经网络倒咖啡...", + "模型正在思考人生...", + "神经元群情激昂...", + "反向传播感悟中...", + "正在召开内部评审会...", + "多个思路正在 PK...", + "神经元:已就位...", + "神经网络的午夜加班...", + "正在假装镇定地高速运转...", + "暗中使力中...", + "绞尽脑汁为你服务...", + "正在对话宇宙...", + "向宇宙借创意...", + "答案在路上了...", + "拼图最后一块到位...", + "思路合流中...", + "逻辑线头找到了...", + "差不多了,收尾中...", + "还差一点点...", + "就差临门一脚...", + "搞定了百分之九十九...", + "最终赢家即将揭晓...", + "答案正在着陆跑道...", + "最后校对一遍...", + "正在打磨最后措辞...", + "正在精简表达...", + "填充最后几块拼图...", + "高质量输出即将发射...", + "系好安全带,即将着陆...", + "即将顿悟...", + "答案就在嘴边...", + "翻阅无数可能中...", + "召唤创意,稍候...", + "脑洞已打开...", + "在十万八千条思路里取最优...", + "把你的问题投影到高维空间...", + "好问题!让我选一个薛定谔的答案...", + "在知识图谱里找路...", + "知识正在快速结晶...", + "把抽象想法具体化...", + "思维导图疯狂生长...", + "在概念森林砍出一条路...", + "想法从潜空间投影...", + "将混乱整理成诗...", + "折叠所有可能成一答...", + "举一反三进行中...", + "联想力爆棚中...", + "正在破解难题密码...", + "智慧的光正在聚焦...", + "字斟句酌,只给精华...", + "想想,再想想...", + "已经快了(真的)...", + "快了!(不是在敷衍)", + "这道题,我会!", + "收到!全力处理中...", + "就在这里,别急...", + "全情投入,心无旁骛...", + "私下已想了七八种方案...", + "比想象中更努力...", + "认真的,从没这么认真过...", + "比光速还快(误)...", + "才华正在集中爆发...", + "正在憋大招...", + "超强输出,即将到来...", + "最强答案正在成形...", + "把散落的灵感拼起来...", + "用知识蒸馏一杯好茶...", + "灵感在深处发酵...", + "知识浪潮涌向输出端...", + "脑图正在扩展中...", + "思维潮水汇聚中...", + "思维火焰正旺...", + "知识在快速结晶...", + "脑内灯泡闪烁中...", + "思路打结,解开中...", + "思维信号穿梭神经丛...", + "秒速百公里地思考...", + "知识储备调用中...", + "知识能量正在充能...", + "智慧内核全速转...", + "脑子已经全力转起来了...", + "正在为你量身定制...", + "把最好的留到最后...", + "用心对待每一个字...", + "全神贯注于你的问题...", + "注意力全给你了...", + "这一刻,全给你...", + "正在起草一份诚意回答...", + "灵感正在悄悄聚拢...", + "倾尽所有给你答案...", + "绝不摆烂,全力以赴...", + "努力营业中...", + "混沌转逻辑中...", + "思考齿轮咬合中...", + "解码深邃想法中...", + "合并所有子思路...", + "抢救迷失的语义...", + "拆解成千个小问题...", + "千个念头取最好...", + "把想法和文字对齐...", + "整理千头万绪中...", + "确认答案合理性中...", + "检查一遍,再检查...", + "把模糊变清晰中...", + "思维正在对焦...", + "多线程思维全力汇总...", + "把逻辑再拧紧一点...", + "让逻辑自己说话...", + "在语言丛林里寻宝...", + "全速冲刺,答案在望...", + "思维扩容,装下更多...", + "为回答精心调味...", + "给逻辑链上润滑油...", + "把回答装进包裹打结中...", + "逻辑拼图已完成...", + "有条不紊,稳步推进...", + "万事就绪,答案出发...", + "脑波达到最高频...", + "思路刚打通,马上来...", + "解开语言的最后一层...", + "用推理缝合碎片...", + "召开紧急大脑会议...", + "整理出最佳路径...", + "正在审阅自己的思维...", + "正在进行最后冲刺...", + "努力中,绝不放弃...", + "正在做些不可思议的事...", + "思考容量拉满...", + "我在,我想,我输出...", + "后台小剧场精彩上演...", + "正在奋笔疾书...", + "在参数空间漫步...", + "正在将想法变为现实...", + "知识全力驰援中...", + "删删改改中...", + "像素精灵正在组字...", + "比特们正在排队...", + "比特流已启动...", + "思维启动加速度...", + "正在给回答注入灵魂...", + "准备好了吗?快了...", + "就差最后一步...", + "输出即将解冻...", + "神经丛林探险中...", + "答案雏形已在手...", + "思路正在合流...", + "尽力而为,必有所获...", + "正在快乐工作中...", + "大脑正在全速燃烧...", + "答案即将成形...", + "思维的潮水汇聚中...", + "正在把复杂变简单...", + "脑中千个念头筛选中...", + "正在为你开天辟地...", + "知识海洋里捞最优解...", + "万亿可能取其一...", + "答案正在破壳而出...", + "大脑高速公路全线畅通...", + "最佳路径已锁定...", + "思维齐步走,答案来了...", + "大招正在蓄力中...", + "答案即将横空出世..." + ] +} diff --git a/src/web-ui/src/locales/zh-TW/settings.json b/src/web-ui/src/locales/zh-TW/settings.json index f435f6829..0a960ab9b 100644 --- a/src/web-ui/src/locales/zh-TW/settings.json +++ b/src/web-ui/src/locales/zh-TW/settings.json @@ -7,6 +7,14 @@ "searchNoResults": "沒有匹配的配置", "searchClear": "清除搜索", "beta": "Beta", + "searchAliases": { + "archivedSessions": ["歸檔", "會話"], + "keyboard": ["快捷鍵", "鍵位"], + "sessionPersonalization": ["夥伴", "個性化"], + "sessionPermissions": ["權限"], + "quickActions": ["快捷動作", "提交"], + "review": ["審核", "程式碼審核"] + }, "tabDescriptions": { "basics": "日誌、終端 Shell、通知與開機啟動。", "appearance": "語言、主題與介面字體大小。", diff --git a/src/web-ui/src/shared/ai-errors/aiErrorPresenter.ts b/src/web-ui/src/shared/ai-errors/aiErrorPresenter.ts index 9c410a64d..c7afba066 100644 --- a/src/web-ui/src/shared/ai-errors/aiErrorPresenter.ts +++ b/src/web-ui/src/shared/ai-errors/aiErrorPresenter.ts @@ -1,3 +1,5 @@ +import providerMessageMatchers from './providerMessageMatchers.json'; + export type AiErrorCategory = | 'network' | 'auth' @@ -209,15 +211,13 @@ function normalizeCategory( 'not enough balance', 'exceeded_current_quota', 'exceeded current quota', - '余额不足', - '无可用资源包', - '账户已欠费', + ...providerMessageMatchers.providerQuota, ]) ) { return 'provider_quota'; } - if (includesAny(`${code} ${message}`, ['billing', 'membership expired', 'subscription expired', '套餐已到期', '1309'])) { + if (includesAny(`${code} ${message}`, ['billing', 'membership expired', 'subscription expired', ...providerMessageMatchers.providerBilling, '1309'])) { return 'provider_billing'; } diff --git a/src/web-ui/src/shared/ai-errors/providerMessageMatchers.json b/src/web-ui/src/shared/ai-errors/providerMessageMatchers.json new file mode 100644 index 000000000..347027f2b --- /dev/null +++ b/src/web-ui/src/shared/ai-errors/providerMessageMatchers.json @@ -0,0 +1,10 @@ +{ + "providerQuota": [ + "余额不足", + "无可用资源包", + "账户已欠费" + ], + "providerBilling": [ + "套餐已到期" + ] +} diff --git a/src/web-ui/src/shared/utils/tabUtils.ts b/src/web-ui/src/shared/utils/tabUtils.ts index 3322d8c8f..ada6e540d 100644 --- a/src/web-ui/src/shared/utils/tabUtils.ts +++ b/src/web-ui/src/shared/utils/tabUtils.ts @@ -277,7 +277,7 @@ export function createConfigCenterTab( export function createReviewPlatformTab(workspacePath?: string): void { const detail = { type: 'review-platform', - title: i18nService.getT()('common:tabs.pullRequests', { defaultValue: 'Pull Requests' }), + title: i18nService.getT()('common:tabs.pullRequests'), data: { workspacePath }, metadata: { workspacePath, diff --git a/src/web-ui/src/tools/editor/components/CodeEditor.tsx b/src/web-ui/src/tools/editor/components/CodeEditor.tsx index 445d9392c..2e83b49f2 100644 --- a/src/web-ui/src/tools/editor/components/CodeEditor.tsx +++ b/src/web-ui/src/tools/editor/components/CodeEditor.tsx @@ -42,6 +42,7 @@ import { import { useI18n } from '@/infrastructure/i18n'; import { EditorBreadcrumb } from './EditorBreadcrumb'; import { EditorStatusBar } from './EditorStatusBar'; +import largeFileExpansionLabels from './largeFileExpansionLabels.json'; const log = createLogger('CodeEditor'); import { @@ -100,7 +101,7 @@ const LARGE_FILE_SIZE_THRESHOLD_BYTES = 1 * 1024 * 1024; // 1MB const LARGE_FILE_MAX_LINE_LENGTH = 20000; const LARGE_FILE_RENDER_LINE_LIMIT = 10000; const LARGE_FILE_MAX_TOKENIZATION_LINE_LENGTH = 2000; -const LARGE_FILE_EXPANSION_LABELS = ['show more', '显示更多', '展开更多']; +const LARGE_FILE_EXPANSION_LABELS = largeFileExpansionLabels; /** Poll disk metadata for open file; only while tab is active (see isActiveTab). */ const FILE_SYNC_POLL_INTERVAL_MS = 1000; diff --git a/src/web-ui/src/tools/editor/components/largeFileExpansionLabels.json b/src/web-ui/src/tools/editor/components/largeFileExpansionLabels.json new file mode 100644 index 000000000..575bc1846 --- /dev/null +++ b/src/web-ui/src/tools/editor/components/largeFileExpansionLabels.json @@ -0,0 +1,5 @@ +[ + "show more", + "显示更多", + "展开更多" +]