diff --git a/BitFun-Installer/src/i18n/locales/en.json b/BitFun-Installer/src/i18n/locales/en.json index 14db2afbf..e87f77d64 100644 --- a/BitFun-Installer/src/i18n/locales/en.json +++ b/BitFun-Installer/src/i18n/locales/en.json @@ -1,32 +1,23 @@ { - "lang": { - "title": "Language", - "subtitle": "Select your preferred language", - "continue": "Continue" - }, "errors": { "install": { "failed": "Installation failed" }, "installPath": { - "notAbsolute": "The installation path must be an absolute path (for example C:\\…).", + "notAbsolute": "The installation path must be an absolute path (for example a folder on a local drive).", "filesystemRoot": "Cannot install to a drive root. Choose a folder inside the drive.", "pathNotDirectory": "The selected path exists but is not a folder.", "directoryMustBeEmptyOrBitfun": "The installation folder must be empty, or already contain a BitFun installation.", "inspectDirectoryFailed": "Could not read the installation folder. Check permissions and try again.", "directoryNotWritable": "The installation folder is not writable. Choose another location or run the installer as administrator (see below).", "parentNotWritable": "The parent folder is not writable. System folders such as Program Files often require administrator rights (see below).", - "adminHint": "To install under protected locations (for example C:\\Program Files), close this installer, right‑click the installer executable, choose \"Run as administrator\", then try again. Alternatively install under your user profile, for example %LOCALAPPDATA%\\Programs, which does not require elevation." + "adminHint": "To install under protected locations (for example Program Files), close this installer, right-click the installer executable, choose \"Run as administrator\", then try again. Alternatively install under your user profile, for example %LOCALAPPDATA%\\Programs, which does not require elevation." } }, "options": { "title": "Options", "subtitle": "Review install location and preferences", - "quickSetup": "Quick setup", - "appLanguage": "App language", "theme": "Theme", - "themeDark": "Dark", - "themeSlate": "Slate", "changeLanguage": "Change language", "pathLabel": "Installation Path", "pathPlaceholder": "Select directory...", @@ -37,13 +28,10 @@ "optionsLabel": "Options", "desktopShortcut": "Create desktop shortcut", "startMenu": "Add to Start Menu", - "contextMenu": "Add right-click context menu", - "addToPath": "Add to system PATH", "launchAfterInstall": "Launch BitFun after setup", "back": "Back", "install": "Install", "installing": "Preparing…", - "nextModel": "Next: Configure model", "existingInstallTitle": "Existing BitFun installation detected", "existingInstallVersion": "Installed version: {{version}}", "existingInstallLocation": "Install location: {{path}}", @@ -54,32 +42,17 @@ "model": { "title": "Model", "subtitle": "Installation complete. Continue with model and theme setup", - "installDone": "Installation complete", "provider": "Provider", "config": "Connection", "modelName": "Model name (e.g. deepseek-v4-flash)", - "apiKey": "API key", - "back": "Back", "skip": "Skip for now", "nextTheme": "Next: Theme", "description": "Configure and manage AI model providers", "providerLabel": "Select Model Provider", "selectProvider": "or select a preset provider", "customProvider": "Custom Configuration", - "getApiKey": "Get API Key", "modelNamePlaceholder": "Enter model name...", "baseUrlPlaceholder": "e.g., https://open.bigmodel.cn/api/paas/v4/chat/completions", - "customRequestBodyPlaceholder": "Example:\n{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", - "jsonValid": "JSON format is valid", - "jsonInvalid": "Invalid JSON format, please check syntax", - "skipSslVerify": "Skip SSL Certificate Verification", - "customHeadersModeMerge": "Merge", - "customHeadersModeReplace": "Replace", - "addHeader": "Add Field", - "headerKey": "key", - "headerValue": "value", - "advancedShow": "Show advanced settings", - "advancedHide": "Hide advanced settings", "providers": { "openbitfun": { "name": "OpenBitFun", @@ -149,13 +122,10 @@ } }, "modelNameSelectPlaceholder": "Select model", - "customModel": "Press Enter to use", "testConnection": "Test Connection", "testing": "Testing...", "testSuccess": "Test successful", "testFailed": "Test failed", - "modelSearchPlaceholder": "Search or enter model name...", - "modelNoResults": "No matching models", "fillApiKeyBeforeFetch": "Enter the API key before fetching models", "fetchingModels": "Fetching model list...", "fetchFailedFallback": "Failed to fetch model list, fell back to common preset models", @@ -169,7 +139,6 @@ "provider": "Request Format", "providerPlaceholder": "Select request format", "modelSelection": "Select Models", - "modelName": "Model Name", "resolvedUrlLabel": "Request URL: " }, "formats": { @@ -183,12 +152,10 @@ }, "progress": { "title": "Installing", - "installing": "Installing...", "prepare": "Preparing", "extract": "Extracting Files", "registry": "Registering Application", "shortcuts": "Creating Shortcuts", - "contextMenu": "Context Menu", "path": "Updating PATH", "config": "Applying startup preferences", "complete": "Finishing Up", @@ -200,7 +167,6 @@ "title": "Theme & Launch", "subtitle": "Choose your startup theme, then launch", "followSystem": "Match system", - "skip": "Skip theme and launch", "themeNames": { "bitfun-dark": "Dark", "bitfun-light": "Light", @@ -213,18 +179,12 @@ } }, "complete": { - "title": "Complete", - "heading": "Installation Complete", - "ready": "BitFun is ready to use.", - "autoLaunch": " It will launch automatically.", - "launchFinish": "Launch BitFun", "finish": "Finish" }, "uninstall": { "title": "Uninstall BitFun", "subtitle": "This removes BitFun and related integrations (shortcuts, context menu, PATH).", "installPath": "Install path", - "inlineHint": "Will remove integration and install directory", "pathUnknown": "Install path not detected", "confirm": "Start uninstall", "uninstalling": "Uninstalling...", diff --git a/BitFun-Installer/src/i18n/locales/zh-TW.json b/BitFun-Installer/src/i18n/locales/zh-TW.json index fd4aa0897..504133ca2 100644 --- a/BitFun-Installer/src/i18n/locales/zh-TW.json +++ b/BitFun-Installer/src/i18n/locales/zh-TW.json @@ -1,32 +1,23 @@ { - "lang": { - "title": "語言", - "subtitle": "選擇您的首選語言", - "continue": "繼續" - }, "errors": { "install": { "failed": "安裝失敗" }, "installPath": { - "notAbsolute": "安裝路徑必須是絕對路徑(例如 C:\\…)。", + "notAbsolute": "安裝路徑必須是絕對路徑。", "filesystemRoot": "不能安裝到磁盤根目錄,請選擇磁盤下的某個文件夾。", "pathNotDirectory": "所選路徑已存在但不是文件夾。", "directoryMustBeEmptyOrBitfun": "安裝目錄必須為空,或已包含 BitFun 安裝。", "inspectDirectoryFailed": "無法讀取安裝目錄,請檢查權限後重試。", "directoryNotWritable": "安裝目錄不可寫入。請更換路徑,或以管理員身份運行安裝器(見下方說明)。", "parentNotWritable": "上級目錄不可寫入。系統目錄(如 Program Files)通常需要管理員權限(見下方說明)。", - "adminHint": "若需安裝到受保護位置(例如 C:\\Program Files),請關閉本安裝器,在安裝程序上右鍵選擇「以管理員身份運行」後重新安裝。也可安裝到當前用戶目錄(例如 %LOCALAPPDATA%\\Programs),一般無需管理員權限。" + "adminHint": "若需安裝到受保護位置(例如 Program Files),請關閉本安裝器,在安裝程序上右鍵選擇「以管理員身份運行」後重新安裝。也可安裝到當前用戶目錄(例如 %LOCALAPPDATA%\\Programs),一般無需管理員權限。" } }, "options": { "title": "選項", "subtitle": "確認安裝位置與安裝偏好", - "quickSetup": "快速設置", - "appLanguage": "應用語言", "theme": "主題", - "themeDark": "暗色", - "themeSlate": "石板灰", "changeLanguage": "切換語言", "pathLabel": "安裝路徑", "pathPlaceholder": "選擇目錄...", @@ -37,13 +28,10 @@ "optionsLabel": "安裝選項", "desktopShortcut": "創建桌面快捷方式", "startMenu": "添加到開始菜單", - "contextMenu": "添加右鍵菜單", - "addToPath": "添加到系統 PATH", "launchAfterInstall": "安裝後啟動 BitFun", "back": "返回", "install": "安裝", "installing": "準備中…", - "nextModel": "下一步:配置模型", "existingInstallTitle": "檢測到本機已安裝 BitFun", "existingInstallVersion": "已安裝版本:{{version}}", "existingInstallLocation": "安裝位置:{{path}}", @@ -54,32 +42,17 @@ "model": { "title": "模型", "subtitle": "安裝完成,繼續配置模型與主題", - "installDone": "安裝完成", "provider": "服務商", "config": "連接信息", "modelName": "模型名稱(如 deepseek-v4-flash)", - "apiKey": "API Key", - "back": "返回", "skip": "稍後配置", "nextTheme": "下一步:主題", "description": "配置和管理 AI 模型提供商", "providerLabel": "選擇模型提供商", "selectProvider": "或選擇預設提供商", "customProvider": "自定義配置", - "getApiKey": "獲取 API Key", "modelNamePlaceholder": "輸入自定義模型名稱...", "baseUrlPlaceholder": "示例:https://open.bigmodel.cn/api/paas/v4/chat/completions", - "customRequestBodyPlaceholder": "{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", - "jsonValid": "JSON格式有效", - "jsonInvalid": "JSON格式錯誤,請檢查語法", - "skipSslVerify": "跳過SSL證書驗證", - "customHeadersModeMerge": "合併覆蓋", - "customHeadersModeReplace": "完全替換", - "addHeader": "添加字段", - "headerKey": "key", - "headerValue": "value", - "advancedShow": "顯示進階設定", - "advancedHide": "隱藏進階設定", "providers": { "openbitfun": { "name": "OpenBitFun", @@ -149,13 +122,10 @@ } }, "modelNameSelectPlaceholder": "選擇模型", - "customModel": "按 Enter 使用", "testConnection": "測試連接", "testing": "測試中...", "testSuccess": "測試成功", "testFailed": "測試失敗", - "modelSearchPlaceholder": "搜索或輸入模型名稱...", - "modelNoResults": "沒有匹配的模型", "fillApiKeyBeforeFetch": "請先填寫 API Key 再獲取模型列表", "fetchingModels": "正在拉取模型列表...", "fetchFailedFallback": "拉取模型列表失敗,已回退到常用預設模型", @@ -169,7 +139,6 @@ "provider": "請求格式", "providerPlaceholder": "選擇請求格式", "modelSelection": "選擇模型", - "modelName": "模型名稱", "resolvedUrlLabel": "實際請求地址:" }, "formats": { @@ -183,12 +152,10 @@ }, "progress": { "title": "安裝中", - "installing": "正在安裝...", "prepare": "準備中", "extract": "正在解壓文件", "registry": "正在註冊應用", "shortcuts": "正在創建快捷方式", - "contextMenu": "右鍵菜單", "path": "正在更新 PATH", "config": "正在應用啟動偏好設置", "complete": "即將完成", @@ -200,7 +167,6 @@ "title": "主題與啟動", "subtitle": "選擇首次啟動主題,然後開始使用", "followSystem": "跟隨系統", - "skip": "跳過主題並啟動", "themeNames": { "bitfun-dark": "暗色", "bitfun-light": "亮色", @@ -213,18 +179,12 @@ } }, "complete": { - "title": "完成", - "heading": "安裝完成", - "ready": "BitFun 已準備就緒。", - "autoLaunch": "將自動啟動應用。", - "launchFinish": "啟動 BitFun", "finish": "完成" }, "uninstall": { "title": "卸載 BitFun", "subtitle": "將移除 BitFun 及其集成項(快捷方式、右鍵菜單、PATH)。", "installPath": "安裝目錄", - "inlineHint": "將清理集成與安裝目錄", "pathUnknown": "未檢測到安裝目錄", "confirm": "開始卸載", "uninstalling": "正在卸載...", diff --git a/BitFun-Installer/src/i18n/locales/zh.json b/BitFun-Installer/src/i18n/locales/zh.json index 5f835a76e..99440a730 100644 --- a/BitFun-Installer/src/i18n/locales/zh.json +++ b/BitFun-Installer/src/i18n/locales/zh.json @@ -1,32 +1,23 @@ { - "lang": { - "title": "语言", - "subtitle": "选择您的首选语言", - "continue": "继续" - }, "errors": { "install": { "failed": "安装失败" }, "installPath": { - "notAbsolute": "安装路径必须是绝对路径(例如 C:\\…)。", + "notAbsolute": "安装路径必须是绝对路径。", "filesystemRoot": "不能安装到磁盘根目录,请选择磁盘下的某个文件夹。", "pathNotDirectory": "所选路径已存在但不是文件夹。", "directoryMustBeEmptyOrBitfun": "安装目录必须为空,或已包含 BitFun 安装。", "inspectDirectoryFailed": "无法读取安装目录,请检查权限后重试。", "directoryNotWritable": "安装目录不可写入。请更换路径,或以管理员身份运行安装器(见下方说明)。", "parentNotWritable": "上级目录不可写入。系统目录(如 Program Files)通常需要管理员权限(见下方说明)。", - "adminHint": "若需安装到受保护位置(例如 C:\\Program Files),请关闭本安装器,在安装程序上右键选择「以管理员身份运行」后重新安装。也可安装到当前用户目录(例如 %LOCALAPPDATA%\\Programs),一般无需管理员权限。" + "adminHint": "若需安装到受保护位置(例如 Program Files),请关闭本安装器,在安装程序上右键选择「以管理员身份运行」后重新安装。也可安装到当前用户目录(例如 %LOCALAPPDATA%\\Programs),一般无需管理员权限。" } }, "options": { "title": "选项", "subtitle": "确认安装位置与安装偏好", - "quickSetup": "快速设置", - "appLanguage": "应用语言", "theme": "主题", - "themeDark": "暗色", - "themeSlate": "石板灰", "changeLanguage": "切换语言", "pathLabel": "安装路径", "pathPlaceholder": "选择目录...", @@ -37,13 +28,10 @@ "optionsLabel": "安装选项", "desktopShortcut": "创建桌面快捷方式", "startMenu": "添加到开始菜单", - "contextMenu": "添加右键菜单", - "addToPath": "添加到系统 PATH", "launchAfterInstall": "安装后启动 BitFun", "back": "返回", "install": "安装", "installing": "准备中…", - "nextModel": "下一步:配置模型", "existingInstallTitle": "检测到本机已安装 BitFun", "existingInstallVersion": "已安装版本:{{version}}", "existingInstallLocation": "安装位置:{{path}}", @@ -54,32 +42,17 @@ "model": { "title": "模型", "subtitle": "安装完成,继续配置模型与主题", - "installDone": "安装完成", "provider": "服务商", "config": "连接信息", "modelName": "模型名称(如 deepseek-v4-flash)", - "apiKey": "API Key", - "back": "返回", "skip": "稍后配置", "nextTheme": "下一步:主题", "description": "配置和管理 AI 模型提供商", "providerLabel": "选择模型提供商", "selectProvider": "或选择预设提供商", "customProvider": "自定义配置", - "getApiKey": "获取 API Key", "modelNamePlaceholder": "输入自定义模型名称...", "baseUrlPlaceholder": "示例:https://open.bigmodel.cn/api/paas/v4/chat/completions", - "customRequestBodyPlaceholder": "例如:\n{\n \"temperature\": 0.8,\n \"top_p\": 0.9\n}", - "jsonValid": "JSON格式有效", - "jsonInvalid": "JSON格式错误,请检查语法", - "skipSslVerify": "跳过SSL证书验证", - "customHeadersModeMerge": "合并", - "customHeadersModeReplace": "替换", - "addHeader": "添加字段", - "headerKey": "key", - "headerValue": "value", - "advancedShow": "Show advanced settings", - "advancedHide": "Hide advanced settings", "providers": { "openbitfun": { "name": "OpenBitFun", @@ -149,13 +122,10 @@ } }, "modelNameSelectPlaceholder": "选择模型", - "customModel": "按 Enter 使用", "testConnection": "测试连接", "testing": "测试中...", "testSuccess": "测试成功", "testFailed": "测试失败", - "modelSearchPlaceholder": "搜索或输入模型名称...", - "modelNoResults": "没有匹配的模型", "fillApiKeyBeforeFetch": "请先填写 API Key 再获取模型列表", "fetchingModels": "正在拉取模型列表...", "fetchFailedFallback": "拉取模型列表失败,已回退到常用预设模型", @@ -169,7 +139,6 @@ "provider": "请求格式", "providerPlaceholder": "选择请求格式", "modelSelection": "选择模型", - "modelName": "模型名称", "resolvedUrlLabel": "实际请求地址:" }, "formats": { @@ -183,12 +152,10 @@ }, "progress": { "title": "安装中", - "installing": "正在安装...", "prepare": "准备中", "extract": "正在解压文件", "registry": "正在注册应用", "shortcuts": "正在创建快捷方式", - "contextMenu": "右键菜单", "path": "正在更新 PATH", "config": "正在应用启动偏好设置", "complete": "即将完成", @@ -200,7 +167,6 @@ "title": "主题与启动", "subtitle": "选择首次启动主题,然后开始使用", "followSystem": "跟随系统", - "skip": "跳过主题并启动", "themeNames": { "bitfun-dark": "暗色", "bitfun-light": "亮色", @@ -213,18 +179,12 @@ } }, "complete": { - "title": "完成", - "heading": "安装完成", - "ready": "BitFun 已准备就绪。", - "autoLaunch": "将自动启动应用。", - "launchFinish": "启动 BitFun", "finish": "完成" }, "uninstall": { "title": "卸载 BitFun", "subtitle": "将移除 BitFun 及其集成项(快捷方式、右键菜单、PATH)。", "installPath": "安装目录", - "inlineHint": "将清理集成与安装目录", "pathUnknown": "未检测到安装目录", "confirm": "开始卸载", "uninstalling": "正在卸载...", diff --git a/docs/architecture/i18n.md b/docs/architecture/i18n.md index db4284a51..09a17b5ec 100644 --- a/docs/architecture/i18n.md +++ b/docs/architecture/i18n.md @@ -48,6 +48,22 @@ The generated contracts expose these terms as a small `shared.*` runtime namespace for each surface; this keeps concept labels aligned without importing another surface's full resource catalog. +`shared.*` is the canonical source for stable concept names, not a replacement +for product-surface copy. Treat resource ownership as three layers: + +1. `shared.*` for product names, feature names, mode names, common tool names, + connection method labels, and shared status nouns. +2. Surface-level common namespaces for short UI atoms owned by that surface, + such as generic actions, empty-state primitives, and shell labels. +3. Feature or route namespaces for workflow sentences, page titles, field + labels, validation text, toasts, and copy whose wording depends on a surface + or scene. + +Specific copy wins over common copy, and common copy wins over shared terms. Do +not configure `shared` as an implicit global fallback namespace. When a feature +needs a shared label, call the shared key directly for standalone labels or pass +the shared value as an interpolation argument for composed sentences. + Surface resources stay in their existing owners: - Web UI: `src/web-ui/src/locales//**/*.json` @@ -90,6 +106,89 @@ 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. +Static Web UI translation keys are audited with namespace context. Literal +`defaultValue` fallbacks are treated as existing debt, not a sanctioned source +of copy: current findings are protected by a per-file key allowlist and should +be reduced as resource keys are added. + +## Key Grouping And Naming + +Use dot-separated semantic keys with camelCase segments inside JSON-based +resources. Namespace ownership should follow the component or feature owner: + +- Keep `common` namespaces small. They may contain generic actions, statuses, + and app-shell atoms, but not large feature workflows. +- Put page and workflow copy in the nearest feature namespace, for example + `settings/review`, `settings/ai-model`, `scenes/agents`, or `flow-chat`. +- Split large feature copy into a dedicated lazy namespace when the copy is + rarely used or owned by a distinct feature. +- Avoid abbreviations such as `f1t` or English-copy-shaped keys. Prefer + `features.connect.title` over `f1t` and `deleteConfirm` over + `areYouSureYouWantToDelete`. + +Allowed exceptions: + +- i18next plural suffixes such as `_one` and `_other`. +- Backend or protocol enum mirrors, when the key segment intentionally matches + a stable runtime value. +- Existing Fluent backend keys use kebab-case until the backend i18n resource + owner is deliberately migrated. + +Product and feature names must not be copied into every namespace by default. +Use `shared:product.name`, `shared:features.*`, `shared:modes.*`, +`shared:agents.*`, `shared:tools.*`, `shared:connectionMethods.*`, and +`shared:statuses.*` as the source of truth for standalone concept labels. +Composed copy should inject those values explicitly so product or feature +renames do not require editing many unrelated sentence keys. + +## Cleanup Rules + +Remove locale keys only after checking every release surface that can reach the +resource: + +- Static translation calls, including namespace-relative `useI18n` or + `useTranslation` calls. +- Dynamic key construction through template literals, concatenation, keyed + metadata such as `labelKey`, and fallback-key arrays. +- Backend or bridge contracts that map stable error codes or enum values into + locale keys. +- Self-contained static surfaces such as the relay homepage. + +If a key is kept only for a dynamic contract, document the mapping near the code +that constructs it. If the dynamic contract cannot be identified, do not delete +the key in the same PR as broad i18n refactoring. + +## Quality Governance + +I18n quality gates should protect correctness without forcing every local change +to run broad builds. Keep the fast path focused on contract generation, resource +audits, and the touched surface checks; let CI cover full builds and broad test +suites. + +The audit layer should distinguish these cases explicitly: + +- Confirmed unused keys: keys that no shipped surface can reach through static + calls, namespace-relative hooks, metadata fields, dynamic factories, backend + bridge mappings, or static page resources. +- Dynamic-key candidates: keys that might be reached by a documented key factory + such as enum-to-i18n mapping, installer error-code mapping, metadata + `labelKey` fields, fallback-key arrays, or template-literal construction. +- Localization quality candidates: values that pass key parity but are likely + not actually localized, such as unchanged Simplified/Traditional Chinese text, + stale English values, or copied placeholder-only strings. +- 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. + +Deletion is safe only for confirmed unused keys. Dynamic-key candidates need an +explicit allowlist or a code-level mapping comment before cleanup. Localization +quality candidates are translation work, not structural cleanup, and should not +be treated as unused keys. + +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. + ## 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 8cf30c49f..85d2e61c8 100644 --- a/docs/development/i18n.md +++ b/docs/development/i18n.md @@ -8,6 +8,8 @@ - Put cross-surface stable terms in `src/shared/i18n/resources/shared//terms.json`. - Keep surface copy in the surface resource owner. +- Do not duplicate stable product, feature, mode, tool, connection-method, or + status labels in feature namespaces when a `shared.*` term exists. - Store translation resources as readable UTF-8 text. Use escapes only for characters that must be escaped, such as control characters, quotes, backslashes, or explicit regex/codepoint ranges. @@ -39,15 +41,22 @@ cargo check --manifest-path BitFun-Installer/src-tauri/Cargo.toml # install cargo test -p bitfun-core i18n -- --nocapture # backend i18n runtime changes ``` +Do not add process-only execution plans to version control. Keep durable i18n +architecture and development rules in `docs/architecture/i18n.md` and this file; +keep rollout checklists, temporary analysis notes, and one-off cleanup plans out +of git. + ## Where To Put Strings Use shared terms only for labels that must stay semantically aligned everywhere, such as product names, feature names, mode names, core agent names, common tool names, and connection method labels. -Use surface resources for workflow text, button copy, validation messages, -empty states, toasts, page titles, and copy that is specific to a product -surface. +Use surface common namespaces for short UI atoms that are genuinely generic +inside that surface, such as `common:actions.cancel` or +`common:status.loading`. Use feature resources for workflow text, validation +messages, empty states, toasts, page titles, field labels, and copy that is +specific to a product surface or scene. Generated runtime contracts expose shared terms as `shared.*`: @@ -65,9 +74,15 @@ fallback keys ordered as `['scenes/agents:labels.deepReview', 'shared:features.deepReview']`. Do not configure `shared` as a global `fallbackNS`. +For standalone product or feature labels, call the shared key directly. For +sentences that include a product or feature name, inject the shared value as a +parameter rather than copying the name into the sentence value. Keep the +surface-specific interpolation syntax local to that surface. + ## Key Policy -Use stable, semantic keys. Do not encode current English copy in the key. +Use stable, semantic keys. JSON resources use dot-separated paths with +camelCase segments. Do not encode current English copy in the key. Good: @@ -85,6 +100,14 @@ button1 newFeatureText ``` +Allowed naming exceptions: + +- i18next plural suffixes such as `_one` and `_other`. +- Runtime enum mirrors, such as protocol values or backend error codes, when the + key is reached by a documented dynamic mapping. +- Existing backend Fluent keys, which remain kebab-case until the backend + resource owner is intentionally migrated. + Shared term keys are reserved for concepts that are expected to mean the same thing across surfaces: @@ -99,16 +122,46 @@ thing across surfaces: Do not put long sentences, layout-specific copy, or flow-specific copy in shared terms. Those belong in the surface resource owner. +Do not add abbreviated or position-based keys such as `f1t` or `button1`. +Group by meaning and owner, for example `features.connect.title`. + +Before deleting a key as unused, check static calls, namespace-relative calls, +template literals, string concatenation, metadata fields such as `labelKey`, and +backend or bridge contracts that map enum/error codes into i18n keys. Only +delete the key when no release surface can reach it. + Do not rely on literal fallback strings in component code. Add the missing key to the relevant locale file instead. Literal fallback strings make audits and -translation completeness harder to enforce. +translation completeness harder to enforce. Existing Web UI +`defaultValue: "..."` fallbacks are tracked by +`scripts/i18n-literal-fallback-baseline.json` as a temporary per-file key +allowlist. Lower that baseline whenever fallback copy is moved into locale +resources. +Dynamic fallbacks are acceptable only when the fallback is real runtime data, +such as a server-provided role name or description. + +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 +wording depends on workflow context. If a repeated value matches an existing +`shared.*` term and represents the same stable concept, prefer reading the +shared key directly or passing the shared value into a sentence as an +interpolation parameter. + +Before moving repeated values into shared terms, check all supported locales. +Identical English strings can still need different translations, and identical +Chinese strings can hide incomplete `zh-TW` localization. Treat duplicate-value +reports as review inputs, not automatic refactors. `pnpm run i18n:audit` fails on missing or extra keys in Web UI, mobile-web, and 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. +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. ## Loading Size @@ -124,9 +177,34 @@ 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. +Large namespaces should be split by workflow ownership when doing so lowers +startup or route-load cost without making key ownership ambiguous. In Web UI, +`WEB_UI_BOOTSTRAP_NAMESPACES` should stay limited to namespaces required by +synchronous module-initialization calls. Do not add a large namespace to the +bootstrap set to avoid loading it at the component boundary; call +`useI18n(namespace)` or `useTranslation(namespace)` where the UI is rendered. + For bundle-sensitive changes, compare generated asset sizes before and after the change and include the result in the PR. +## Localization Quality + +Key parity only proves that each locale has the same keys. It does not prove that +the translation is complete or regionally correct. + +For localization-sensitive changes: + +- Keep `zh-CN`, `zh-TW`, and `en-US` values readable UTF-8 and reviewable in + diffs. +- Do not copy `zh-CN` values into `zh-TW` as a substitute for Traditional + Chinese localization unless the wording is intentionally identical. +- Keep placeholders semantically equivalent across locales, not only + syntactically present. +- Prefer shared terminology for stable product concepts so later naming changes + do not require edits across many feature namespaces. +- Treat English fallback text in static HTML as first-paint or fetch-failure + resilience, not as a replacement for locale resources. + ## Language Detection Input boundaries may accept aliases from browsers, old configs, and links. diff --git a/scripts/generate-i18n-contract.mjs b/scripts/generate-i18n-contract.mjs index 2b6bc1a57..756d81f2d 100644 --- a/scripts/generate-i18n-contract.mjs +++ b/scripts/generate-i18n-contract.mjs @@ -33,6 +33,10 @@ function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); } +function normalizeGeneratedText(content) { + return String(content).replace(/\r\n/g, '\n'); +} + function readSharedTerms(contract) { return Object.fromEntries( contract.locales.map((locale) => { @@ -554,7 +558,8 @@ function main() { const nextContent = output.generate(contract, sharedTermsByLocale); if (checkOnly) { const currentContent = fs.existsSync(output.path) ? fs.readFileSync(output.path, 'utf8') : null; - if (currentContent !== nextContent) { + const currentContentForCheck = currentContent == null ? null : normalizeGeneratedText(currentContent); + if (currentContentForCheck !== normalizeGeneratedText(nextContent)) { changedFiles.push(path.relative(root, output.path).split(path.sep).join('/')); } } else { diff --git a/scripts/i18n-audit.mjs b/scripts/i18n-audit.mjs index 1b8df0353..d5b0ef84b 100644 --- a/scripts/i18n-audit.mjs +++ b/scripts/i18n-audit.mjs @@ -7,6 +7,7 @@ const require = createRequire(import.meta.url); 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 sharedTermsDir = path.join(root, 'src', 'shared', 'i18n', 'resources', 'shared'); const webLocalesDir = path.join(root, 'src', 'web-ui', 'src', 'locales'); const namespaceRegistryPath = path.join( @@ -37,6 +38,7 @@ const localeContract = readJsonFile(contractPath); let errorCount = 0; let warningCount = 0; +let auditTypeScript = null; function reportError(message) { errorCount += 1; @@ -48,6 +50,17 @@ function reportWarning(message) { console.warn(`[i18n:audit] WARN ${message}`); } +function loadTypeScriptForAudit() { + try { + return require('typescript'); + } catch (error) { + reportError( + `Failed to load TypeScript for i18n audit: ${error.message}. Run pnpm install before running pnpm run i18n:audit.`, + ); + return null; + } +} + function toPosixPath(value) { return value.split(path.sep).join('/'); } @@ -138,6 +151,10 @@ function sortedUnique(values) { return Array.from(new Set(values)).sort(); } +function isPlainObject(value) { + return value != null && typeof value === 'object' && !Array.isArray(value); +} + function extractI18nextPlaceholders(value) { const matches = String(value).matchAll(/\{\{\s*-?\s*([A-Za-z_][\w]*)\s*\}\}/g); return sortedUnique(Array.from(matches, (match) => match[1])); @@ -272,11 +289,8 @@ function flattenTsObjectEntries(ts, objectLiteral, prefix = '') { } function readMobileMessagesByLocale() { - let ts; - try { - ts = require('typescript'); - } catch (error) { - reportError(`Failed to load TypeScript for mobile-web i18n audit: ${error.message}`); + const ts = auditTypeScript; + if (!ts) { return new Map(); } @@ -797,34 +811,179 @@ function lineNumberAt(text, index) { return text.slice(0, index).split(/\r?\n/).length; } -function collectWebUiStaticTranslationKeys() { +function createAuditSourceFile(file, text) { + const ts = auditTypeScript; + return ts.createSourceFile( + file, + text, + ts.ScriptTarget.Latest, + true, + file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); +} + +function staticStringLiteral(ts, expression) { + const value = unwrapTsExpression(ts, expression); + if (!value) return null; + if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) { + return value.text; + } + return null; +} + +function staticStringArrayLiteral(ts, expression) { + const value = unwrapTsExpression(ts, expression); + if (!value || !ts.isArrayLiteralExpression(value)) return null; + + const values = []; + for (const element of value.elements) { + const literal = staticStringLiteral(ts, element); + if (!literal) return null; + values.push(literal); + } + return values.length > 0 ? values : null; +} + +function isI18nServiceGetTCall(ts, expression) { + return ( + ts.isCallExpression(expression) && + ts.isPropertyAccessExpression(expression.expression) && + expression.expression.name.text === 'getT' && + ts.isIdentifier(expression.expression.expression) && + expression.expression.expression.text === 'i18nService' + ); +} + +function isI18nServiceTCall(ts, expression) { + return ( + ts.isPropertyAccessExpression(expression) && + expression.name.text === 't' && + ts.isIdentifier(expression.expression) && + expression.expression.text === 'i18nService' + ); +} + +function isI18nHookCall(ts, expression) { + return ( + ts.isCallExpression(expression) && + ts.isIdentifier(expression.expression) && + (expression.expression.text === 'useI18n' || expression.expression.text === 'useTranslation') + ); +} + +function collectWebUiTranslationCallFacts() { + const ts = auditTypeScript; 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)}`, - }); + const sourceFile = createAuditSourceFile(file, text); + const hookNamespaceByTranslator = new Map(); + const hookNamespaceListByTranslator = new Map(); + const fullKeyTranslatorNames = new Set(); + + function rememberHookTranslator(node) { + if (!ts.isVariableDeclaration(node) || !node.initializer || !isI18nHookCall(ts, node.initializer)) { + return; + } + + const namespace = staticStringLiteral(ts, node.initializer.arguments[0]); + const namespaces = namespace ? null : staticStringArrayLiteral(ts, node.initializer.arguments[0]); + if (!namespace && !namespaces) return; + + function rememberLocalName(localName) { + if (!localName) return; + if (namespace) { + hookNamespaceByTranslator.set(localName, namespace); + } else { + hookNamespaceListByTranslator.set(localName, namespaces); + } + } + + if (ts.isObjectBindingPattern(node.name)) { + for (const element of node.name.elements) { + const propertyName = element.propertyName ? propertyNameToString(ts, element.propertyName) : propertyNameToString(ts, element.name); + const localName = propertyNameToString(ts, element.name); + if (propertyName === 't' && localName) { + rememberLocalName(localName); + } + } + } else if (ts.isArrayBindingPattern(node.name)) { + const first = node.name.elements[0]; + if (first && ts.isBindingElement(first) && ts.isIdentifier(first.name)) { + rememberLocalName(first.name.text); + } + } + } + + function rememberGetTTranslator(node) { + if ( + ts.isVariableDeclaration(node) && + ts.isIdentifier(node.name) && + node.initializer && + isI18nServiceGetTCall(ts, node.initializer) + ) { + fullKeyTranslatorNames.add(node.name.text); } } + + function recordTranslationCall(node) { + if (!ts.isCallExpression(node)) return; + + const key = staticStringLiteral(ts, node.arguments[0]); + if (!key) return; + + let fullKey = null; + let kind = 'full'; + if (isI18nServiceTCall(ts, node.expression) || isI18nServiceGetTCall(ts, node.expression)) { + if (!key.includes(':')) return; + fullKey = key; + } else if (ts.isIdentifier(node.expression) && fullKeyTranslatorNames.has(node.expression.text)) { + if (!key.includes(':')) return; + fullKey = key; + } else if (ts.isIdentifier(node.expression) && hookNamespaceByTranslator.has(node.expression.text)) { + const namespace = hookNamespaceByTranslator.get(node.expression.text); + fullKey = key.includes(':') ? key : `${namespace}:${key}`; + kind = key.includes(':') ? 'hook-full' : 'hook-relative'; + } else if (ts.isIdentifier(node.expression) && hookNamespaceListByTranslator.has(node.expression.text)) { + const namespaces = hookNamespaceListByTranslator.get(node.expression.text); + fullKey = key.includes(':') ? key : `${namespaces[0]}:${key}`; + kind = key.includes(':') ? 'hook-full-array' : 'hook-relative-array'; + } + + if (!fullKey) return; + + output.push({ + key: fullKey, + kind, + options: node.arguments[1], + location: `${toPosixPath(path.relative(root, file))}:${lineNumberAt(text, node.getStart(sourceFile))}`, + file: toPosixPath(path.relative(root, file)), + sourceFile, + }); + } + + function visit(node) { + rememberHookTranslator(node); + rememberGetTTranslator(node); + recordTranslationCall(node); + ts.forEachChild(node, visit); + } + + visit(sourceFile); } return output; } +function collectWebUiStaticTranslationKeys() { + return collectWebUiTranslationCallFacts().map(({ key, kind, location }) => ({ key, kind, location })); +} + function buildWebUiKeySet(namespaces) { const keys = new Set(); for (const namespace of namespaces) { @@ -841,14 +1000,139 @@ function auditWebUiStaticTranslationKeys(namespaces) { .filter(({ key }) => !knownKeys.has(key)); if (missing.length > 0) { + const relativeCount = missing.filter(({ kind }) => kind === 'hook-relative').length; reportError( - `Found ${missing.length} unknown static Web UI i18n key(s). First entries: ${ + `Found ${missing.length} unknown static Web UI i18n key(s), including ${relativeCount} relative static Web UI i18n key(s). First entries: ${ missing.slice(0, 12).map(({ key, location }) => `${location} ${key}`).join(', ') }`, ); } } +function isLiteralFallbackInitializer(ts, initializer) { + const value = unwrapTsExpression(ts, initializer); + if (!value) return false; + if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value) || ts.isTemplateExpression(value)) { + return true; + } + if (ts.isArrayLiteralExpression(value)) { + return value.elements.some((element) => ( + ts.isStringLiteral(element) || + ts.isNoSubstitutionTemplateLiteral(element) || + ts.isTemplateExpression(element) + )); + } + return false; +} + +function collectWebUiLiteralFallbackFindings() { + const ts = auditTypeScript; + const findings = []; + + for (const call of collectWebUiTranslationCallFacts()) { + const options = unwrapTsExpression(ts, call.options); + if (!options || !ts.isObjectLiteralExpression(options)) continue; + + for (const property of options.properties) { + if (!ts.isPropertyAssignment(property)) continue; + if (propertyNameToString(ts, property.name) !== 'defaultValue') continue; + if (!isLiteralFallbackInitializer(ts, property.initializer)) continue; + + findings.push({ + file: call.file, + location: call.location, + key: call.key, + }); + } + } + + return findings; +} + +function auditWebUiLiteralFallbackBudget() { + if (!fs.existsSync(literalFallbackBaselinePath)) { + reportError('Missing scripts/i18n-literal-fallback-baseline.json'); + return; + } + + const baseline = readJsonFile(literalFallbackBaselinePath); + const budgetByFile = new Map( + (baseline.budgets ?? []).map((entry) => [entry.path, entry]), + ); + const findingsByFile = new Map(); + + for (const finding of collectWebUiLiteralFallbackFindings()) { + 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} literal i18next defaultValue fallback(s) but is missing from scripts/i18n-literal-fallback-baseline.json. First entries: ${ + findings.slice(0, 8).map((finding) => `${finding.location} ${finding.key}`).join(', ') + }`, + ); + continue; + } + + const actualCountByKey = new Map(); + for (const finding of findings) { + actualCountByKey.set(finding.key, (actualCountByKey.get(finding.key) ?? 0) + 1); + } + + if (Array.isArray(budget.literalDefaultValues) || isPlainObject(budget.literalDefaultValues)) { + const allowedCountByKey = new Map( + Array.isArray(budget.literalDefaultValues) + ? budget.literalDefaultValues.map((entry) => [entry.key, entry.count]) + : Object.entries(budget.literalDefaultValues), + ); + + for (const [key, count] of actualCountByKey.entries()) { + const allowed = allowedCountByKey.get(key); + if (typeof allowed !== 'number') { + reportError(`${file} has unbudgeted literal i18next defaultValue fallback for "${key}"`); + } else if (count > allowed) { + reportError(`${file} has ${count} literal i18next defaultValue fallback(s) for "${key}", budget is ${allowed}`); + } else if (count < allowed) { + reportError(`${file} has ${count} literal i18next defaultValue fallback(s) for "${key}", below baseline ${allowed}; lower scripts/i18n-literal-fallback-baseline.json.`); + } + } + + for (const [key, allowed] of allowedCountByKey.entries()) { + if (allowed > 0 && !actualCountByKey.has(key)) { + reportError(`${file} no longer has a literal i18next defaultValue fallback for "${key}"; lower scripts/i18n-literal-fallback-baseline.json.`); + } + } + } else if (typeof budget.maxLiteralDefaultValues === 'number') { + if (findings.length > budget.maxLiteralDefaultValues) { + reportError( + `${file} has ${findings.length} literal i18next defaultValue fallback(s), budget is ${budget.maxLiteralDefaultValues}. First entries: ${ + findings.slice(0, 8).map((finding) => `${finding.location} ${finding.key}`).join(', ') + }`, + ); + } else if (findings.length < budget.maxLiteralDefaultValues) { + reportError( + `${file} has ${findings.length} literal i18next defaultValue fallback(s), below baseline ${budget.maxLiteralDefaultValues}; lower scripts/i18n-literal-fallback-baseline.json.`, + ); + } + } else { + reportError(`${file} has an invalid literal fallback baseline entry`); + } + } + + for (const [file, budget] of budgetByFile.entries()) { + const hasBudgetedFindings = Array.isArray(budget.literalDefaultValues) || isPlainObject(budget.literalDefaultValues) + ? Object.keys(budget.literalDefaultValues).length > 0 + : budget.maxLiteralDefaultValues > 0; + if (hasBudgetedFindings && !findingsByFile.has(file)) { + reportError( + `${file} no longer has literal i18next defaultValue fallback(s); remove it from scripts/i18n-literal-fallback-baseline.json.`, + ); + } + } +} + function countCjkSourceLines(scanRoot, predicate) { const cjkPattern = /\p{Script=Han}/u; const findings = []; @@ -919,9 +1203,13 @@ auditMobileWebBoundary(); const namespaces = auditNamespaceCoverage(); auditKeyParity(namespaces); auditWebI18nextPlaceholderParity(namespaces); -auditWebUiStaticTranslationKeys(namespaces); -auditMobileWebMessageParity(); -auditMobileWebPlaceholderParity(); +auditTypeScript = loadTypeScriptForAudit(); +if (auditTypeScript) { + auditWebUiStaticTranslationKeys(namespaces); + auditWebUiLiteralFallbackBudget(); + auditMobileWebMessageParity(); + auditMobileWebPlaceholderParity(); +} auditInstallerKeyParity(); auditInstallerPlaceholderParity(); auditCoreFluentParity(); diff --git a/scripts/i18n-contract.test.mjs b/scripts/i18n-contract.test.mjs index d2d4456d5..094879ec7 100644 --- a/scripts/i18n-contract.test.mjs +++ b/scripts/i18n-contract.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { execFileSync } from 'node:child_process'; +import { execFileSync, spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; @@ -23,6 +23,35 @@ function readText(relativePath) { return fs.readFileSync(path.join(root, relativePath), 'utf8'); } +function writeText(relativePath, content) { + fs.mkdirSync(path.dirname(path.join(root, relativePath)), { recursive: true }); + fs.writeFileSync(path.join(root, relativePath), content, 'utf8'); +} + +function runI18nAudit() { + return spawnSync(process.execPath, ['scripts/i18n-audit.mjs'], { + cwd: root, + encoding: 'utf8', + }); +} + +function withTemporaryTextFile(relativePath, content, callback) { + const absolutePath = path.join(root, relativePath); + const existed = fs.existsSync(absolutePath); + const previous = existed ? fs.readFileSync(absolutePath, 'utf8') : null; + + writeText(relativePath, content); + try { + return callback(); + } finally { + if (existed) { + fs.writeFileSync(absolutePath, previous, 'utf8'); + } else { + fs.rmSync(absolutePath, { force: true }); + } + } +} + function flattenKeys(value, prefix = '') { if (value == null || typeof value !== 'object' || Array.isArray(value)) { return prefix ? [prefix] : []; @@ -259,6 +288,104 @@ test('i18n audit fails literal fallbacks and unknown static keys', () => { assert.match(auditSource, /collectWebUiStaticTranslationKeys/, 'static key collection should be explicit'); }); +test('i18n audit gates object-form literal fallbacks with an explicit budget', () => { + const auditSource = readText('scripts/i18n-audit.mjs'); + const fallbackBaselineSource = readText('scripts/i18n-literal-fallback-baseline.json'); + const fallbackBaseline = JSON.parse(fallbackBaselineSource); + + assert.ok( + fallbackBaseline.budgets.every((entry) => ( + entry.literalDefaultValues && + typeof entry.literalDefaultValues === 'object' && + !Array.isArray(entry.literalDefaultValues) + )), + 'literal fallback baseline should allowlist exact keys per file', + ); + assert.match(auditSource, /i18n-literal-fallback-baseline\.json/, 'audit should read the literal fallback baseline'); + assert.match(auditSource, /auditWebUiLiteralFallbackBudget/, 'object-form defaultValue fallbacks should have a no-growth gate'); + assert.match(auditSource, /defaultValue/, 'audit should inspect i18next defaultValue options'); +}); + +test('i18n audit validates static hook translation keys with namespace context', () => { + const auditSource = readText('scripts/i18n-audit.mjs'); + + assert.match(auditSource, /use(?:I18n|Translation)/, 'audit should inspect hook namespace declarations'); + assert.match(auditSource, /hookNamespaceByTranslator/, 'audit should remember namespace aliases returned by hooks'); + assert.match(auditSource, /relative static Web UI i18n key/, 'audit should report missing relative hook keys clearly'); +}); + +test('i18n audit catches array-namespace keys and stale literal fallback budgets', { concurrency: false }, () => { + const arrayNamespaceFixture = 'src/web-ui/src/__i18n_array_namespace_audit_fixture__.tsx'; + const missingFullKey = 'components:__arrayNamespaceAuditMissingKey__'; + const missingRelativeKey = 'components:__arrayNamespaceAuditMissingRelativeKey__'; + const missingUseTranslationKey = 'common:__arrayNamespaceUseTranslationMissingKey__'; + + withTemporaryTextFile( + arrayNamespaceFixture, + [ + "import { useI18n, useTranslation } from '@/infrastructure/i18n';", + 'export function I18nArrayNamespaceAuditFixture() {', + " const { t } = useI18n(['components', 'common', 'errors']);", + " const [translate] = useTranslation(['common', 'errors']);", + ` return
{t('${missingFullKey}')}{t('__arrayNamespaceAuditMissingRelativeKey__')}{translate('${missingUseTranslationKey}')}
;`, + '}', + '', + ].join('\n'), + () => { + const result = runI18nAudit(); + assert.notEqual(result.status, 0, 'array namespace full keys must be included in unknown-key audit'); + assert.match( + `${result.stdout}\n${result.stderr}`, + new RegExp(missingFullKey), + 'audit output should identify the missing array-namespace key', + ); + assert.match( + `${result.stdout}\n${result.stderr}`, + new RegExp(missingRelativeKey), + 'audit output should resolve relative array-namespace keys against the first namespace', + ); + assert.match( + `${result.stdout}\n${result.stderr}`, + new RegExp(missingUseTranslationKey), + 'audit output should cover useTranslation array namespace aliases', + ); + }, + ); + + const baselinePath = 'scripts/i18n-literal-fallback-baseline.json'; + const baseline = readJson(baselinePath); + baseline.budgets.push({ + path: 'src/web-ui/src/__removed_literal_fallback_fixture__.tsx', + literalDefaultValues: { + 'common:actions.close': 1, + }, + }); + + withTemporaryTextFile(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`, () => { + const result = runI18nAudit(); + assert.notEqual(result.status, 0, 'stale literal fallback baseline entries must fail audit'); + assert.match( + `${result.stdout}\n${result.stderr}`, + /no longer has literal i18next defaultValue fallback/, + 'audit output should explain the stale literal fallback budget', + ); + }); +}); + +test('i18n generation checks are stable across line endings', () => { + const generatorSource = readText('scripts/generate-i18n-contract.mjs'); + + assert.match(generatorSource, /normalizeGeneratedText/, 'generated contract check should normalize line endings'); + assert.match(generatorSource, /currentContentForCheck/, 'generated contract check should compare normalized current content'); +}); + +test('i18n audit preflights TypeScript before TypeScript-backed checks', () => { + const auditSource = readText('scripts/i18n-audit.mjs'); + + assert.match(auditSource, /loadTypeScriptForAudit/, 'audit should load TypeScript through a single preflight helper'); + assert.match(auditSource, /Run pnpm install/, 'missing TypeScript should tell contributors how to fix the local setup'); +}); + 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-literal-fallback-baseline.json b/scripts/i18n-literal-fallback-baseline.json new file mode 100644 index 000000000..45b113e65 --- /dev/null +++ b/scripts/i18n-literal-fallback-baseline.json @@ -0,0 +1,585 @@ +{ + "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": { + "scenes/agents:nav.teams": 1, + "scenes/agents:reviewTeams.default.name": 1, + "scenes/agents:reviewTeams.default.summary": 1, + "scenes/agents:reviewTeams.default.tags": 1, + "scenes/agents:reviewTeams.detail.localOnly": 1, + "scenes/agents:reviewTeams.detail.open": 1, + "scenes/agents:teamsZone.empty": 1, + "scenes/agents:teamsZone.subtitle": 1, + "scenes/agents:teamsZone.title": 1 + } + }, + { + "path": "src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx", + "literalDefaultValues": { + "scenes/agents:reviewTeams.default.members": 1, + "scenes/agents:reviewTeams.detail.back": 1, + "scenes/agents:reviewTeams.detail.extraCount": 1, + "scenes/agents:reviewTeams.detail.fileCountValue": 1, + "scenes/agents:reviewTeams.detail.fileSplitThreshold": 1, + "scenes/agents:reviewTeams.detail.instancesValue": 1, + "scenes/agents:reviewTeams.detail.judgeTimeout": 1, + "scenes/agents:reviewTeams.detail.loading": 1, + "scenes/agents:reviewTeams.detail.localOnly": 2, + "scenes/agents:reviewTeams.detail.localOnlyDescription": 1, + "scenes/agents:reviewTeams.detail.lockedCount": 1, + "scenes/agents:reviewTeams.detail.maxSameRoleInstances": 1, + "scenes/agents:reviewTeams.detail.membersDescription": 1, + "scenes/agents:reviewTeams.detail.membersTitle": 1, + "scenes/agents:reviewTeams.detail.memberTypes.core": 1, + "scenes/agents:reviewTeams.detail.memberTypes.extra": 1, + "scenes/agents:reviewTeams.detail.memberTypes.locked": 1, + "scenes/agents:reviewTeams.detail.noTimeout": 1, + "scenes/agents:reviewTeams.detail.openSettings": 2, + "scenes/agents:reviewTeams.detail.parallelDescription": 1, + "scenes/agents:reviewTeams.detail.parallelLabel": 1, + "scenes/agents:reviewTeams.detail.policyMetricsLabel": 1, + "scenes/agents:reviewTeams.detail.policyStrategyLabel": 1, + "scenes/agents:reviewTeams.detail.policySummaryAction": 1, + "scenes/agents:reviewTeams.detail.policySummaryEyebrow": 1, + "scenes/agents:reviewTeams.detail.policySummaryIntro": 1, + "scenes/agents:reviewTeams.detail.policySummaryTitle": 1, + "scenes/agents:reviewTeams.detail.qualityGate": 2, + "scenes/agents:reviewTeams.detail.responsibilities": 1, + "scenes/agents:reviewTeams.detail.reviewerTimeout": 1, + "scenes/agents:reviewTeams.detail.secondsValue": 1, + "scenes/agents:reviewTeams.detail.splitDisabled": 1, + "scenes/agents:reviewTeams.detail.subtitle": 1, + "scenes/agents:reviewTeams.detail.summaryDescription": 1, + "scenes/agents:reviewTeams.detail.summaryTitle": 1, + "scenes/agents:reviewTeams.detail.title": 1, + "scenes/agents:reviewTeams.detail.warning": 1, + "settings/default-model:selection.fast": 1, + "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/app/scenes/settings/SettingsNav.tsx", + "literalDefaultValues": { + "settings:title": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx", + "literalDefaultValues": { + "flow-chat:childSession.stoppingReview": 2, + "flow-chat:childSession.stopReview": 2, + "flow-chat:childSession.stopReviewFailed": 1, + "flow-chat:deepReviewActionBar.minimizedDeep": 1, + "flow-chat:deepReviewActionBar.minimizedFix": 1, + "flow-chat:deepReviewActionBar.minimizedFixCompleted": 1, + "flow-chat:deepReviewActionBar.minimizedFixFailed": 1, + "flow-chat:deepReviewActionBar.minimizedFixReview": 1, + "flow-chat:deepReviewActionBar.minimizedResume": 1, + "flow-chat:deepReviewActionBar.minimizedReviewInterrupted": 1, + "flow-chat:deepReviewActionBar.minimizedReviewRunningDeep": 1, + "flow-chat:deepReviewActionBar.minimizedReviewRunningStandard": 1, + "flow-chat:deepReviewActionBar.minimizedStandard": 1, + "flow-chat:deepReviewActionBar.restore": 1, + "flow-chat:flowChatHeader.btwBackTooltipWithoutTurn": 1, + "flow-chat:flowChatHeader.btwBackTooltipWithTurn": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/ChatInput.tsx", + "literalDefaultValues": { + "flow-chat:btw.empty": 1, + "flow-chat:btw.nestedDisabled": 3, + "flow-chat:btw.noSession": 3, + "flow-chat:btw.title": 1, + "flow-chat:chatInput.compactAction": 1, + "flow-chat:chatInput.compactBusy": 1, + "flow-chat:chatInput.compactFailed": 1, + "flow-chat:chatInput.compactNoSession": 1, + "flow-chat:chatInput.compactUsage": 2, + "flow-chat:chatInput.deepreviewAction": 1, + "flow-chat:chatInput.deepreviewBusy": 1, + "flow-chat:chatInput.deepreviewFailed": 1, + "flow-chat:chatInput.deepreviewNestedDisabled": 1, + "flow-chat:chatInput.deepreviewNoSession": 1, + "flow-chat:chatInput.deepreviewThreadTitle": 1, + "flow-chat:chatInput.deepreviewUsage": 1, + "flow-chat:chatInput.goalAction": 1, + "flow-chat:chatInput.goalActivated": 1, + "flow-chat:chatInput.goalAiFailed": 1, + "flow-chat:chatInput.goalFailed": 1, + "flow-chat:chatInput.goalGenerating": 1, + "flow-chat:chatInput.goalNestedDisabled": 1, + "flow-chat:chatInput.goalNoSession": 1, + "flow-chat:chatInput.goalUsage": 2, + "flow-chat:chatInput.initAction": 1, + "flow-chat:chatInput.initBusy": 1, + "flow-chat:chatInput.initFailed": 1, + "flow-chat:chatInput.initNoSession": 1, + "flow-chat:chatInput.initUsage": 2, + "flow-chat:chatInput.noMatchingCommand": 3, + "flow-chat:chatInput.promptCacheGuardBody": 1, + "flow-chat:chatInput.promptCacheGuardCancel": 1, + "flow-chat:chatInput.promptCacheGuardConfirm": 1, + "flow-chat:chatInput.promptCacheGuardTitle": 1, + "flow-chat:chatInput.quickAction": 2, + "flow-chat:chatInput.usageAction": 1, + "flow-chat:chatInput.usageBusy": 1, + "flow-chat:chatInput.usageCommandUsage": 2, + "flow-chat:chatInput.usageFailed": 1, + "flow-chat:chatInput.usageNoSession": 2, + "flow-chat:chatInput.usageNoWorkspace": 1, + "flow-chat:input.largePastePlaceholder": 1, + "flow-chat:input.messageTooLarge": 2, + "flow-chat:input.removeImage": 1, + "flow-chat:usage.loading.markdown": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewConsent.cancel": 2, + "flow-chat:deepReviewConsent.confirm": 1, + "flow-chat:deepReviewConsent.dontShowAgain": 1, + "flow-chat:deepReviewConsent.eyebrow": 1, + "flow-chat:deepReviewConsent.runStrategy": 1, + "flow-chat:deepReviewConsent.selectedStrategy": 1, + "flow-chat:deepReviewConsent.sessionConcurrencyBody": 1, + "flow-chat:deepReviewConsent.sessionConcurrencyTitle": 1, + "flow-chat:deepReviewConsent.skippedGroupTitle": 1, + "flow-chat:deepReviewConsent.skippedMore": 1, + "flow-chat:deepReviewConsent.skippedReasons.budgetLimited": 1, + "flow-chat:deepReviewConsent.skippedReasons.disabled": 1, + "flow-chat:deepReviewConsent.skippedReasons.invalidTooling": 1, + "flow-chat:deepReviewConsent.skippedReasons.notApplicable": 1, + "flow-chat:deepReviewConsent.skippedReasons.skipped": 1, + "flow-chat:deepReviewConsent.skippedReasons.unavailable": 1, + "flow-chat:deepReviewConsent.skippedReviewers": 1, + "flow-chat:deepReviewConsent.strategyOverrideTitle": 1, + "flow-chat:deepReviewConsent.strategyRuntimeImpact": 1, + "flow-chat:deepReviewConsent.strategyTokenImpact": 1, + "flow-chat:deepReviewConsent.summaryTitle": 1, + "flow-chat:deepReviewConsent.title": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx", + "literalDefaultValues": { + "flow-chat:flowChatHeader.backgroundSubagents": 4, + "flow-chat:flowChatHeader.backgroundSubagentUntitled": 1, + "flow-chat:flowChatHeader.jumpToCurrentTurn": 1, + "flow-chat:flowChatHeader.nextTurn": 2, + "flow-chat:flowChatHeader.previousTurn": 2, + "flow-chat:flowChatHeader.searchClose": 2, + "flow-chat:flowChatHeader.searchNext": 2, + "flow-chat:flowChatHeader.searchNoResults": 1, + "flow-chat:flowChatHeader.searchOpen": 2, + "flow-chat:flowChatHeader.searchPlaceholder": 2, + "flow-chat:flowChatHeader.searchPrevious": 2, + "flow-chat:flowChatHeader.searchResult": 1, + "flow-chat:flowChatHeader.subagentStatusFinishing": 1, + "flow-chat:flowChatHeader.subagentStatusProcessing": 1, + "flow-chat:flowChatHeader.turnBadge": 2, + "flow-chat:flowChatHeader.turnList": 1, + "flow-chat:flowChatHeader.untitledTurn": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/modern/ForkSessionButton.tsx", + "literalDefaultValues": { + "flow-chat:modelRound.forkDialog": 1, + "flow-chat:modelRound.forkFailed": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.tsx", + "literalDefaultValues": { + "flow-chat:sessionFileModificationsBar.deepReviewSource": 1, + "flow-chat:sessionFileModificationsBar.reviewSource": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx", + "literalDefaultValues": { + "flow-chat:sessionFilesBadge.actionsButton": 1, + "flow-chat:sessionFilesBadge.actionsButtonRunning": 1, + "flow-chat:sessionFilesBadge.actionsMenuHint": 1, + "flow-chat:sessionFilesBadge.collapseFileDiffList": 1, + "flow-chat:sessionFilesBadge.deepReview.displayMessage": 1, + "flow-chat:sessionFilesBadge.deepReview.displayMessageFiltered": 1, + "flow-chat:sessionFilesBadge.deepReview.threadTitle": 1, + "flow-chat:sessionFilesBadge.expandChangeListAriaCue": 1, + "flow-chat:sessionFilesBadge.expandChangeListCue": 1, + "flow-chat:sessionFilesBadge.filesSummaryCount": 3, + "flow-chat:sessionFilesBadge.review.displayMessageFiltered": 1, + "flow-chat:sessionFilesBadge.review.filteredNotice": 2, + "flow-chat:sessionFilesBadge.review.noEligibleFiles": 2, + "flow-chat:sessionFilesBadge.review.promptFiltered": 1, + "flow-chat:sessionFilesBadge.review.threadTitle": 1, + "flow-chat:sessionFilesBadge.reviewRunningHint": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/ScrollToTurnHeaderButton.tsx", + "literalDefaultValues": { + "flow-chat:scroll.toCurrentTurn": 2 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/StickyTaskIndicator.tsx", + "literalDefaultValues": { + "flow-chat:stickyTaskIndicator.tooltip": 1, + "flow-chat:toolCards.taskTool.defaultAgentKind": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx", + "literalDefaultValues": { + "flow-chat:toolCards.taskDetailPanel.stoppingSubagent": 1, + "flow-chat:toolCards.taskDetailPanel.stopSubagent": 1, + "flow-chat:toolCards.taskDetailPanel.stopSubagentFailed": 1, + "flow-chat:toolCards.taskDetailPanel.stopSubagentHint": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/deep-review/action-bar/CapacityQueueNotice.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewActionBar.capacityQueue.activeReviewerCount": 1, + "flow-chat:deepReviewActionBar.capacityQueue.activeReviewerDetail": 1, + "flow-chat:deepReviewActionBar.capacityQueue.activeReviewerTitle": 1, + "flow-chat:deepReviewActionBar.capacityQueue.cancelQueued": 1, + "flow-chat:deepReviewActionBar.capacityQueue.continueQueue": 1, + "flow-chat:deepReviewActionBar.capacityQueue.detail": 1, + "flow-chat:deepReviewActionBar.capacityQueue.elapsed": 2, + "flow-chat:deepReviewActionBar.capacityQueue.elapsedWithMax": 1, + "flow-chat:deepReviewActionBar.capacityQueue.longLaunchBatchWaitDetail": 1, + "flow-chat:deepReviewActionBar.capacityQueue.openReviewSettings": 1, + "flow-chat:deepReviewActionBar.capacityQueue.optionalReviewer": 1, + "flow-chat:deepReviewActionBar.capacityQueue.pausedTitle": 1, + "flow-chat:deepReviewActionBar.capacityQueue.pauseQueue": 1, + "flow-chat:deepReviewActionBar.capacityQueue.providerDetail": 1, + "flow-chat:deepReviewActionBar.capacityQueue.providerTitle": 1, + "flow-chat:deepReviewActionBar.capacityQueue.reason": 1, + "flow-chat:deepReviewActionBar.capacityQueue.reviewerStatusPaused": 1, + "flow-chat:deepReviewActionBar.capacityQueue.reviewerStatusQueued": 1, + "flow-chat:deepReviewActionBar.capacityQueue.sessionBusy": 1, + "flow-chat:deepReviewActionBar.capacityQueue.skipOptionalQueued": 1, + "flow-chat:deepReviewActionBar.capacityQueue.stopHint": 1, + "flow-chat:deepReviewActionBar.capacityQueue.title": 1, + "flow-chat:deepReviewActionBar.capacityQueue.waitingReviewersTitle": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/deep-review/action-bar/DecisionExecutionGate.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewActionBar.decisionGate.cancel": 1, + "flow-chat:deepReviewActionBar.decisionGate.description": 1, + "flow-chat:deepReviewActionBar.decisionGate.missingSelection": 1, + "flow-chat:deepReviewActionBar.decisionGate.noOptionsHint": 1, + "flow-chat:deepReviewActionBar.decisionGate.supplementLabel": 1, + "flow-chat:deepReviewActionBar.decisionGate.supplementPlaceholder": 1, + "flow-chat:deepReviewActionBar.decisionGate.title": 1, + "flow-chat:toolCards.codeReview.remediationActions.recommended": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewActionBar.capacityQueue.controlFailed": 1, + "flow-chat:deepReviewActionBar.capacityQueue.controlFailedWithReason": 1, + "flow-chat:deepReviewActionBar.capacityQueue.controlPartiallyFailedWithReason": 1, + "flow-chat:deepReviewActionBar.contextOverflowTitle": 1, + "flow-chat:deepReviewActionBar.customInstructionsPlaceholder": 1, + "flow-chat:deepReviewActionBar.degradation.compressContextPending": 1, + "flow-chat:deepReviewActionBar.degradation.reduceReviewersPending": 1, + "flow-chat:deepReviewActionBar.diagnosticsCopied": 1, + "flow-chat:deepReviewActionBar.diagnosticsCopyFailed": 1, + "flow-chat:deepReviewActionBar.elapsedTime": 1, + "flow-chat:deepReviewActionBar.fixAndReviewRunning": 1, + "flow-chat:deepReviewActionBar.fixCompleted": 1, + "flow-chat:deepReviewActionBar.fixCompletedMessage": 1, + "flow-chat:deepReviewActionBar.fixFailed": 1, + "flow-chat:deepReviewActionBar.fixRunning": 1, + "flow-chat:deepReviewActionBar.fixTimeout": 1, + "flow-chat:deepReviewActionBar.hideCustomInput": 1, + "flow-chat:deepReviewActionBar.longRunningHint": 1, + "flow-chat:deepReviewActionBar.minimize": 1, + "flow-chat:deepReviewActionBar.progressResumePreserved": 1, + "flow-chat:deepReviewActionBar.replaceInputConfirmAction": 1, + "flow-chat:deepReviewActionBar.replaceInputConfirmMessage": 1, + "flow-chat:deepReviewActionBar.replaceInputConfirmTitle": 1, + "flow-chat:deepReviewActionBar.resumeBlocked": 1, + "flow-chat:deepReviewActionBar.resumeBlockedConfirmAction": 1, + "flow-chat:deepReviewActionBar.resumeBlockedConfirmMessage": 1, + "flow-chat:deepReviewActionBar.resumeBlockedConfirmTitle": 1, + "flow-chat:deepReviewActionBar.resumeBlockedWithReason": 1, + "flow-chat:deepReviewActionBar.resumeFailed": 1, + "flow-chat:deepReviewActionBar.resumeFailedMessage": 1, + "flow-chat:deepReviewActionBar.resumeFailedWithReason": 1, + "flow-chat:deepReviewActionBar.resumeRequestDisplay": 1, + "flow-chat:deepReviewActionBar.resumeRunning": 1, + "flow-chat:deepReviewActionBar.retryIncompleteFailed": 1, + "flow-chat:deepReviewActionBar.retryIncompleteRequestDisplay": 1, + "flow-chat:deepReviewActionBar.reviewError": 1, + "flow-chat:deepReviewActionBar.reviewErrorWithReason": 1, + "flow-chat:deepReviewActionBar.reviewInterrupted": 1, + "flow-chat:deepReviewActionBar.reviewInterruptedWithReason": 1, + "flow-chat:deepReviewActionBar.reviewWaitingCapacity": 1, + "flow-chat:deepReviewActionBar.showCustomInput": 1, + "flow-chat:reviewActionBar.noIssuesFound": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/deep-review/action-bar/PartialResultsPanel.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewActionBar.hidePartialResults": 1, + "flow-chat:deepReviewActionBar.partialIssues": 1, + "flow-chat:deepReviewActionBar.partialRemediationItems": 1, + "flow-chat:deepReviewActionBar.partialResultsDescription": 1, + "flow-chat:deepReviewActionBar.partialReviewerSummaries": 1, + "flow-chat:deepReviewActionBar.viewPartialResults": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/deep-review/action-bar/RecoveryPlanPreview.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewActionBar.recoveryPreserve": 1, + "flow-chat:deepReviewActionBar.recoveryRerun": 1, + "flow-chat:deepReviewActionBar.recoverySkip": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/deep-review/action-bar/RemediationSelectionPanel.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewActionBar.remediationStatus.fixed": 1, + "flow-chat:deepReviewActionBar.remediationStatus.fixing": 1, + "flow-chat:reviewActionBar.needsDecisionTag": 1, + "flow-chat:toolCards.codeReview.remediationActions.collapseOptions": 1, + "flow-chat:toolCards.codeReview.remediationActions.expandOptions": 1, + "flow-chat:toolCards.codeReview.remediationActions.noSelectionHint": 1, + "flow-chat:toolCards.codeReview.remediationActions.recommended": 1, + "flow-chat:toolCards.codeReview.remediationActions.selectionCount": 1, + "flow-chat:toolCards.codeReview.remediationActions.ungrouped": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/deep-review/action-bar/ReviewActionControls.tsx", + "literalDefaultValues": { + "flow-chat:deepReviewActionBar.continueFix": 1, + "flow-chat:deepReviewActionBar.copyDiagnostics": 1, + "flow-chat:deepReviewActionBar.fillBackInput": 1, + "flow-chat:deepReviewActionBar.fillBackInputHint": 1, + "flow-chat:deepReviewActionBar.fixInterrupted": 1, + "flow-chat:deepReviewActionBar.minimize": 1, + "flow-chat:deepReviewActionBar.openModelSettings": 1, + "flow-chat:deepReviewActionBar.resumeReview": 1, + "flow-chat:deepReviewActionBar.retryIncompleteSlices": 1, + "flow-chat:deepReviewActionBar.skipRemaining": 1, + "flow-chat:deepReviewActionBar.switchModel": 1, + "flow-chat:deepReviewActionBar.viewPartialResults": 1, + "flow-chat:toolCards.codeReview.remediationActions.fixAndReview": 1, + "flow-chat:toolCards.codeReview.remediationActions.startFix": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts", + "literalDefaultValues": { + "flow-chat:chatInput.goalAchieved": 1, + "flow-chat:chatInput.goalVerifyFailed": 1, + "flow-chat:chatInput.goalVerifying": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/services/openBtwSession.ts", + "literalDefaultValues": { + "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": { + "flow-chat:toolCards.codeReview.deepReviewResult": 1, + "flow-chat:toolCards.codeReview.reliabilityStatus.title": 2, + "flow-chat:toolCards.codeReview.remediationActions.certainty": 1, + "flow-chat:toolCards.codeReview.remediationActions.collapsePlan": 1, + "flow-chat:toolCards.codeReview.remediationActions.expandPlan": 1, + "flow-chat:toolCards.codeReview.remediationActions.location": 1, + "flow-chat:toolCards.codeReview.remediationActions.noRelatedIssue": 1, + "flow-chat:toolCards.codeReview.remediationActions.recommended": 1, + "flow-chat:toolCards.codeReview.remediationActions.relatedIssue": 1, + "flow-chat:toolCards.codeReview.remediationActions.severity": 1, + "flow-chat:toolCards.codeReview.remediationPlan": 1, + "flow-chat:toolCards.codeReview.reviewerIssues": 1, + "flow-chat:toolCards.codeReview.reviewerIssuesUnknown": 1, + "flow-chat:toolCards.codeReview.reviewerTeam": 1, + "flow-chat:toolCards.codeReview.reviewMode": 1, + "flow-chat:toolCards.codeReview.reviewScope": 1, + "flow-chat:toolCards.codeReview.runManifest.recommendedStrategy": 1, + "flow-chat:toolCards.codeReview.runManifest.reviewDepth": 1, + "flow-chat:toolCards.codeReview.runManifest.riskRecommendationTitle": 1, + "flow-chat:toolCards.codeReview.sectionItemCount": 3, + "flow-chat:toolCards.codeReview.sections.coverage": 1, + "flow-chat:toolCards.codeReview.sections.remediation": 1, + "flow-chat:toolCards.codeReview.sections.strengths": 1, + "flow-chat:toolCards.codeReview.sections.summary": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx", + "literalDefaultValues": { + "flow-chat:toolCards.contextCompression.noSummaryNotice": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.tsx", + "literalDefaultValues": { + "flow-chat:toolCards.getFileDiff.diffFile": 1, + "flow-chat:toolCards.getFileDiff.failed": 2, + "flow-chat:toolCards.getFileDiff.gettingDiff": 1, + "flow-chat:toolCards.getFileDiff.preparing": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx", + "literalDefaultValues": { + "flow-chat:toolCards.git.commandCopied": 4, + "flow-chat:toolCards.git.copyCommand": 4, + "flow-chat:toolCards.git.copyCommandFailed": 2 + } + }, + { + "path": "src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx", + "literalDefaultValues": { + "flow-chat:toolCards.readFile.permissionRequest": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/tool-cards/ReviewSessionSummaryCard.tsx", + "literalDefaultValues": { + "flow-chat:toolCards.reviewSessionSummary.changedFilesTitle": 1, + "flow-chat:toolCards.reviewSessionSummary.deepTitle": 1, + "flow-chat:toolCards.reviewSessionSummary.emptySummary": 1, + "flow-chat:toolCards.reviewSessionSummary.failed": 1, + "flow-chat:toolCards.reviewSessionSummary.filesChanged": 1, + "flow-chat:toolCards.reviewSessionSummary.issueCount": 1, + "flow-chat:toolCards.reviewSessionSummary.noIssues": 1, + "flow-chat:toolCards.reviewSessionSummary.openReview": 1, + "flow-chat:toolCards.reviewSessionSummary.running": 1, + "flow-chat:toolCards.reviewSessionSummary.standardTitle": 1, + "flow-chat:toolCards.reviewSessionSummary.waitingSummary": 1 + } + }, + { + "path": "src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.tsx", + "literalDefaultValues": { + "flow-chat:toolCards.timeout.cancelledDurationTooltip": 1, + "flow-chat:toolCards.timeout.completedDurationTooltip": 1, + "flow-chat:toolCards.timeout.durationTooltip": 1, + "flow-chat:toolCards.timeout.failedDurationTooltip": 1, + "flow-chat:toolCards.timeout.failedDurationTooltipWithReason": 1 + } + }, + { + "path": "src/web-ui/src/infrastructure/config/components/AppearanceConfig.tsx", + "literalDefaultValues": { + "settings/basics:appearance.languageRowHint": 1, + "settings/basics:appearance.themeRowHint": 1 + } + }, + { + "path": "src/web-ui/src/infrastructure/config/components/ReviewConfig.tsx", + "literalDefaultValues": { + "settings/default-model:selection.fast": 2, + "settings/default-model:selection.primary": 2 + } + }, + { + "path": "src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx", + "literalDefaultValues": { + "settings/skills:filters.project": 1, + "settings/skills:filters.user": 1 + } + }, + { + "path": "src/web-ui/src/tools/editor/components/MarkdownEditor.tsx", + "literalDefaultValues": { + "tools:editor.markdownEditor.copiedMarkdown": 4, + "tools:editor.markdownEditor.copyMarkdown": 4 + } + } + ] +} diff --git a/src/mobile-web/src/i18n/messages.ts b/src/mobile-web/src/i18n/messages.ts index 803faa409..125e5e724 100644 --- a/src/mobile-web/src/i18n/messages.ts +++ b/src/mobile-web/src/i18n/messages.ts @@ -15,10 +15,7 @@ export const messages: Record = { continue: 'Continue', cancel: 'Cancel', switch: 'Switch', - workspace: 'Workspace', - userId: 'User ID', loading: 'Loading...', - language: 'Language', switchLanguage: 'Switch language', toggleTheme: 'Toggle theme', attachImage: 'Attach image', @@ -52,19 +49,13 @@ export const messages: Record = { continue: 'Continue', }, sessions: { - remoteCockpit: 'Remote cockpit', workspace: 'Workspace', switchWorkspace: 'Switch workspace', selectWorkspace: 'Select Workspace', noWorkspaces: 'No workspaces found', noWorkspaceSelected: 'No workspace selected', - selectMode: 'Mode', - chooseMode: 'Choose Your Mode', - changeMode: 'Change Mode', proMode: 'Expert Mode', - proModeDesc: 'Best for focused, one-shot tasks with a clear goal.', assistantMode: 'Assistant Mode', - assistantModeDesc: 'Best for ongoing work with context and personal preferences.', assistant: 'Assistant', defaultAssistant: 'Default Assistant', switchAssistant: 'Switch assistant', @@ -90,7 +81,6 @@ export const messages: Record = { agentCowork: 'Cowork', agentClaw: 'Claw', agentDefault: 'Default', - pullToRefresh: 'Pull to refresh', searchSessions: 'Search sessions...', renameSession: 'Rename', deleteSession: 'Delete', @@ -133,8 +123,6 @@ export const messages: Record = { analyzingImage: 'Analyzing image with image understanding model...', inputPlaceholder: 'How can I help you...', collapsedInputPlaceholder: 'Ask BitFun...', - workingPlaceholder: 'BitFun is working...', - streamingTapToQueue: 'Assistant is replying — tap to type a follow-up (queued)', collapsedStreamingPlaceholder: 'Replying...', messageQueued: 'Message queued; it will run after the current step', imageAnalyzingPlaceholder: 'Analyzing image...', @@ -144,7 +132,6 @@ export const messages: Record = { modelAutoDesc: 'Automatically route to the best model', modelPrimary: 'Primary Model', modelFast: 'Fast Model', - modelNotConfigured: 'Model not configured', askQuestionCount: '{count} question{suffix}', waiting: 'Waiting', modeAgentic: 'Agentic', @@ -197,10 +184,7 @@ export const messages: Record = { continue: '继续', cancel: '取消', switch: '切换', - workspace: '工作区', - userId: '用户 ID', loading: '加载中...', - language: '语言', switchLanguage: '切换语言', toggleTheme: '切换主题', attachImage: '添加图片', @@ -234,19 +218,13 @@ export const messages: Record = { continue: '继续', }, sessions: { - remoteCockpit: 'Remote cockpit', workspace: '工作区', switchWorkspace: '切换工作区', selectWorkspace: '选择工作区', noWorkspaces: '暂无工作区', noWorkspaceSelected: '未选择工作区', - selectMode: '模式', - chooseMode: '选择你的模式', - changeMode: '切换模式', proMode: '专业模式', - proModeDesc: '适合目标明确、一次完成的即时任务。', assistantMode: '助理模式', - assistantModeDesc: '适合持续推进、需要延续上下文和个人偏好的任务。', assistant: '助理', defaultAssistant: '默认助理', switchAssistant: '切换助理', @@ -272,7 +250,6 @@ export const messages: Record = { agentCowork: 'Cowork', agentClaw: 'Claw', agentDefault: '默认', - pullToRefresh: '下拉刷新', searchSessions: '搜索会话...', renameSession: '重命名', deleteSession: '删除', @@ -315,8 +292,6 @@ export const messages: Record = { analyzingImage: '正在使用图像理解模型分析图片...', inputPlaceholder: '有什么可以帮您...', collapsedInputPlaceholder: '发消息...', - workingPlaceholder: 'BitFun 正在处理中...', - streamingTapToQueue: '助手正在回复 — 点击输入后续消息(将排队)', collapsedStreamingPlaceholder: '正在回复...', messageQueued: '已加入队列,将在当前步骤结束后处理', imageAnalyzingPlaceholder: '正在分析图片...', @@ -326,7 +301,6 @@ export const messages: Record = { modelAutoDesc: '自动路由到最合适的模型', modelPrimary: 'Primary 模型', modelFast: 'Fast 模型', - modelNotConfigured: '模型未配置', askQuestionCount: '{count} 个问题{suffix}', waiting: '等待中', modeAgentic: 'Agentic', @@ -379,10 +353,7 @@ export const messages: Record = { continue: '繼續', cancel: '取消', switch: '切換', - workspace: '工作區', - userId: '用戶 ID', loading: '加載中...', - language: '語言', switchLanguage: '切換語言', toggleTheme: '切換主題', attachImage: '添加圖片', @@ -416,19 +387,13 @@ export const messages: Record = { continue: '繼續', }, sessions: { - remoteCockpit: 'Remote cockpit', workspace: '工作區', switchWorkspace: '切換工作區', selectWorkspace: '選擇工作區', noWorkspaces: '暫無工作區', noWorkspaceSelected: '未選擇工作區', - selectMode: '模式', - chooseMode: '選擇你的模式', - changeMode: '切換模式', proMode: '專業模式', - proModeDesc: '適合目標明確、一次完成的即時任務。', assistantMode: '助理模式', - assistantModeDesc: '適合持續推進、需要延續上下文和個人偏好的任務。', assistant: '助理', defaultAssistant: '默認助理', switchAssistant: '切換助理', @@ -454,7 +419,6 @@ export const messages: Record = { agentCowork: 'Cowork', agentClaw: 'Claw', agentDefault: '默認', - pullToRefresh: '下拉刷新', searchSessions: '搜尋會話...', renameSession: '重新命名', deleteSession: '刪除', @@ -497,8 +461,6 @@ export const messages: Record = { analyzingImage: '正在使用圖像理解模型分析圖片...', inputPlaceholder: '有什麼可以幫您...', collapsedInputPlaceholder: '發消息...', - workingPlaceholder: 'BitFun 正在處理中...', - streamingTapToQueue: '助手正在回覆 — 點擊輸入後續消息(將排隊)', collapsedStreamingPlaceholder: '正在回覆...', messageQueued: '已加入隊列,將在當前步驟結束後處理', imageAnalyzingPlaceholder: '正在分析圖片...', @@ -508,7 +470,6 @@ export const messages: Record = { modelAutoDesc: '自動路由到最合適的模型', modelPrimary: 'Primary 模型', modelFast: 'Fast 模型', - modelNotConfigured: '模型未配置', askQuestionCount: '{count} 個問題{suffix}', waiting: '等待中', modeAgentic: 'Agentic', diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx index 0c4857ad5..f679f7df3 100644 --- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx +++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx @@ -310,9 +310,7 @@ const CreateAgentPage: React.FC = () => {
handleReviewChange(e.target.checked)} size="small" />
@@ -324,9 +322,7 @@ const CreateAgentPage: React.FC = () => { {t('agentsOverview.form.tools')} {review - ? t('agentsOverview.form.reviewToolsHint', { - defaultValue: 'Review Sub-Agents can only use read-only tools.', - }) + ? t('agentsOverview.form.reviewToolsHint') : t('agentsOverview.form.toolsHint', { optionalLabel: t('agentsOverview.form.toolsOptional'), })} 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 1c396613a..34b7ec33d 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx +++ b/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.tsx @@ -73,7 +73,7 @@ const AssistantQuickInput: React.FC = ({ setValue(''); } catch (err) { log.error('send quick message', err); - notificationService.error(t('errors.sendFailed', { defaultValue: 'Failed to send message' })); + notificationService.error(t('errors.sendFailed')); } finally { setSending(false); } @@ -88,7 +88,7 @@ const AssistantQuickInput: React.FC = ({ }, [handleSend, isImeEnter]); const placeholder = assistantName - ? t('input.assistantPlaceholder', { name: assistantName, defaultValue: `Message ${assistantName}…` }) + ? t('input.assistantPlaceholder', { name: assistantName }) : t('input.placeholder', { defaultValue: 'Send a message…' }); return ( @@ -111,7 +111,7 @@ const AssistantQuickInput: React.FC = ({
- {t('input.sendHint', { defaultValue: 'Enter to send · Shift+Enter for new line' })} + {t('input.sendHint')}
= ({ isLoading={sending} disabled={!value.trim() || sending} onClick={() => { void handleSend(); }} - aria-label={t('actions.send', { defaultValue: 'Send' })} + aria-label={t('actions.send')} className="aqi__send" > {sending diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 3361db881..344240e1d 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -1840,7 +1840,6 @@ export const ChatInput: React.FC = ({ const requiredNames = requiredArgs.map(argument => argument.name).join(', '); notificationService.warning( t('chatInput.mcpPromptMissingArgs', { - defaultValue: 'This MCP prompt requires arguments: {{args}}', args: requiredNames, }) ); @@ -1898,7 +1897,7 @@ export const ChatInput: React.FC = ({ notificationService.error( error instanceof Error ? error.message : t('error.unknown'), { - title: t('chatInput.mcpPromptFailed', { defaultValue: 'MCP prompt failed' }), + title: t('chatInput.mcpPromptFailed'), duration: 5000, } ); @@ -2896,7 +2895,7 @@ export const ChatInput: React.FC = ({
{mcpPromptCommandsLoading && items.length === 0 ? (
- {t('chatInput.loadingMcpPrompts', { defaultValue: 'Loading MCP prompts…' })} + {t('chatInput.loadingMcpPrompts')}
) : items.length > 0 ? ( items.map((item, index) => ( diff --git a/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx b/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx index a53f7495b..4e8b59201 100644 --- a/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx +++ b/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx @@ -586,9 +586,7 @@ export const TaskDetailPanel: React.FC = ({ data }) => {
- {t('toolCards.taskDetailPanel.loadingMore', { - defaultValue: 'Loading more output...', - })} + {t('toolCards.taskDetailPanel.loadingMore')}
)} @@ -599,9 +597,7 @@ export const TaskDetailPanel: React.FC = ({ data }) => { {isSnapshotHydrated ? t('toolCards.taskDetailPanel.status.running') - : t('toolCards.taskDetailPanel.loading', { - defaultValue: 'Loading task details...', - })} + : t('toolCards.taskDetailPanel.loading')}
)} diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx index fb49015f5..1df8d2923 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx @@ -386,8 +386,8 @@ export const FlowChatHeader: React.FC = ({ variant="ghost" size="xs" onClick={handleOpenPullRequests} - tooltip={t('flowChatHeader.pullRequests', { defaultValue: 'Pull requests' })} - aria-label={t('flowChatHeader.pullRequests', { defaultValue: 'Pull requests' })} + tooltip={t('flowChatHeader.pullRequests')} + aria-label={t('flowChatHeader.pullRequests')} data-testid="flowchat-header-pull-requests" > diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx index c0fc4dd29..631edeb36 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx @@ -379,7 +379,7 @@ export const ModelRoundItem = React.memo( {hasDeferredGroups && (
- {t('modelRound.loadingMoreHistory', { defaultValue: 'Loading more history...' })} + {t('modelRound.loadingMoreHistory')}
)} 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 f657a0a3b..7126577b7 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewReportExportActions.tsx @@ -60,15 +60,15 @@ export const CodeReviewReportExportActions: React.FC ({ listeners: new Set<(state: { sessions: Map; activeSessionId: string | null }) => void>(), })); +const translations: Record = { + 'toolCards.codeReview.sections.runManifest': 'Run manifest', + 'toolCards.codeReview.runManifest.target': 'Target', + 'toolCards.codeReview.runManifest.budget': 'Budget', + 'toolCards.codeReview.runManifest.estimatedCalls': 'Estimated calls', + 'toolCards.codeReview.runManifest.activeGroupTitle': 'Will run', + 'toolCards.codeReview.runManifest.skippedGroupTitle': 'Skipped reviewers', +}; + vi.mock('react-i18next', () => ({ initReactI18next: { type: '3rdParty', @@ -24,7 +33,7 @@ vi.mock('react-i18next', () => ({ }, useTranslation: () => ({ t: (key: string, options?: Record) => { - const value = typeof options?.defaultValue === 'string' ? options.defaultValue : key; + const value = typeof options?.defaultValue === 'string' ? options.defaultValue : translations[key] ?? key; return value.replace(/\{\{(\w+)\}\}/g, (_match, name) => String(options?.[name] ?? '')); }, }), diff --git a/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx index b39402ffb..c6a006466 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx @@ -739,7 +739,7 @@ export const CodeReviewToolCard: React.FC = React.memo(({ {runManifest && ( = React.memo(({
- {t('toolCards.codeReview.runManifest.target', { defaultValue: 'Target' })} + {t('toolCards.codeReview.runManifest.target')} {formatRunManifestTarget(runManifest)}
- {t('toolCards.codeReview.runManifest.budget', { defaultValue: 'Budget' })} + {t('toolCards.codeReview.runManifest.budget')} {runManifest.tokenBudget.mode}
- {t('toolCards.codeReview.runManifest.estimatedCalls', { defaultValue: 'Estimated calls' })} + {t('toolCards.codeReview.runManifest.estimatedCalls')} {runManifest.tokenBudget.estimatedReviewerCalls}
{runManifest.strategyRecommendation && ( @@ -794,7 +794,7 @@ export const CodeReviewToolCard: React.FC = React.memo(({ {activeRunManifestReviewers.length > 0 && (
- {t('toolCards.codeReview.runManifest.activeGroupTitle', { defaultValue: 'Will run' })} + {t('toolCards.codeReview.runManifest.activeGroupTitle')}
{activeRunManifestReviewers.map((member) => ( @@ -810,7 +810,7 @@ export const CodeReviewToolCard: React.FC = React.memo(({ {runManifest.skippedReviewers.length > 0 && (
- {t('toolCards.codeReview.runManifest.skippedGroupTitle', { defaultValue: 'Skipped reviewers' })} + {t('toolCards.codeReview.runManifest.skippedGroupTitle')}
    {runManifest.skippedReviewers.map((member) => ( diff --git a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx index 1212c82f2..ec1435182 100644 --- a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx @@ -599,7 +599,7 @@ const SkillsConfig: React.FC = () => { {renderAddForm('user')} @@ -615,7 +615,7 @@ const SkillsConfig: React.FC = () => { {renderAddForm('project')} diff --git a/src/web-ui/src/infrastructure/i18n/README.md b/src/web-ui/src/infrastructure/i18n/README.md index 8548d4dc7..7eebe1063 100644 --- a/src/web-ui/src/infrastructure/i18n/README.md +++ b/src/web-ui/src/infrastructure/i18n/README.md @@ -1,102 +1,80 @@ [中文](README.zh-CN.md) | **English** -# Frontend i18n +# Web UI I18n -## Quick Start +This README is the Web UI runtime entry point. The cross-surface contract, +resource ownership, key policy, and verification rules live in: -```typescript -import { useI18n } from '@/infrastructure/i18n'; +- `docs/architecture/i18n.md` +- `docs/development/i18n.md` -const { t } = useI18n('components'); -{t('section.key')} -``` +Keep this file small so local runtime examples do not drift from the shared +project rules. -## Core API +## Runtime Usage -### `useI18n(namespace)` +Use `useI18n(namespace)` or `useTranslation(namespace)` for route, scene, and +feature UI. This lets Web UI load non-bootstrap namespaces lazily. ```typescript +import { useI18n } from '@/infrastructure/i18n'; + const { t } = useI18n('components'); t('dialog.confirm.ok'); t('session.title', { id: 123 }); ``` -Multiple namespaces: +Multiple namespaces are allowed when a component owns copy from more than one +namespace. Relative keys resolve through the first namespace, so prefer explicit +`namespace:key` strings when the key belongs to a later namespace. ```typescript -const { t } = useI18n('components'); -const { t: tSettings } = useI18n('settings'); -``` +const { t } = useI18n(['components', 'common']); -Common returns: - -```typescript -const { - t, - currentLanguage, - changeLanguage, - isReady, - formatDate, - formatNumber, -} = useI18n('components'); +t('components:dialog.confirm.ok'); +t('common:actions.cancel'); ``` -Non-React usage: +Direct `i18nService.t('namespace:key')` calls are for non-React or +module-initialization paths only. The namespace must be in +`WEB_UI_BOOTSTRAP_NAMESPACES`. ```typescript import { i18nService } from '@/infrastructure/i18n'; -i18nService.t('namespace:section.key'); +i18nService.t('common:actions.cancel'); ``` -## Locale Files - -Path: `src/web-ui/src/locales/{zh-CN,en-US}/` - -| Namespace | File | Purpose | -|----------|------|------| -| `common` | `common.json` | Shared text | -| `components` | `components.json` | UI components | -| `flow-chat` | `flow-chat.json` | Chat features | -| `settings` | `settings.json` | Settings | -| `errors` | `errors.json` | Error messages | -| `panels/*` | `panels/*.json` | Panels | -| `settings/*` | `settings/*.json` | Settings subpages | - -## Add Translations - -1. Add keys to both `locales/zh-CN/` and `locales/en-US/`: - -```json -// locales/zh-CN/components.json -{ - "myFeature": { - "title": "我的功能", - "desc": "共 {{count}} 项" - } -} - -// locales/en-US/components.json -{ - "myFeature": { - "title": "My Feature", - "desc": "{{count}} items" - } -} -``` +## Resource Ownership + +Web UI locale resources live under: + +- `src/web-ui/src/locales//**/*.json` -2. Use in components: +Supported locale directories are defined by the shared i18n contract and must +stay aligned with `ALL_NAMESPACES` in `presets/namespaceRegistry.ts`. + +Stable product, feature, mode, tool, connection-method, and status labels should +come from the explicit `shared` namespace when the meaning is the same across +surfaces: ```typescript -const { t } = useI18n('components'); -t('myFeature.title'); -t('myFeature.desc', { count: 5 }); +t('shared:features.deepReview'); ``` -## Conventions +Feature workflow copy belongs in the nearest feature namespace. Do not import +Web UI locale files from mobile-web, installer, backend, or static pages. + +## Checks + +Run the smallest matching checks: + +```bash +pnpm run i18n:audit # Web UI locale JSON/resource-only changes +pnpm run i18n:contract:test # generated contract, shared terms, or namespace-loading rules +pnpm run type-check:web # Web UI i18n runtime, hooks, or TypeScript call sites +``` -- Namespace equals filename without `.json`; nested folders use `/` -- Keys use dot notation: `section.subsection.key` -- Interpolation uses `{{variable}}` -- Keep both languages in sync +For runtime behavior changes, also run the nearest focused Web UI test. CI +covers broad builds and full suites. diff --git a/src/web-ui/src/infrastructure/i18n/README.zh-CN.md b/src/web-ui/src/infrastructure/i18n/README.zh-CN.md index c555e3bc4..80d6996e7 100644 --- a/src/web-ui/src/infrastructure/i18n/README.zh-CN.md +++ b/src/web-ui/src/infrastructure/i18n/README.zh-CN.md @@ -1,102 +1,70 @@ **中文** | [English](README.md) -# 前端国际化(i18n) +# Web UI 国际化 -## 快速使用 +本文档只作为 Web UI 运行时入口。跨形态语言契约、资源归属、key 规范和校验规则以以下文档为准: -```typescript -import { useI18n } from '@/infrastructure/i18n'; +- `docs/architecture/i18n.md` +- `docs/development/i18n.md` -const { t } = useI18n('components'); -{t('section.key')} -``` +这里保持精简,避免本地示例和项目级规范分叉。 -## 核心 API +## 运行时用法 -### `useI18n(namespace)` +路由、场景和功能 UI 应优先使用 `useI18n(namespace)` 或 +`useTranslation(namespace)`,这样非启动命名空间可以按需懒加载。 ```typescript +import { useI18n } from '@/infrastructure/i18n'; + const { t } = useI18n('components'); t('dialog.confirm.ok'); t('session.title', { id: 123 }); ``` -多命名空间: +组件确实拥有多个命名空间的文案时,可以传入 namespace 数组。相对 key 会按第一个 namespace 解析,因此引用后续 namespace 时应写完整 `namespace:key`。 ```typescript -const { t } = useI18n('components'); -const { t: tSettings } = useI18n('settings'); -``` +const { t } = useI18n(['components', 'common']); -常用返回值: - -```typescript -const { - t, - currentLanguage, - changeLanguage, - isReady, - formatDate, - formatNumber, -} = useI18n('components'); +t('components:dialog.confirm.ok'); +t('common:actions.cancel'); ``` -非 React 环境: +直接 `i18nService.t('namespace:key')` 只用于非 React 或模块初始化路径。对应 namespace 必须在 `WEB_UI_BOOTSTRAP_NAMESPACES` 中。 ```typescript import { i18nService } from '@/infrastructure/i18n'; -i18nService.t('namespace:section.key'); +i18nService.t('common:actions.cancel'); ``` -## 翻译文件 - -路径:`src/web-ui/src/locales/{zh-CN,en-US}/` - -| 命名空间 | 文件 | 用途 | -|----------|------|------| -| `common` | `common.json` | 通用文本 | -| `components` | `components.json` | UI 组件 | -| `flow-chat` | `flow-chat.json` | 聊天功能 | -| `settings` | `settings.json` | 设置页 | -| `errors` | `errors.json` | 错误信息 | -| `panels/*` | `panels/*.json` | 面板 | -| `settings/*` | `settings/*.json` | 设置子页 | - -## 添加翻译 - -1. 在 `locales/zh-CN/` 与 `locales/en-US/` 的对应 JSON 同步新增 key: - -```json -// locales/zh-CN/components.json -{ - "myFeature": { - "title": "我的功能", - "desc": "共 {{count}} 项" - } -} - -// locales/en-US/components.json -{ - "myFeature": { - "title": "My Feature", - "desc": "{{count}} items" - } -} -``` +## 资源归属 + +Web UI 的 locale 资源位于: + +- `src/web-ui/src/locales//**/*.json` -2. 在组件中使用: +支持的 locale 目录由统一 i18n contract 决定,并且必须和 +`presets/namespaceRegistry.ts` 中的 `ALL_NAMESPACES` 保持一致。 + +产品名、功能名、模式名、工具名、连接方式和状态等稳定概念,如果跨形态语义一致,应显式读取 `shared` namespace: ```typescript -const { t } = useI18n('components'); -t('myFeature.title'); -t('myFeature.desc', { count: 5 }); +t('shared:features.deepReview'); ``` -## 约定 +功能流程文案应放在最近的功能 namespace 中。不要从 mobile-web、installer、backend 或静态页面导入 Web UI locale 文件。 + +## 校验 + +按变更类型选择最小校验: + +```bash +pnpm run i18n:audit # Web UI locale JSON/resource-only 变更 +pnpm run i18n:contract:test # 生成契约、shared terms 或 namespace 加载规则变更 +pnpm run type-check:web # Web UI i18n runtime、hooks 或 TypeScript 调用点变更 +``` -- 命名空间为文件名(去掉 `.json`),子目录用 `/` 分隔 -- key 使用点号分隔:`section.subsection.key` -- 插值使用 `{{variable}}` -- 两种语言必须同步更新 +运行时行为变更还应运行最近的 Web UI 聚焦测试。广泛构建和完整套件由 CI 覆盖。 diff --git a/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts b/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts index 7f1f820d9..7fc11f8ba 100644 --- a/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts +++ b/src/web-ui/src/infrastructure/i18n/presets/namespaceRegistry.ts @@ -22,7 +22,6 @@ export const ALL_NAMESPACES = [ 'settings', 'settings/acp-agents', 'settings/agentic-tools', - 'settings/agents', 'settings/ai-features', 'settings/ai-model', 'settings/appearance', diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 72c4a0942..ae2393e94 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -63,7 +63,8 @@ "switchWorkspace": "Switch Workspace", "recentWorkspaces": "Recent Workspaces", "noRecentWorkspaces": "No Recent Workspaces", - "noWorkspaceOpen": "Select Workspace" + "noWorkspaceOpen": "Select Workspace", + "remoteConnectRequiresWorkspace": "Open a workspace before using remote control." }, "nav": { "aria": { @@ -1104,5 +1105,8 @@ "message": "Choose what happens when you click the close button: quit exits the app completely, or minimize to tray keeps it running in the background.", "quit": "Quit", "minimizeToTray": "Minimize to Tray" - } + }, + "collapse": "Collapse", + "expand": "Expand", + "retry": "Retry" } diff --git a/src/web-ui/src/locales/en-US/components.json b/src/web-ui/src/locales/en-US/components.json index 26b51ab56..440f0d2af 100644 --- a/src/web-ui/src/locales/en-US/components.json +++ b/src/web-ui/src/locales/en-US/components.json @@ -117,7 +117,8 @@ "result": "Task Result", "processing": "AI is processing task...", "defaultType": "AI Task", - "unspecifiedTask": "Unspecified task" + "unspecifiedTask": "Unspecified task", + "taskResult": "Task Result" }, "searchCard": { "grepTitle": "Text Search", @@ -632,5 +633,10 @@ "fallback": { "noCodeContent": "No code content available" } - } + }, + "header": { + "selectProjectDirectory": "Select a project directory to open" + }, + "hide": "Hide", + "show": "Show" } 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 8e33c9442..c824fe988 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -403,7 +403,9 @@ "messageTooLarge": "Message exceeds the maximum length of {{max}} characters ({{count}} provided).", "openWorkspaceFolder": "Open workspace folder", "openWorkspaceFolderFailed": "Failed to open workspace folder: {{error}}", - "spaceToActivate": "Press Space to type" + "spaceToActivate": "Press Space to type", + "assistantPlaceholder": "Message {{name}}...", + "sendHint": "Enter to send / Shift+Enter for new line" }, "workspaceStrip": { "branchTooltipUnavailable": "Not a git repository or no current branch" @@ -463,7 +465,8 @@ "modelError": "Model Error", "contextTooLong": "Context Too Long", "rateLimited": "Rate Limited", - "unknown": "Unknown error" + "unknown": "Unknown error", + "processingFailed": "Processing failed" }, "chatInput": { "addBoostTooltip": "Agent modes, image, or skills", @@ -559,7 +562,10 @@ }, "openFolder": "Open folder…", "selectWorkspaceTitle": "Select workspace directory", - "switchWorkspaceFailed": "Failed to switch workspace: {{error}}" + "switchWorkspaceFailed": "Failed to switch workspace: {{error}}", + "mcpPromptMissingArgs": "This MCP prompt requires arguments: {{args}}", + "mcpPromptFailed": "MCP prompt failed", + "loadingMcpPrompts": "Loading MCP prompts..." }, "planner": { "title": "Task Planning", @@ -885,24 +891,6 @@ "optionalReviewers_one": "{{count}} extra specialist", "optionalReviewers_other": "{{count}} extra specialists", "summaryFirstReview": "Summary-first coverage", - "targetTagLabels": { - "frontendUi": "Frontend UI", - "frontendStyle": "Frontend styles", - "frontendI18n": "Frontend i18n", - "frontendContract": "Frontend contract", - "desktopContract": "Desktop contract", - "webServerContract": "Web server contract", - "backendCore": "Backend core", - "transport": "Transport", - "apiLayer": "API layer", - "aiAdapter": "AI adapter", - "installerUi": "Installer UI", - "test": "Tests", - "docs": "Docs", - "config": "Config", - "generatedOrLock": "Generated or lockfile", - "unknown": "Unknown area" - }, "estimatedCalls": "{{count}} reviewer calls", "estimatedCalls_one": "{{count}} reviewer call", "estimatedCalls_other": "{{count}} reviewer calls", @@ -953,7 +941,8 @@ "backgroundSubagents": "Background subagents ({{count}})", "backgroundSubagentUntitled": "Background subagent", "subagentStatusProcessing": "Running", - "subagentStatusFinishing": "Finishing" + "subagentStatusFinishing": "Finishing", + "pullRequests": "Pull requests" }, "stickyTaskIndicator": { "tooltip": "Jump to current task" @@ -1039,7 +1028,8 @@ "feedbackThanks": "Thank you for your feedback!", "feedbackDevVersion": "Feedback is not available in dev version", "userLabel": "👤 User:", - "toolCallLabel": "🔧 Tool call: {{name}}" + "toolCallLabel": "🔧 Tool call: {{name}}", + "loadingMoreHistory": "Loading more history..." }, "exploreRegion": { "readFiles_one": "Read {{count}} file", @@ -1325,7 +1315,9 @@ "running": "Running", "completed": "Completed", "failed": "Failed" - } + }, + "loadingMore": "Loading more output...", + "loading": "Loading task details..." }, "timeout": { "disableTooltip": "Disable timeout", @@ -1339,29 +1331,6 @@ "failedDurationTooltipWithReason": "Failed after {{duration}}: {{reason}}", "cancelledDurationTooltip": "Cancelled after {{duration}}" }, - "template": { - "get": "Get Template:", - "getTemplate": "Get Template", - "loading": "Loading", - "loaded": "Loaded", - "templateDetails": "Template Details", - "templateId": "Template ID:", - "templateName": "Template Name:", - "contentSize": "Content Size:", - "characters": "{{count}} characters", - "templateReady": "Template Ready", - "templateReadyDesc": "Template structure loaded successfully, ready for requirements document creation", - "templateLoadFailed": "Template loading failed", - "requirementFramework": "Requirement Framework Overview", - "productVision": "Product Vision", - "businessGoals": "Business Goals", - "userPersonas": "User Personas", - "userStories": "User Stories", - "featureList": "Feature List", - "functionalSpecTemplate": "Functional Specification Template", - "nonFunctionalRequirements": "Non-functional Requirements", - "acceptanceCriteria": "Acceptance Criteria" - }, "mcp": { "confirmExecute": "Confirm Execute", "cancel": "Cancel", @@ -1458,19 +1427,6 @@ "agentType": "Agent type", "message": "Message" }, - "imageAnalysis": { - "parsingAnalysisInfo": "Parsing analysis info...", - "unknownImage": "Unknown image", - "clipboardImage": "Clipboard image", - "analyzeImageContent": "Analyze image content", - "imageAnalysis": "Image analysis", - "completed": "Completed", - "analyzing": "Analyzing", - "preparing": "Preparing", - "analysisPrompt": "Analysis prompt", - "focusAreas": "Focus areas", - "analysisResult": "Analysis result" - }, "contextCompression": { "beforeUserMessage": "Before user message", "toolBatchComplete": "Tool batch complete", @@ -1513,26 +1469,6 @@ "labelPath": "Path", "labelStats": "Stats" }, - "ideControl": { - "parsingOperation": "Parsing operation...", - "configCenter": "Config Center", - "planner": "Task Planner", - "terminal": "Terminal", - "fileViewer": "File Viewer", - "codeEditor": "Code Editor", - "navigateToPosition": "Navigate to position", - "adjustIdeLayout": "Adjust IDE layout", - "manageTab": "Manage tab", - "focusView": "Focus view", - "executeIdeOperation": "Execute IDE operation", - "operationType": "Operation type", - "panelType": "Panel type", - "configSection": "Config section", - "sessionId": "Session ID", - "filePath": "File path", - "workspacePath": "Workspace", - "panelPosition": "Panel position" - }, "linter": { "parsingParams": "Parsing parameters...", "checkFailed": "Check failed:", @@ -1655,7 +1591,12 @@ "high_risk_only": "High-risk-only", "risk_expanded": "Risk-expanded", "full_depth": "Full-depth" - } + }, + "target": "Target", + "budget": "Budget", + "estimatedCalls": "Estimated calls", + "activeGroupTitle": "Will run", + "skippedGroupTitle": "Skipped reviewers" }, "sectionItemCount": "{{count}} items", "remediationPlan": "Remediation Plan", @@ -1712,7 +1653,8 @@ "remediation": "Remediation Plan", "strengths": "Code Strengths", "team": "Code Review Team", - "coverage": "Coverage Notes" + "coverage": "Coverage Notes", + "runManifest": "Run manifest" }, "groups": { "must_fix": "Must fix", @@ -2001,5 +1943,11 @@ "injectedAt": "Injected before round {{round}}", "statusPending": "Waiting to trigger", "statusInjected": "Triggered" + }, + "errors": { + "sendFailed": "Failed to send message" + }, + "actions": { + "send": "Send" } } diff --git a/src/web-ui/src/locales/en-US/scenes/agents.json b/src/web-ui/src/locales/en-US/scenes/agents.json index 5be5de226..c09959c6f 100644 --- a/src/web-ui/src/locales/en-US/scenes/agents.json +++ b/src/web-ui/src/locales/en-US/scenes/agents.json @@ -156,7 +156,9 @@ "cancel": "Cancel", "submit": "Create", "createSuccess": "Created agent \"{{name}}\"", - "createFailed": "Create failed: " + "createFailed": "Create failed: ", + "review": "Review", + "reviewToolsHint": "Review Sub-Agents can only use read-only tools." } }, "reviewTeams": { diff --git a/src/web-ui/src/locales/en-US/settings/agents.json b/src/web-ui/src/locales/en-US/settings/agents.json deleted file mode 100644 index 1a82b355a..000000000 --- a/src/web-ui/src/locales/en-US/settings/agents.json +++ /dev/null @@ -1 +0,0 @@ -{"title":"Agents","subtitle":"Manage user-level and project-level agents","toolbar":{"searchPlaceholder":"Search Agents...","refreshTooltip":"Refresh Agent list","refreshSuccess":"Agent list refreshed","addTooltip":"Add Agent"},"filters":{"all":"All","builtin":"Built-in","user":"User","project":"Project"},"section":{"builtin":{"description":"Built-in AI sub-agents and modes."},"user":{"description":"Your custom agents."},"project":{"description":"Project-level agents for the current workspace."},"customAgents":{"title":"Custom Agents","description":"User-level and project-level agents. Switch between them using the tabs below."}},"form":{"titleEdit":"Edit Agent","titleCreate":"Add Agent","fields":{"level":"Level","levelUser":"User-level (Global)","levelProject":"Project-level (Current Workspace)","levelProjectDisabled":" - Need to open workspace first","currentWorkspace":"Current workspace: {{path}}","name":"Agent Name *","namePlaceholder":"e.g., code-writer","description":"Description *","descriptionPlaceholder":"Brief description of Agent function","tools":"Tools","toolsOptional":"optional","toolsPlaceholder":"e.g., FileReadTool, FileWriteTool, BashTool","color":"Color","colors":{"blue":"Blue","red":"Red","green":"Green","purple":"Purple","orange":"Orange","yellow":"Yellow"},"systemPrompt":"System Prompt (Markdown) *","systemPromptPlaceholder":"Enter Agent\u0027s system prompt...","readonly":"Read-only","readonlyDescription":"Read-only agents only use read tools and can run in parallel with other tasks.","enabled":"Enabled"},"actions":{"cancel":"Cancel","save":"Save","create":"Create"}},"list":{"loading":"Loading...","loadingBuiltin":"Loading built-in Sub Agents...","errorPrefix":"Failed to load: ","empty":{"noMatch":"No matching Agents found","noMatchBuiltin":"No matching built-in Sub Agents found"},"item":{"builtin":"Built-in","subAgent":"Sub-Agent","mode":"Mode","toolsCount":"{{count}} tools","editTooltip":"Edit","deleteTooltip":"Delete","toolsLabel":"Available tools:","modelLabel":"Model Config","systemPromptLabel":"System prompt:","updateTimePrefix":"Updated: "}},"messages":{"noWorkspaceForProject":"Please open a workspace first to create project-level Agent","nameRequired":"Please enter Agent name","nameFormatError":"Name must start with a letter and contain only letters, numbers, - and _","descriptionRequired":"Please enter Agent description","toolsRequired":"Please add at least one tool","contentRequired":"Please enter system prompt","updateSuccess":"Agent \"{{name}}\" updated successfully","createSuccess":"Agent \"{{name}}\" created successfully","operationFailed":"Failed to {{operation}} Agent: {{error}}","deleteSuccess":"Agent \"{{name}}\" deleted","deleteFailed":"Failed to delete: {{error}}","confirmDeleteSubagent":"Delete Agent \"{{name}}\"? The agent file will be removed and it will disappear from the list.","toggleSuccess":"Agent \"{{name}}\" {{status}}","toggleFailed":"Failed to toggle: {{error}}","subagentToggleSuccess":"Sub Agent \"{{name}}\" {{status}}","subagentToggleFailed":"Failed to toggle: {{error}}","enabled":"enabled","disabled":"disabled","update":"update","create":"create","modelUpdated":"\"{{name}}\" will use {{model}}","modelUpdateFailed":"Failed to set model","saveFailed":"Save failed"},"model":{"primary":"Primary Model","fast":"Fast Model"},"mode":{"tools":{"label":"Available tools","selectAll":"Select all","clear":"Clear","reset":"Reset to default","expand":"Expand","collapse":"Collapse","enabled":"Enabled"},"messages":{"confirmReset":"Reset this mode\u0027s tool configuration?","resetSuccess":"Tool configuration reset","resetFailed":"Reset failed","confirmSelectAll":"Enable all tools?","confirmClearAll":"Clear all tools?","allToolsEnabled":"All tools enabled","allToolsDisabled":"All tools disabled","toolEnabled":"\"{{name}}\" enabled tool: {{tool}}","toolDisabled":"\"{{name}}\" disabled tool: {{tool}}","toolToggleFailed":"Failed to toggle tool","modelUpdated":"\"{{name}}\" will use {{model}}","modelUpdateFailed":"Failed to set model"}}} \ No newline at end of file diff --git a/src/web-ui/src/locales/en-US/settings/skills.json b/src/web-ui/src/locales/en-US/settings/skills.json index 446991262..3714fe9a7 100644 --- a/src/web-ui/src/locales/en-US/settings/skills.json +++ b/src/web-ui/src/locales/en-US/settings/skills.json @@ -90,5 +90,13 @@ "marketDownloadFailed": "Failed to download: {{error}}", "enabled": "enabled", "disabled": "disabled" + }, + "section": { + "user": { + "description": "Skills installed for current user." + }, + "project": { + "description": "Project-level Skills in current workspace." + } } } diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 293e7174e..870a4dbca 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -63,7 +63,8 @@ "switchWorkspace": "切换工作区", "recentWorkspaces": "最近工作区", "noRecentWorkspaces": "暂无最近工作区", - "noWorkspaceOpen": "选择工作区" + "noWorkspaceOpen": "选择工作区", + "remoteConnectRequiresWorkspace": "请先打开工作区再使用远程控制。" }, "nav": { "aria": { @@ -1104,5 +1105,8 @@ "message": "请选择关闭操作:退出程序将完全停止运行,最小化到托盘可保持后台运行随时唤回。", "quit": "退出程序", "minimizeToTray": "最小化到托盘" - } + }, + "collapse": "折叠", + "expand": "展开", + "retry": "重试" } diff --git a/src/web-ui/src/locales/zh-CN/components.json b/src/web-ui/src/locales/zh-CN/components.json index 11b69388b..da93a9082 100644 --- a/src/web-ui/src/locales/zh-CN/components.json +++ b/src/web-ui/src/locales/zh-CN/components.json @@ -117,7 +117,8 @@ "result": "任务结果", "processing": "AI正在处理任务...", "defaultType": "AI任务", - "unspecifiedTask": "未指定任务" + "unspecifiedTask": "未指定任务", + "taskResult": "任务结果" }, "searchCard": { "grepTitle": "文本搜索", @@ -632,5 +633,10 @@ "fallback": { "noCodeContent": "无代码内容" } - } + }, + "header": { + "selectProjectDirectory": "选择要打开的项目目录" + }, + "hide": "隐藏", + "show": "显示" } 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 c647fa18a..71e7c5a8a 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -403,7 +403,9 @@ "messageTooLarge": "消息超过最大长度 {{max}} 个字符(当前 {{count}} 个字符)。", "openWorkspaceFolder": "打开工作区文件夹", "openWorkspaceFolderFailed": "打开工作区文件夹失败:{{error}}", - "spaceToActivate": "按空格键快速键入" + "spaceToActivate": "按空格键快速键入", + "assistantPlaceholder": "给 {{name}} 发送消息...", + "sendHint": "Enter 发送 / Shift+Enter 换行" }, "workspaceStrip": { "branchTooltipUnavailable": "非 Git 仓库或当前无分支" @@ -463,7 +465,8 @@ "modelError": "模型错误", "contextTooLong": "上下文过长", "rateLimited": "请求过于频繁", - "unknown": "未知错误" + "unknown": "未知错误", + "processingFailed": "处理失败" }, "chatInput": { "addBoostTooltip": "智能体模式、图片或 Skill", @@ -559,7 +562,10 @@ "usageCommandUsage": "请单独输入 /usage,不要附加其他内容。", "usageBusy": "请等待当前会话空闲后再使用 /usage。", "usageNoWorkspace": "生成用量报告需要先打开一个项目。", - "usageFailed": "用量报告生成失败" + "usageFailed": "用量报告生成失败", + "mcpPromptMissingArgs": "此 MCP 提示词需要参数:{{args}}", + "mcpPromptFailed": "MCP 提示词执行失败", + "loadingMcpPrompts": "正在加载 MCP 提示词..." }, "planner": { "title": "任务规划", @@ -885,24 +891,6 @@ "optionalReviewers_one": "{{count}} 个额外专家", "optionalReviewers_other": "{{count}} 个额外专家", "summaryFirstReview": "先摘要再覆盖", - "targetTagLabels": { - "frontendUi": "前端 UI", - "frontendStyle": "前端样式", - "frontendI18n": "前端国际化", - "frontendContract": "前端契约", - "desktopContract": "桌面契约", - "webServerContract": "Web 服务契约", - "backendCore": "后端核心", - "transport": "传输层", - "apiLayer": "API 层", - "aiAdapter": "AI 适配器", - "installerUi": "安装器 UI", - "test": "测试", - "docs": "文档", - "config": "配置", - "generatedOrLock": "生成文件或锁文件", - "unknown": "未知区域" - }, "estimatedCalls": "{{count}} 次审核调用", "estimatedCalls_one": "{{count}} 次审核调用", "estimatedCalls_other": "{{count}} 次审核调用", @@ -953,7 +941,8 @@ "backgroundSubagents": "后台子 Agent({{count}})", "backgroundSubagentUntitled": "后台子 Agent", "subagentStatusProcessing": "运行中", - "subagentStatusFinishing": "收尾中" + "subagentStatusFinishing": "收尾中", + "pullRequests": "拉取请求" }, "stickyTaskIndicator": { "tooltip": "跳转到当前任务" @@ -1039,7 +1028,8 @@ "feedbackThanks": "感谢您的反馈!", "feedbackDevVersion": "开发版本暂不提供反馈功能", "userLabel": "👤 用户:", - "toolCallLabel": "🔧 工具调用: {{name}}" + "toolCallLabel": "🔧 工具调用: {{name}}", + "loadingMoreHistory": "正在加载更多历史..." }, "exploreRegion": { "readFiles_one": "读取了 {{count}} 个文件", @@ -1325,7 +1315,9 @@ "running": "执行中", "completed": "已完成", "failed": "执行失败" - } + }, + "loadingMore": "正在加载更多输出...", + "loading": "正在加载任务详情..." }, "timeout": { "disableTooltip": "关闭超时限制", @@ -1339,29 +1331,6 @@ "failedDurationTooltipWithReason": "执行失败,耗时 {{duration}}:{{reason}}", "cancelledDurationTooltip": "已取消,耗时 {{duration}}" }, - "template": { - "get": "获取模板:", - "getTemplate": "获取模板", - "loading": "加载中", - "loaded": "已加载", - "templateDetails": "模板详情", - "templateId": "模板 ID:", - "templateName": "模板名称:", - "contentSize": "内容大小:", - "characters": "{{count}} 字符", - "templateReady": "模板已就绪", - "templateReadyDesc": "模板结构已成功加载,可用于需求文档创建", - "templateLoadFailed": "模板加载失败", - "requirementFramework": "需求框架总览", - "productVision": "产品愿景", - "businessGoals": "业务目标", - "userPersonas": "用户画像", - "userStories": "用户故事", - "featureList": "功能清单", - "functionalSpecTemplate": "功能规格模板", - "nonFunctionalRequirements": "非功能需求", - "acceptanceCriteria": "验收标准" - }, "mcp": { "confirmExecute": "确认执行", "cancel": "取消", @@ -1458,19 +1427,6 @@ "agentType": "Agent 类型", "message": "消息内容" }, - "imageAnalysis": { - "parsingAnalysisInfo": "解析分析信息中...", - "unknownImage": "未知图片", - "clipboardImage": "剪贴板图片", - "analyzeImageContent": "分析图片内容", - "imageAnalysis": "图片分析", - "completed": "已完成", - "analyzing": "正在分析", - "preparing": "准备分析", - "analysisPrompt": "分析提示", - "focusAreas": "关注点", - "analysisResult": "分析结果" - }, "contextCompression": { "beforeUserMessage": "用户消息前", "toolBatchComplete": "工具批次完成", @@ -1513,26 +1469,6 @@ "labelPath": "路径", "labelStats": "统计" }, - "ideControl": { - "parsingOperation": "解析操作中...", - "configCenter": "配置中心", - "planner": "任务规划", - "terminal": "终端", - "fileViewer": "文件浏览器", - "codeEditor": "代码编辑器", - "navigateToPosition": "导航到指定位置", - "adjustIdeLayout": "调整IDE布局", - "manageTab": "管理标签页", - "focusView": "聚焦视图", - "executeIdeOperation": "执行IDE操作", - "operationType": "操作类型", - "panelType": "面板类型", - "configSection": "配置区域", - "sessionId": "会话ID", - "filePath": "文件路径", - "workspacePath": "工作区", - "panelPosition": "面板位置" - }, "linter": { "parsingParams": "解析参数中...", "checkFailed": "检查失败:", @@ -1655,7 +1591,12 @@ "high_risk_only": "仅高风险", "risk_expanded": "风险扩展", "full_depth": "完整深度" - } + }, + "target": "目标", + "budget": "预算", + "estimatedCalls": "预计调用次数", + "activeGroupTitle": "将运行", + "skippedGroupTitle": "已跳过评审者" }, "sectionItemCount": "{{count}} 项", "remediationPlan": "修复计划", @@ -1712,7 +1653,8 @@ "remediation": "修复计划", "strengths": "代码优点", "team": "代码审核团队", - "coverage": "覆盖与置信度" + "coverage": "覆盖与置信度", + "runManifest": "运行清单" }, "groups": { "must_fix": "必须修复", @@ -2001,5 +1943,11 @@ "injectedAt": "已在第 {{round}} 轮前插入", "statusPending": "等待触发", "statusInjected": "已触发" + }, + "errors": { + "sendFailed": "发送消息失败" + }, + "actions": { + "send": "发送" } } diff --git a/src/web-ui/src/locales/zh-CN/scenes/agents.json b/src/web-ui/src/locales/zh-CN/scenes/agents.json index 96e69dfce..e50955d16 100644 --- a/src/web-ui/src/locales/zh-CN/scenes/agents.json +++ b/src/web-ui/src/locales/zh-CN/scenes/agents.json @@ -156,7 +156,9 @@ "cancel": "取消", "submit": "创建", "createSuccess": "已创建 Agent「{{name}}」", - "createFailed": "创建失败:" + "createFailed": "创建失败:", + "review": "评审", + "reviewToolsHint": "评审子代理只能使用只读工具。" } }, "reviewTeams": { diff --git a/src/web-ui/src/locales/zh-CN/settings/agents.json b/src/web-ui/src/locales/zh-CN/settings/agents.json deleted file mode 100644 index 01a9fdf63..000000000 --- a/src/web-ui/src/locales/zh-CN/settings/agents.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "title": "智能体", - "subtitle": "管理用户级和项目级智能体", - "toolbar": { - "searchPlaceholder": "搜索Agent...", - "refreshTooltip": "刷新 Agent 列表", - "refreshSuccess": "Agent 列表已刷新", - "addTooltip": "添加Agent" - }, - "filters": { - "all": "所有", - "builtin": "内置", - "user": "用户级", - "project": "项目级" - }, - "section": { - "builtin": { "description": "系统内置的 AI 子 Agent 与工作模式。" }, - "user": { "description": "当前用户自定义的 Agent。" }, - "project": { "description": "当前工作区的项目级 Agent。" }, - "customAgents": { - "title": "自定义 Agent", - "description": "用户级与项目级 Agent,通过下方标签切换。" - } - }, - "form": { - "titleEdit": "编辑Agent", - "titleCreate": "添加Agent", - "fields": { - "level": "级别", - "levelUser": "用户级(全局)", - "levelProject": "项目级(当前工作空间)", - "levelProjectDisabled": " - 需要先打开工作区", - "currentWorkspace": "当前工作区: {{path}}", - "name": "Agent名称 *", - "namePlaceholder": "例如: code-writer", - "description": "描述 *", - "descriptionPlaceholder": "简短描述Agent的功能", - "tools": "工具", - "toolsOptional": "可选", - "toolsPlaceholder": "例如: FileReadTool, FileWriteTool, BashTool", - "color": "颜色", - "colors": { - "blue": "蓝色", - "red": "红色", - "green": "绿色", - "purple": "紫色", - "orange": "橙色", - "yellow": "黄色" - }, - "systemPrompt": "系统提示词(Markdown格式)*", - "systemPromptPlaceholder": "输入Agent的系统提示词...", - "readonly": "只读", - "readonlyDescription": "只读 Agent 仅使用只读工具,可与其它任务并发执行。", - "enabled": "启用状态" - }, - "actions": { - "cancel": "取消", - "save": "保存", - "create": "创建" - } - }, - "list": { - "loading": "加载中...", - "loadingBuiltin": "加载内置 Sub-Agent...", - "errorPrefix": "加载失败: ", - "empty": { - "noMatch": "没有找到匹配的 Agent", - "noMatchBuiltin": "没有找到匹配的内置 Sub-Agent" - }, - "item": { - "builtin": "内置", - "subAgent": "Sub-Agent", - "mode": "模式", - "toolsCount": "{{count}} 工具", - "editTooltip": "编辑", - "deleteTooltip": "删除", - "toolsLabel": "可用工具:", - "modelLabel": "模型配置", - "systemPromptLabel": "系统提示词:", - "updateTimePrefix": "更新时间: " - } - }, - "messages": { - "noWorkspaceForProject": "请先打开一个工作区才能创建项目级Agent", - "nameRequired": "请输入Agent名称", - "nameFormatError": "名称必须以英文字母开头,仅允许英文字母、数字、-、_", - "descriptionRequired": "请输入Agent描述", - "toolsRequired": "请至少添加一个工具", - "contentRequired": "请输入系统提示词", - "updateSuccess": "Agent\"{{name}}\"更新成功", - "createSuccess": "Agent\"{{name}}\"创建成功", - "operationFailed": "{{operation}}Agent失败: {{error}}", - "deleteSuccess": "Agent\"{{name}}\"已删除", - "deleteFailed": "删除失败: {{error}}", - "confirmDeleteSubagent": "确定要删除 Agent「{{name}}」吗?将删除对应文件并从列表中移除。", - "toggleSuccess": "Agent\"{{name}}\"已{{status}}", - "toggleFailed": "切换状态失败: {{error}}", - "subagentToggleSuccess": "Sub Agent \"{{name}}\" 已{{status}}", - "subagentToggleFailed": "切换状态失败: {{error}}", - "enabled": "启用", - "disabled": "禁用", - "update": "更新", - "create": "创建", - "modelUpdated": "\"{{name}}\" 将使用 {{model}}", - "modelUpdateFailed": "模型设置失败", - "saveFailed": "保存失败" - }, - "model": { - "primary": "主力模型", - "fast": "快速模型" - }, - "mode": { - "tools": { - "label": "可用工具", - "selectAll": "全选", - "clear": "清空", - "reset": "重置为默认", - "expand": "展开", - "collapse": "折叠", - "enabled": "已启用" - }, - "messages": { - "confirmReset": "确定要重置该模式的工具配置吗?", - "resetSuccess": "工具配置已重置", - "resetFailed": "重置失败", - "confirmSelectAll": "确定要启用所有工具吗?", - "confirmClearAll": "确定要清空所有工具吗?", - "allToolsEnabled": "已启用所有工具", - "allToolsDisabled": "已清空所有工具", - "toolEnabled": "\"{{name}}\" 已启用工具: {{tool}}", - "toolDisabled": "\"{{name}}\" 已禁用工具: {{tool}}", - "toolToggleFailed": "工具切换失败", - "modelUpdated": "\"{{name}}\" 将使用 {{model}}", - "modelUpdateFailed": "模型设置失败" - } - } -} diff --git a/src/web-ui/src/locales/zh-CN/settings/skills.json b/src/web-ui/src/locales/zh-CN/settings/skills.json index 700e78505..8a24286a5 100644 --- a/src/web-ui/src/locales/zh-CN/settings/skills.json +++ b/src/web-ui/src/locales/zh-CN/settings/skills.json @@ -90,5 +90,13 @@ "marketDownloadFailed": "下载失败: {{error}}", "enabled": "启用", "disabled": "禁用" + }, + "section": { + "user": { + "description": "当前用户安装的 Skills。" + }, + "project": { + "description": "当前工作区中的项目级 Skills。" + } } } diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index b41615e29..fa2536aef 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -63,7 +63,8 @@ "switchWorkspace": "切換工作區", "recentWorkspaces": "最近工作區", "noRecentWorkspaces": "暫無最近工作區", - "noWorkspaceOpen": "選擇工作區" + "noWorkspaceOpen": "選擇工作區", + "remoteConnectRequiresWorkspace": "請先開啟工作區再使用遠端控制。" }, "nav": { "aria": { @@ -1104,5 +1105,8 @@ "message": "請選擇關閉操作:退出程式將完全停止執行,最小化到托盤可保持背景執行隨時喚回。", "quit": "退出程式", "minimizeToTray": "最小化到托盤" - } + }, + "collapse": "收合", + "expand": "展開", + "retry": "重試" } diff --git a/src/web-ui/src/locales/zh-TW/components.json b/src/web-ui/src/locales/zh-TW/components.json index fc9cc2ebe..df9cad6dc 100644 --- a/src/web-ui/src/locales/zh-TW/components.json +++ b/src/web-ui/src/locales/zh-TW/components.json @@ -117,7 +117,8 @@ "result": "任務結果", "processing": "AI正在處理任務...", "defaultType": "AI任務", - "unspecifiedTask": "未指定任務" + "unspecifiedTask": "未指定任務", + "taskResult": "任務結果" }, "searchCard": { "grepTitle": "文本搜索", @@ -632,5 +633,10 @@ "fallback": { "noCodeContent": "無代碼內容" } - } + }, + "header": { + "selectProjectDirectory": "選擇要開啟的專案目錄" + }, + "hide": "隱藏", + "show": "顯示" } 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 eca8be933..124f643bd 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -403,7 +403,9 @@ "messageTooLarge": "消息超過最大長度 {{max}} 個字符(當前 {{count}} 個字符)。", "openWorkspaceFolder": "打開工作區文件夾", "openWorkspaceFolderFailed": "打開工作區文件夾失敗:{{error}}", - "spaceToActivate": "按空格鍵快速鍵入" + "spaceToActivate": "按空格鍵快速鍵入", + "assistantPlaceholder": "傳訊息給 {{name}}...", + "sendHint": "Enter 傳送 / Shift+Enter 換行" }, "workspaceStrip": { "branchTooltipUnavailable": "非 Git 存放庫或目前無分支" @@ -463,7 +465,8 @@ "modelError": "模型錯誤", "contextTooLong": "上下文過長", "rateLimited": "請求過於頻繁", - "unknown": "未知錯誤" + "unknown": "未知錯誤", + "processingFailed": "處理失敗" }, "chatInput": { "addBoostTooltip": "智能體模式、圖片或 Skill", @@ -559,7 +562,10 @@ "usageCommandUsage": "請單獨輸入 /usage,不要附加其他內容。", "usageBusy": "請等待目前工作階段閒置後再使用 /usage。", "usageNoWorkspace": "產生用量報告需要先開啟一個專案。", - "usageFailed": "用量報告產生失敗" + "usageFailed": "用量報告產生失敗", + "mcpPromptMissingArgs": "此 MCP 提示詞需要參數:{{args}}", + "mcpPromptFailed": "MCP 提示詞執行失敗", + "loadingMcpPrompts": "正在載入 MCP 提示詞..." }, "planner": { "title": "任務規劃", @@ -885,24 +891,6 @@ "optionalReviewers_one": "{{count}} 個額外專家", "optionalReviewers_other": "{{count}} 個額外專家", "summaryFirstReview": "先摘要再覆蓋", - "targetTagLabels": { - "frontendUi": "前端 UI", - "frontendStyle": "前端樣式", - "frontendI18n": "前端國際化", - "frontendContract": "前端契約", - "desktopContract": "桌面契約", - "webServerContract": "Web 服務契約", - "backendCore": "後端核心", - "transport": "傳輸層", - "apiLayer": "API 層", - "aiAdapter": "AI 介接器", - "installerUi": "安裝器 UI", - "test": "測試", - "docs": "文件", - "config": "設定", - "generatedOrLock": "生成檔或鎖定檔", - "unknown": "未知區域" - }, "estimatedCalls": "{{count}} 次審核調用", "estimatedCalls_one": "{{count}} 次審核調用", "estimatedCalls_other": "{{count}} 次審核調用", @@ -953,7 +941,8 @@ "backgroundSubagents": "背景子 Agent({{count}})", "backgroundSubagentUntitled": "背景子 Agent", "subagentStatusProcessing": "運行中", - "subagentStatusFinishing": "收尾中" + "subagentStatusFinishing": "收尾中", + "pullRequests": "拉取請求" }, "stickyTaskIndicator": { "tooltip": "跳轉到當前任務" @@ -1039,7 +1028,8 @@ "feedbackThanks": "感謝您的反饋!", "feedbackDevVersion": "開發版本暫不提供反饋功能", "userLabel": "👤 用戶:", - "toolCallLabel": "🔧 工具調用: {{name}}" + "toolCallLabel": "🔧 工具調用: {{name}}", + "loadingMoreHistory": "正在載入更多歷史..." }, "exploreRegion": { "readFiles_one": "讀取了 {{count}} 個文件", @@ -1325,7 +1315,9 @@ "running": "執行中", "completed": "已完成", "failed": "執行失敗" - } + }, + "loadingMore": "正在載入更多輸出...", + "loading": "正在載入任務詳情..." }, "timeout": { "disableTooltip": "關閉超時限制", @@ -1339,29 +1331,6 @@ "failedDurationTooltipWithReason": "執行失敗,耗時 {{duration}}:{{reason}}", "cancelledDurationTooltip": "已取消,耗時 {{duration}}" }, - "template": { - "get": "獲取模板:", - "getTemplate": "獲取模板", - "loading": "加載中", - "loaded": "已加載", - "templateDetails": "模板詳情", - "templateId": "模板 ID:", - "templateName": "模板名稱:", - "contentSize": "內容大小:", - "characters": "{{count}} 字符", - "templateReady": "模板已就緒", - "templateReadyDesc": "模板結構已成功加載,可用於需求文檔創建", - "templateLoadFailed": "模板加載失敗", - "requirementFramework": "需求框架總覽", - "productVision": "產品願景", - "businessGoals": "業務目標", - "userPersonas": "用戶畫像", - "userStories": "用戶故事", - "featureList": "功能清單", - "functionalSpecTemplate": "功能規格模板", - "nonFunctionalRequirements": "非功能需求", - "acceptanceCriteria": "驗收標準" - }, "mcp": { "confirmExecute": "確認執行", "cancel": "取消", @@ -1458,19 +1427,6 @@ "agentType": "Agent 類型", "message": "消息內容" }, - "imageAnalysis": { - "parsingAnalysisInfo": "解析分析信息中...", - "unknownImage": "未知圖片", - "clipboardImage": "剪貼板圖片", - "analyzeImageContent": "分析圖片內容", - "imageAnalysis": "圖片分析", - "completed": "已完成", - "analyzing": "正在分析", - "preparing": "準備分析", - "analysisPrompt": "分析提示", - "focusAreas": "關注點", - "analysisResult": "分析結果" - }, "contextCompression": { "beforeUserMessage": "用戶消息前", "toolBatchComplete": "工具批次完成", @@ -1513,26 +1469,6 @@ "labelPath": "路徑", "labelStats": "統計" }, - "ideControl": { - "parsingOperation": "解析操作中...", - "configCenter": "配置中心", - "planner": "任務規劃", - "terminal": "終端", - "fileViewer": "文件瀏覽器", - "codeEditor": "代碼編輯器", - "navigateToPosition": "導航到指定位置", - "adjustIdeLayout": "調整IDE佈局", - "manageTab": "管理標籤頁", - "focusView": "聚焦視圖", - "executeIdeOperation": "執行IDE操作", - "operationType": "操作類型", - "panelType": "面板類型", - "configSection": "配置區域", - "sessionId": "會話ID", - "filePath": "文件路徑", - "workspacePath": "工作區", - "panelPosition": "面板位置" - }, "linter": { "parsingParams": "解析參數中...", "checkFailed": "檢查失敗:", @@ -1655,7 +1591,12 @@ "high_risk_only": "僅高風險", "risk_expanded": "風險擴展", "full_depth": "完整深度" - } + }, + "target": "目標", + "budget": "預算", + "estimatedCalls": "預計呼叫次數", + "activeGroupTitle": "將執行", + "skippedGroupTitle": "已跳過審查者" }, "sectionItemCount": "{{count}} 項", "remediationPlan": "修復計劃", @@ -1712,7 +1653,8 @@ "remediation": "修復計劃", "strengths": "代碼優點", "team": "程式碼審核團隊", - "coverage": "覆蓋與信心" + "coverage": "覆蓋與信心", + "runManifest": "執行清單" }, "groups": { "must_fix": "必須修復", @@ -2001,5 +1943,11 @@ "injectedAt": "已在第 {{round}} 輪前插入", "statusPending": "等待觸發", "statusInjected": "已觸發" + }, + "errors": { + "sendFailed": "傳送訊息失敗" + }, + "actions": { + "send": "傳送" } } diff --git a/src/web-ui/src/locales/zh-TW/scenes/agents.json b/src/web-ui/src/locales/zh-TW/scenes/agents.json index fafe105ef..4f9c5ac34 100644 --- a/src/web-ui/src/locales/zh-TW/scenes/agents.json +++ b/src/web-ui/src/locales/zh-TW/scenes/agents.json @@ -151,7 +151,9 @@ "cancel": "取消", "submit": "創建", "createSuccess": "已創建 Agent「{{name}}」", - "createFailed": "創建失敗:" + "createFailed": "創建失敗:", + "review": "審查", + "reviewToolsHint": "審查子代理只能使用唯讀工具。" } }, "teamsZone": { diff --git a/src/web-ui/src/locales/zh-TW/settings/agents.json b/src/web-ui/src/locales/zh-TW/settings/agents.json deleted file mode 100644 index 067bcbb85..000000000 --- a/src/web-ui/src/locales/zh-TW/settings/agents.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "title": "智能體", - "subtitle": "管理用戶級和項目級智能體", - "toolbar": { - "searchPlaceholder": "搜索Agent...", - "refreshTooltip": "刷新 Agent 列表", - "refreshSuccess": "Agent 列表已刷新", - "addTooltip": "添加Agent" - }, - "filters": { - "all": "所有", - "builtin": "內置", - "user": "用戶級", - "project": "項目級" - }, - "section": { - "builtin": { "description": "系統內置的 AI 子 Agent 與工作模式。" }, - "user": { "description": "當前用戶自定義的 Agent。" }, - "project": { "description": "當前工作區的項目級 Agent。" }, - "customAgents": { - "title": "自定義 Agent", - "description": "用戶級與項目級 Agent,通過下方標籤切換。" - } - }, - "form": { - "titleEdit": "編輯Agent", - "titleCreate": "添加Agent", - "fields": { - "level": "級別", - "levelUser": "用戶級(全局)", - "levelProject": "項目級(當前工作空間)", - "levelProjectDisabled": " - 需要先打開工作區", - "currentWorkspace": "當前工作區: {{path}}", - "name": "Agent名稱 *", - "namePlaceholder": "例如: code-writer", - "description": "描述 *", - "descriptionPlaceholder": "簡短描述Agent的功能", - "tools": "工具", - "toolsOptional": "可選", - "toolsPlaceholder": "例如: FileReadTool, FileWriteTool, BashTool", - "color": "顏色", - "colors": { - "blue": "藍色", - "red": "紅色", - "green": "綠色", - "purple": "紫色", - "orange": "橙色", - "yellow": "黃色" - }, - "systemPrompt": "系統提示詞(Markdown格式)*", - "systemPromptPlaceholder": "輸入Agent的系統提示詞...", - "readonly": "只讀", - "readonlyDescription": "只讀 Agent 僅使用只讀工具,可與其它任務併發執行。", - "enabled": "啟用狀態" - }, - "actions": { - "cancel": "取消", - "save": "保存", - "create": "創建" - } - }, - "list": { - "loading": "加載中...", - "loadingBuiltin": "加載內置 Sub-Agent...", - "errorPrefix": "加載失敗: ", - "empty": { - "noMatch": "沒有找到匹配的 Agent", - "noMatchBuiltin": "沒有找到匹配的內置 Sub-Agent" - }, - "item": { - "builtin": "內置", - "subAgent": "Sub-Agent", - "mode": "模式", - "toolsCount": "{{count}} 工具", - "editTooltip": "編輯", - "deleteTooltip": "刪除", - "toolsLabel": "可用工具:", - "modelLabel": "模型配置", - "systemPromptLabel": "系統提示詞:", - "updateTimePrefix": "更新時間: " - } - }, - "messages": { - "noWorkspaceForProject": "請先打開一個工作區才能創建項目級Agent", - "nameRequired": "請輸入Agent名稱", - "nameFormatError": "名稱必須以英文字母開頭,僅允許英文字母、數字、-、_", - "descriptionRequired": "請輸入Agent描述", - "toolsRequired": "請至少添加一個工具", - "contentRequired": "請輸入系統提示詞", - "updateSuccess": "Agent\"{{name}}\"更新成功", - "createSuccess": "Agent\"{{name}}\"創建成功", - "operationFailed": "{{operation}}Agent失敗: {{error}}", - "deleteSuccess": "Agent\"{{name}}\"已刪除", - "deleteFailed": "刪除失敗: {{error}}", - "confirmDeleteSubagent": "確定要刪除 Agent「{{name}}」嗎?將刪除對應文件並從列表中移除。", - "toggleSuccess": "Agent\"{{name}}\"已{{status}}", - "toggleFailed": "切換狀態失敗: {{error}}", - "subagentToggleSuccess": "Sub Agent \"{{name}}\" 已{{status}}", - "subagentToggleFailed": "切換狀態失敗: {{error}}", - "enabled": "啟用", - "disabled": "禁用", - "update": "更新", - "create": "創建", - "modelUpdated": "\"{{name}}\" 將使用 {{model}}", - "modelUpdateFailed": "模型設置失敗", - "saveFailed": "保存失敗" - }, - "model": { - "primary": "主力模型", - "fast": "快速模型" - }, - "mode": { - "tools": { - "label": "可用工具", - "selectAll": "全選", - "clear": "清空", - "reset": "重置為默認", - "expand": "展開", - "collapse": "摺疊", - "enabled": "已啟用" - }, - "messages": { - "confirmReset": "確定要重置該模式的工具配置嗎?", - "resetSuccess": "工具配置已重置", - "resetFailed": "重置失敗", - "confirmSelectAll": "確定要啟用所有工具嗎?", - "confirmClearAll": "確定要清空所有工具嗎?", - "allToolsEnabled": "已啟用所有工具", - "allToolsDisabled": "已清空所有工具", - "toolEnabled": "\"{{name}}\" 已啟用工具: {{tool}}", - "toolDisabled": "\"{{name}}\" 已禁用工具: {{tool}}", - "toolToggleFailed": "工具切換失敗", - "modelUpdated": "\"{{name}}\" 將使用 {{model}}", - "modelUpdateFailed": "模型設置失敗" - } - } -} diff --git a/src/web-ui/src/locales/zh-TW/settings/skills.json b/src/web-ui/src/locales/zh-TW/settings/skills.json index 08e12d2d6..1bc2f3d51 100644 --- a/src/web-ui/src/locales/zh-TW/settings/skills.json +++ b/src/web-ui/src/locales/zh-TW/settings/skills.json @@ -90,5 +90,13 @@ "marketDownloadFailed": "下載失敗: {{error}}", "enabled": "啟用", "disabled": "禁用" + }, + "section": { + "user": { + "description": "目前使用者安裝的 Skills。" + }, + "project": { + "description": "目前工作區中的專案級 Skills。" + } } }