From a79be46e1e2f55db3025b8e4be24ba9a2f74dc7d Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Wed, 17 Jun 2026 13:46:34 +0800 Subject: [PATCH 1/5] feat(i18n): localize tool display names in TUI and web-shell badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat-stream tool badges (TodoWrite, Shell, ReadFile…) showed raw English display names even under a Chinese UI, while the surrounding panels were already localized. Route badge rendering through a locale-aware lookup so they translate, falling back to the English display name for untranslated/unknown tools. - CLI: add localizeToolDisplayName() keyed by 'toolDisplayName.' (namespaced so tool labels never collide with same-spelled generic UI strings, e.g. a standalone 'Shell'); use it in ToolMessage, CompactToolGroupDisplay, InlineParallelAgentsDisplay, and the background-view LiveAgentPanel + BackgroundTasksDialog. Add zh + zh-TW translations and the matching en.js parity keys. - web-shell: add localizeToolDisplayName(toolName, t) keyed by 'toolName.' with English fallback; use it in ToolGroup and TasksStatusMessage; add zh-CN translations. - Tests: unit coverage for both helpers (translation, English fallback, unknown passthrough, generic-UI-key non-collision) plus a zh-locale render assertion for the ToolMessage badge. --- packages/cli/src/i18n/index.test.ts | 32 ++++++++++++++ packages/cli/src/i18n/index.ts | 13 ++++++ packages/cli/src/i18n/locales/en.js | 44 +++++++++++++++++++ packages/cli/src/i18n/locales/zh-TW.js | 43 ++++++++++++++++++ packages/cli/src/i18n/locales/zh.js | 44 +++++++++++++++++++ .../background-view/BackgroundTasksDialog.tsx | 4 +- .../background-view/LiveAgentPanel.tsx | 5 ++- .../messages/CompactToolGroupDisplay.tsx | 4 +- .../messages/InlineParallelAgentsDisplay.tsx | 5 ++- .../components/messages/ToolMessage.test.tsx | 41 +++++++++++++++++ .../ui/components/messages/ToolMessage.tsx | 3 +- .../messages/TasksStatusMessage.tsx | 12 +++-- .../client/components/messages/ToolGroup.tsx | 6 +-- .../messages/toolFormatting.test.ts | 23 ++++++++++ .../components/messages/toolFormatting.ts | 15 +++++++ .../components/messages/tools/toolDisplay.tsx | 6 ++- packages/web-shell/client/i18n.tsx | 43 ++++++++++++++++++ 17 files changed, 328 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/i18n/index.test.ts b/packages/cli/src/i18n/index.test.ts index 5c20ccb328f..64198bef699 100644 --- a/packages/cli/src/i18n/index.test.ts +++ b/packages/cli/src/i18n/index.test.ts @@ -153,3 +153,35 @@ describe('supported language resolution', () => { expect(resolveSupportedLanguage('zh-HK')).toBe('zh'); }); }); + +describe('localizeToolDisplayName', () => { + beforeEach(() => { + vi.resetModules(); + }); + + it('translates tool badges without colliding with generic UI strings', async () => { + const { setLanguageAsync, localizeToolDisplayName, t } = await import( + './index.js' + ); + await setLanguageAsync('zh'); + + // The namespaced `toolDisplayName.*` key translates the badge... + expect(localizeToolDisplayName('Shell')).toBe('终端'); + expect(localizeToolDisplayName('TodoWrite')).toBe('任务清单'); + expect(localizeToolDisplayName('Lsp')).toBe('语言服务'); + // ...while a same-spelled standalone UI string keeps its own value. + expect(t('Shell')).toBe('Shell'); + }); + + it('falls back to the English display name for untranslated tools', async () => { + const { setLanguageAsync, localizeToolDisplayName } = await import( + './index.js' + ); + await setLanguageAsync('en'); + + expect(localizeToolDisplayName('TodoWrite')).toBe('TodoWrite'); + expect(localizeToolDisplayName('Shell')).toBe('Shell'); + // An unknown tool name passes through unchanged. + expect(localizeToolDisplayName('MysteryTool')).toBe('MysteryTool'); + }); +}); diff --git a/packages/cli/src/i18n/index.ts b/packages/cli/src/i18n/index.ts index 86f91cb0112..d8eca878ee5 100644 --- a/packages/cli/src/i18n/index.ts +++ b/packages/cli/src/i18n/index.ts @@ -272,6 +272,19 @@ export function t(key: string, params?: Record): string { return interpolate(translation, params); } +/** + * Locale-aware tool display name for chat-stream badges. Looks up the + * `toolDisplayName.` key so tool labels never collide + * with same-spelled generic UI strings (e.g. a standalone "Shell" label that + * intentionally stays English). Falls back to the English display name when the + * active locale has no entry, so English and untranslated tools are unaffected. + */ +export function localizeToolDisplayName(displayName: string): string { + const key = `toolDisplayName.${displayName}`; + const translated = t(key); + return translated === key ? displayName : translated; +} + /** * Get a translation that is an array of strings. * @param key The translation key diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 181aea45fbd..47017066dba 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -8,6 +8,50 @@ // The key serves as both the translation key and the default English text export default { + // ============================================================================ + // Tool display names (chat-stream badge labels) + // ---------------------------------------------------------------------------- + // Namespaced `toolDisplayName.` keys (from core + // `ToolDisplayNames`). Per this file's key-is-default-text convention each + // English entry maps to itself; `localizeToolDisplayName` detects that + // self-mapping and returns the bare display name. Localized values live in + // zh.js / zh-TW.js; other locales fall back to the English display name. + // ============================================================================ + 'toolDisplayName.Edit': 'toolDisplayName.Edit', + 'toolDisplayName.WriteFile': 'toolDisplayName.WriteFile', + 'toolDisplayName.ReadFile': 'toolDisplayName.ReadFile', + 'toolDisplayName.Grep': 'toolDisplayName.Grep', + 'toolDisplayName.Glob': 'toolDisplayName.Glob', + 'toolDisplayName.Shell': 'toolDisplayName.Shell', + 'toolDisplayName.Shell Command': 'toolDisplayName.Shell Command', + 'toolDisplayName.TodoWrite': 'toolDisplayName.TodoWrite', + 'toolDisplayName.SaveMemory': 'toolDisplayName.SaveMemory', + 'toolDisplayName.Agent': 'toolDisplayName.Agent', + 'toolDisplayName.Skill': 'toolDisplayName.Skill', + 'toolDisplayName.EnterPlanMode': 'toolDisplayName.EnterPlanMode', + 'toolDisplayName.ExitPlanMode': 'toolDisplayName.ExitPlanMode', + 'toolDisplayName.WebFetch': 'toolDisplayName.WebFetch', + 'toolDisplayName.WebSearch': 'toolDisplayName.WebSearch', + 'toolDisplayName.ListFiles': 'toolDisplayName.ListFiles', + 'toolDisplayName.Lsp': 'toolDisplayName.Lsp', + 'toolDisplayName.AskUserQuestion': 'toolDisplayName.AskUserQuestion', + 'toolDisplayName.CronCreate': 'toolDisplayName.CronCreate', + 'toolDisplayName.CronList': 'toolDisplayName.CronList', + 'toolDisplayName.CronDelete': 'toolDisplayName.CronDelete', + 'toolDisplayName.TaskCreate': 'toolDisplayName.TaskCreate', + 'toolDisplayName.TaskUpdate': 'toolDisplayName.TaskUpdate', + 'toolDisplayName.TaskList': 'toolDisplayName.TaskList', + 'toolDisplayName.TaskStop': 'toolDisplayName.TaskStop', + 'toolDisplayName.TeamCreate': 'toolDisplayName.TeamCreate', + 'toolDisplayName.TeamDelete': 'toolDisplayName.TeamDelete', + 'toolDisplayName.SendMessage': 'toolDisplayName.SendMessage', + 'toolDisplayName.StructuredOutput': 'toolDisplayName.StructuredOutput', + 'toolDisplayName.Monitor': 'toolDisplayName.Monitor', + 'toolDisplayName.NotebookEdit': 'toolDisplayName.NotebookEdit', + 'toolDisplayName.ToolSearch': 'toolDisplayName.ToolSearch', + 'toolDisplayName.EnterWorktree': 'toolDisplayName.EnterWorktree', + 'toolDisplayName.ExitWorktree': 'toolDisplayName.ExitWorktree', + 'toolDisplayName.Workflow': 'toolDisplayName.Workflow', // ============================================================================ // Help / UI Components // ============================================================================ diff --git a/packages/cli/src/i18n/locales/zh-TW.js b/packages/cli/src/i18n/locales/zh-TW.js index 832d263b1cc..78bdee75b15 100644 --- a/packages/cli/src/i18n/locales/zh-TW.js +++ b/packages/cli/src/i18n/locales/zh-TW.js @@ -9,6 +9,49 @@ // then extensively hand-corrected for Taiwan vocabulary conventions. // This file is the authoritative source — do not overwrite with auto-generated output. export default { + // ============================================================================ + // Tool display names (chat-stream badge labels) + // ---------------------------------------------------------------------------- + // Keyed by `toolDisplayName.` (from core + // `ToolDisplayNames`); a missing key falls back to the English display name + // via `localizeToolDisplayName`. A product name (e.g. `Notebook`) is kept + // verbatim inside an otherwise-translated label. + // ============================================================================ + 'toolDisplayName.Edit': '編輯', + 'toolDisplayName.WriteFile': '寫入檔案', + 'toolDisplayName.ReadFile': '讀取檔案', + 'toolDisplayName.Grep': '搜尋', + 'toolDisplayName.Glob': '比對檔案', + 'toolDisplayName.Shell': '終端', + 'toolDisplayName.Shell Command': '終端指令', + 'toolDisplayName.TodoWrite': '任務清單', + 'toolDisplayName.SaveMemory': '儲存記憶', + 'toolDisplayName.Agent': '智慧體', + 'toolDisplayName.Skill': '技能', + 'toolDisplayName.EnterPlanMode': '進入計畫模式', + 'toolDisplayName.ExitPlanMode': '退出計畫模式', + 'toolDisplayName.WebFetch': '網路擷取', + 'toolDisplayName.WebSearch': '網路搜尋', + 'toolDisplayName.ListFiles': '列出檔案', + 'toolDisplayName.Lsp': '語言服務', + 'toolDisplayName.AskUserQuestion': '詢問使用者', + 'toolDisplayName.CronCreate': '建立定時任務', + 'toolDisplayName.CronList': '定時任務清單', + 'toolDisplayName.CronDelete': '刪除定時任務', + 'toolDisplayName.TaskCreate': '建立任務', + 'toolDisplayName.TaskUpdate': '更新任務', + 'toolDisplayName.TaskList': '任務列表', + 'toolDisplayName.TaskStop': '停止任務', + 'toolDisplayName.TeamCreate': '建立團隊', + 'toolDisplayName.TeamDelete': '刪除團隊', + 'toolDisplayName.SendMessage': '傳送訊息', + 'toolDisplayName.StructuredOutput': '結構化輸出', + 'toolDisplayName.Monitor': '監控', + 'toolDisplayName.NotebookEdit': '編輯 Notebook', + 'toolDisplayName.ToolSearch': '工具搜尋', + 'toolDisplayName.EnterWorktree': '進入工作樹', + 'toolDisplayName.ExitWorktree': '退出工作樹', + 'toolDisplayName.Workflow': '工作流程', '↑ to manage attachments': '↑ 管理附件', '← → select, Delete to remove, ↓ to exit': '← → 選擇,Delete 刪除,↓ 退出', 'Attachments: ': '附件:', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 3d96c0970e4..67f45bc3a92 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -7,6 +7,50 @@ // Chinese translations for Qwen Code CLI export default { + // ============================================================================ + // Tool display names (chat-stream badge labels) + // ---------------------------------------------------------------------------- + // Keyed by `toolDisplayName.` (from core + // `ToolDisplayNames`). The namespace prevents collisions with same-spelled + // generic UI strings (e.g. a standalone "Shell"). A missing key falls back to + // the English display name via `localizeToolDisplayName`. A product name is + // kept verbatim inside an otherwise-translated label (e.g. `Notebook`). + // ============================================================================ + 'toolDisplayName.Edit': '编辑', + 'toolDisplayName.WriteFile': '写入文件', + 'toolDisplayName.ReadFile': '读取文件', + 'toolDisplayName.Grep': '搜索', + 'toolDisplayName.Glob': '匹配文件', + 'toolDisplayName.Shell': '终端', + 'toolDisplayName.Shell Command': '终端命令', + 'toolDisplayName.TodoWrite': '任务清单', + 'toolDisplayName.SaveMemory': '保存记忆', + 'toolDisplayName.Agent': '智能体', + 'toolDisplayName.Skill': '技能', + 'toolDisplayName.EnterPlanMode': '进入计划模式', + 'toolDisplayName.ExitPlanMode': '退出计划模式', + 'toolDisplayName.WebFetch': '网络抓取', + 'toolDisplayName.WebSearch': '网络搜索', + 'toolDisplayName.ListFiles': '列出文件', + 'toolDisplayName.Lsp': '语言服务', + 'toolDisplayName.AskUserQuestion': '询问用户', + 'toolDisplayName.CronCreate': '创建定时任务', + 'toolDisplayName.CronList': '定时任务列表', + 'toolDisplayName.CronDelete': '删除定时任务', + 'toolDisplayName.TaskCreate': '创建任务', + 'toolDisplayName.TaskUpdate': '更新任务', + 'toolDisplayName.TaskList': '任务列表', + 'toolDisplayName.TaskStop': '停止任务', + 'toolDisplayName.TeamCreate': '创建团队', + 'toolDisplayName.TeamDelete': '删除团队', + 'toolDisplayName.SendMessage': '发送消息', + 'toolDisplayName.StructuredOutput': '结构化输出', + 'toolDisplayName.Monitor': '监控', + 'toolDisplayName.NotebookEdit': '编辑 Notebook', + 'toolDisplayName.ToolSearch': '工具搜索', + 'toolDisplayName.EnterWorktree': '进入工作树', + 'toolDisplayName.ExitWorktree': '退出工作树', + 'toolDisplayName.Workflow': '工作流', // ============================================================================ // Help / UI Components // ============================================================================ diff --git a/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx b/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx index 6ce74240619..24c80841017 100644 --- a/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx +++ b/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx @@ -42,7 +42,7 @@ import { type DreamDialogEntry, entryId, } from '../../hooks/useBackgroundTaskView.js'; -import { t } from '../../../i18n/index.js'; +import { localizeToolDisplayName, t } from '../../../i18n/index.js'; // `DialogEntry['status']` widens the shell status union with the agent-only // `paused` state, so dialog handlers can switch on a single combined enum. @@ -57,7 +57,7 @@ const TOOL_DISPLAY_BY_NAME: Record = Object.fromEntries( ); function formatActivityLabel(name: string, description: string | undefined) { - const display = TOOL_DISPLAY_BY_NAME[name] ?? name; + const display = localizeToolDisplayName(TOOL_DISPLAY_BY_NAME[name] ?? name); const singleLineDesc = description ? description.replace(/\s*\n\s*/g, ' ').trim() : ''; diff --git a/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx b/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx index 4ed6f73a9dd..0c0c116decf 100644 --- a/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx +++ b/packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx @@ -34,6 +34,7 @@ import { ToolDisplayNames, ToolNames, } from '@qwen-code/qwen-code-core'; +import { localizeToolDisplayName } from '../../../i18n/index.js'; import { useBackgroundTaskViewActions, useBackgroundTaskViewState, @@ -137,7 +138,9 @@ const TOOL_DISPLAY_BY_NAME: Record = Object.fromEntries( function activityLabel(entry: AgentDialogEntry): string { const last = entry.recentActivities?.at(-1); if (!last) return ''; - const display = TOOL_DISPLAY_BY_NAME[last.name] ?? last.name; + const display = localizeToolDisplayName( + TOOL_DISPLAY_BY_NAME[last.name] ?? last.name, + ); const desc = last.description?.replace(/\s*\n\s*/g, ' ').trim(); return desc ? `${display} ${desc}` : display; } diff --git a/packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx b/packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx index ea08396acf5..852a7df927e 100644 --- a/packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx +++ b/packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx @@ -11,7 +11,7 @@ import { ToolCallStatus } from '../../types.js'; import type { AnsiOutputDisplay } from '@qwen-code/qwen-code-core'; import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js'; import { theme } from '../../semantic-colors.js'; -import { t } from '../../../i18n/index.js'; +import { localizeToolDisplayName, t } from '../../../i18n/index.js'; import { ToolStatusIndicator } from '../shared/ToolStatusIndicator.js'; import { ToolElapsedTime } from '../shared/ToolElapsedTime.js'; @@ -166,7 +166,7 @@ export const CompactToolGroupDisplay: React.FC< {compactLabel ? renderSummaryHeader(compactLabel, toolCalls.length) : renderDefaultHeader( - activeTool.name, + localizeToolDisplayName(activeTool.name), activeToolDescription, toolCalls.length, )} diff --git a/packages/cli/src/ui/components/messages/InlineParallelAgentsDisplay.tsx b/packages/cli/src/ui/components/messages/InlineParallelAgentsDisplay.tsx index a444a48dc5f..c2a00987f9f 100644 --- a/packages/cli/src/ui/components/messages/InlineParallelAgentsDisplay.tsx +++ b/packages/cli/src/ui/components/messages/InlineParallelAgentsDisplay.tsx @@ -32,6 +32,7 @@ import { ConfigContext } from '../../contexts/ConfigContext.js'; import { theme } from '../../semantic-colors.js'; import { formatDuration, formatTokenCount } from '../../utils/formatters.js'; import { escapeAnsiCtrlCodes } from '../../utils/textUtils.js'; +import { localizeToolDisplayName } from '../../../i18n/index.js'; interface InlineParallelAgentsDisplayProps { toolCalls: readonly IndividualToolCallDisplay[]; @@ -108,7 +109,9 @@ function activityLabel(row: RowData): string { // registry's live array. const last = row.recentActivity; if (!last) return ''; - const display = TOOL_DISPLAY_BY_NAME[last.name] ?? last.name; + const display = localizeToolDisplayName( + TOOL_DISPLAY_BY_NAME[last.name] ?? last.name, + ); const desc = last.description?.replace(/\s*\n\s*/g, ' ').trim(); return desc ? `${display} ${desc}` : display; } diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index d9949cc851e..8056ac85e18 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -930,3 +930,44 @@ describe('', () => { expect(output).toContain('- Step 2'); }); }); + +describe(' localized badge', () => { + const localizedProps: ToolMessageProps = { + callId: 'tool-i18n', + name: 'ReadFile', + description: '', + resultDisplay: '', + status: ToolCallStatus.Success, + contentWidth: 80, + confirmationDetails: undefined, + emphasis: 'medium', + config: {} as Config, + }; + + afterEach(async () => { + const { setLanguageAsync } = await import('../../../i18n/index.js'); + await setLanguageAsync('en'); + }); + + it('shows the localized display name under the zh locale', async () => { + const { setLanguageAsync } = await import('../../../i18n/index.js'); + await setLanguageAsync('zh'); + const { lastFrame } = renderWithContext( + , + StreamingState.Idle, + ); + const output = lastFrame() ?? ''; + expect(output).toContain('读取文件'); + expect(output).not.toContain('ReadFile'); + }); + + it('keeps the English display name under the en locale', async () => { + const { setLanguageAsync } = await import('../../../i18n/index.js'); + await setLanguageAsync('en'); + const { lastFrame } = renderWithContext( + , + StreamingState.Idle, + ); + expect(lastFrame() ?? '').toContain('ReadFile'); + }); +}); diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index da4ee28a3a1..4d8ba15319b 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -28,6 +28,7 @@ import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; import { PlanSummaryDisplay } from '../PlanSummaryDisplay.js'; import { ShellInputPrompt } from '../ShellInputPrompt.js'; import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js'; +import { localizeToolDisplayName } from '../../../i18n/index.js'; import { formatDuration, formatTokenCount } from '../../utils/formatters.js'; import { theme } from '../../semantic-colors.js'; import { useSettings } from '../../contexts/SettingsContext.js'; @@ -800,7 +801,7 @@ const ToolInfo: React.FC = ({ strikethrough={status === ToolCallStatus.Canceled} > - {name} + {localizeToolDisplayName(name)} {' '} {description} diff --git a/packages/web-shell/client/components/messages/TasksStatusMessage.tsx b/packages/web-shell/client/components/messages/TasksStatusMessage.tsx index d4f1f07d1fb..57dc39abab6 100644 --- a/packages/web-shell/client/components/messages/TasksStatusMessage.tsx +++ b/packages/web-shell/client/components/messages/TasksStatusMessage.tsx @@ -8,7 +8,7 @@ import { useDelayedGlobalKeyDown } from '../../hooks/useDelayedGlobalKeyDown'; import { useI18n } from '../../i18n'; import { formatRuntime } from '../../utils/formatRuntime'; import { createSentinelSerializer } from '../../utils/sentinelMessage'; -import { formatToolDisplayName } from './toolFormatting'; +import { localizeToolDisplayName } from './toolFormatting'; import styles from './TasksStatusMessage.module.css'; const ACTIVE_EVENT = 'web-shell:tasks-panel-active'; @@ -147,8 +147,12 @@ function windowTasks( }; } -function formatActivityLabel(name: string, description: string | undefined) { - const display = formatToolDisplayName(name); +function formatActivityLabel( + name: string, + description: string | undefined, + t: ReturnType['t'], +) { + const display = localizeToolDisplayName(name, t); const singleLineDescription = description ? description.replace(/\s*\n\s*/g, ' ').trim() : ''; @@ -634,7 +638,7 @@ function TaskDetail({ {task.recentActivities.slice(-5).map((a, i, arr) => { const isLast = i === arr.length - 1; - const desc = formatActivityLabel(a.name, a.description); + const desc = formatActivityLabel(a.name, a.description, t); return (
): ACPToolCall { return { @@ -190,4 +192,25 @@ describe('toolFormatting', () => { expect(result.length).toBeLessThan(5000); expect(result.endsWith('...')).toBe(true); }); + + describe('localizeToolDisplayName', () => { + it('translates known tool names in Chinese', () => { + const t = getTranslator('zh-CN'); + expect(localizeToolDisplayName('todo_write', t)).toBe('任务清单'); + expect(localizeToolDisplayName('grep_search', t)).toBe('搜索'); + expect(localizeToolDisplayName('lsp', t)).toBe('语言服务'); + }); + + it('falls back to the English display name when the locale has no entry', () => { + const t = getTranslator('en'); + expect(localizeToolDisplayName('todo_write', t)).toBe('TodoWrite'); + expect(localizeToolDisplayName('grep_search', t)).toBe('Grep'); + }); + + it('falls back to the raw wire name for unknown tools', () => { + expect( + localizeToolDisplayName('mystery_tool', getTranslator('zh-CN')), + ).toBe('mystery_tool'); + }); + }); }); diff --git a/packages/web-shell/client/components/messages/toolFormatting.ts b/packages/web-shell/client/components/messages/toolFormatting.ts index e18d956db8e..49320421046 100644 --- a/packages/web-shell/client/components/messages/toolFormatting.ts +++ b/packages/web-shell/client/components/messages/toolFormatting.ts @@ -39,6 +39,21 @@ export function formatToolDisplayName(toolName: string): string { return TOOL_DISPLAY_NAMES[toolName] ?? toolName; } +/** + * Locale-aware tool display name for chat-stream badges. Looks up the + * `toolName.` i18n key; when the active language has no entry the + * translator returns the key verbatim, in which case we fall back to the + * English {@link formatToolDisplayName}. Pass the `t` from `useI18n()`. + */ +export function localizeToolDisplayName( + toolName: string, + t: (key: string, vars?: Record) => string, +): string { + const key = `toolName.${toolName}`; + const translated = t(key); + return translated === key ? formatToolDisplayName(toolName) : translated; +} + export function isAskUserQuestionToolName(toolName: string): boolean { const normalized = toolName.toLowerCase(); return normalized === 'ask_user_question' || normalized === 'askuserquestion'; diff --git a/packages/web-shell/client/components/messages/tools/toolDisplay.tsx b/packages/web-shell/client/components/messages/tools/toolDisplay.tsx index bab651267e3..fe79f724234 100644 --- a/packages/web-shell/client/components/messages/tools/toolDisplay.tsx +++ b/packages/web-shell/client/components/messages/tools/toolDisplay.tsx @@ -1,5 +1,9 @@ import styles from './ToolChrome.module.css'; -export { formatToolDisplayName, truncateText } from '../toolFormatting'; +export { + formatToolDisplayName, + localizeToolDisplayName, + truncateText, +} from '../toolFormatting'; export function StatusIcon({ status }: { status: string }) { switch (status) { diff --git a/packages/web-shell/client/i18n.tsx b/packages/web-shell/client/i18n.tsx index 0e68a5d4936..bda16900713 100644 --- a/packages/web-shell/client/i18n.tsx +++ b/packages/web-shell/client/i18n.tsx @@ -884,6 +884,49 @@ const EN: Messages = { const ZH: Messages = { ...EN, + // Tool display names (chat-stream badge labels). Keyed by `toolName.`; + // a wire name with no entry here falls back to the English display name via + // `localizeToolDisplayName`. A product name (e.g. `Notebook`) stays verbatim. + 'toolName.edit': '编辑', + 'toolName.write_file': '写入文件', + 'toolName.read_file': '读取文件', + 'toolName.grep_search': '搜索', + 'toolName.glob': '匹配文件', + 'toolName.run_shell_command': '终端', + 'toolName.todo_write': '任务清单', + 'toolName.save_memory': '保存记忆', + 'toolName.agent': '智能体', + 'toolName.skill': '技能', + 'toolName.enter_plan_mode': '进入计划模式', + 'toolName.exit_plan_mode': '退出计划模式', + 'toolName.web_fetch': '网络抓取', + 'toolName.web_search': '网络搜索', + 'toolName.list_directory': '列出文件', + 'toolName.lsp': '语言服务', + 'toolName.ask_user_question': '询问用户', + 'toolName.cron_create': '创建定时任务', + 'toolName.cron_list': '定时任务列表', + 'toolName.cron_delete': '删除定时任务', + 'toolName.task_create': '创建任务', + 'toolName.task_update': '更新任务', + 'toolName.task_list': '任务列表', + 'toolName.task_stop': '停止任务', + 'toolName.team_create': '创建团队', + 'toolName.team_delete': '删除团队', + 'toolName.send_message': '发送消息', + 'toolName.structured_output': '结构化输出', + 'toolName.monitor': '监控', + 'toolName.notebook_edit': '编辑 Notebook', + 'toolName.tool_search': '工具搜索', + 'toolName.enter_worktree': '进入工作树', + 'toolName.exit_worktree': '退出工作树', + 'toolName.workflow': '工作流', + // web-shell-only wire aliases (see TOOL_DISPLAY_NAMES in toolFormatting.ts) + 'toolName.bash': '终端', + 'toolName.shell': '终端命令', + 'toolName.read': '读取文件', + 'toolName.write': '写入文件', + 'toolName.search': '搜索', 'about.auth': '认证', 'about.baseUrl': 'Base URL', 'about.fastModel': '快速模型', From dc70552eca1e822006a26dea24382f2eb1615097 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Wed, 17 Jun 2026 20:47:03 +0800 Subject: [PATCH 2/5] fix(i18n): English fallback for 7 web-shell tools + zh completeness guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review of #5220: 7 wire names (enter_plan_mode, task_create/update/list, team_create/delete, workflow) had zh translations but were absent from web-shell's English TOOL_DISPLAY_NAMES, so English users saw raw wire names. Add the English entries to keep the fallback map aligned. Also add a CLI test that iterates core ToolDisplayNames and asserts each has a zh toolDisplayName.* entry — check-i18n can't catch this because the keys are built dynamically, not as t() string literals. --- packages/cli/src/i18n/index.test.ts | 17 +++++++++++++++++ .../components/messages/toolFormatting.ts | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/packages/cli/src/i18n/index.test.ts b/packages/cli/src/i18n/index.test.ts index 64198bef699..dbb37f7c87a 100644 --- a/packages/cli/src/i18n/index.test.ts +++ b/packages/cli/src/i18n/index.test.ts @@ -184,4 +184,21 @@ describe('localizeToolDisplayName', () => { // An unknown tool name passes through unchanged. expect(localizeToolDisplayName('MysteryTool')).toBe('MysteryTool'); }); + + it('has a zh translation for every core tool display name', async () => { + const { setLanguageAsync, localizeToolDisplayName } = await import( + './index.js' + ); + const { ToolDisplayNames } = await import('@qwen-code/qwen-code-core'); + await setLanguageAsync('zh'); + + // Guards against a new tool landing without a `toolDisplayName.*` entry: + // every English display name must resolve to a different (translated) zh + // string. check-i18n can't catch this because the keys are built + // dynamically, never as `t('toolDisplayName.X')` string literals. + const untranslated = Object.values(ToolDisplayNames).filter( + (name) => localizeToolDisplayName(name) === name, + ); + expect(untranslated).toEqual([]); + }); }); diff --git a/packages/web-shell/client/components/messages/toolFormatting.ts b/packages/web-shell/client/components/messages/toolFormatting.ts index 49320421046..5d8cd03d2f9 100644 --- a/packages/web-shell/client/components/messages/toolFormatting.ts +++ b/packages/web-shell/client/components/messages/toolFormatting.ts @@ -27,6 +27,13 @@ const TOOL_DISPLAY_NAMES: Record = { tool_search: 'ToolSearch', enter_worktree: 'EnterWorktree', exit_worktree: 'ExitWorktree', + enter_plan_mode: 'EnterPlanMode', + task_create: 'TaskCreate', + task_update: 'TaskUpdate', + task_list: 'TaskList', + team_create: 'TeamCreate', + team_delete: 'TeamDelete', + workflow: 'Workflow', web_search: 'WebSearch', bash: 'Shell', shell: 'Shell Command', From 8dccb170cb5541ba736c3e0722bd476c20232607 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Thu, 18 Jun 2026 03:43:03 +0800 Subject: [PATCH 3/5] fix(i18n): localize remaining web-shell tool-name render sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses ci-bot review of #5220 — three more call sites rendered raw tool names: AskUserQuestion's hardcoded 'AskUserQuestion' label, getAgentCurrentToolHint (parallel-agent activity hint in ToolGroup + ParallelAgentsGroup), and the StatsMessage tool table. Route all through localizeToolDisplayName, plus the SubAgentPanel sub-tool rows found in the same sweep. localizeToolDisplayName degrades to the English display name (then the raw name), so English output is unchanged or improved (raw wire names in the stats table now resolve to display names). --- .../client/components/messages/AskUserQuestion.tsx | 5 ++++- .../web-shell/client/components/messages/StatsMessage.tsx | 3 ++- .../web-shell/client/components/messages/ToolGroup.tsx | 2 +- .../web-shell/client/components/messages/toolFormatting.ts | 7 +++++-- .../components/messages/tools/ParallelAgentsGroup.tsx | 2 +- .../client/components/messages/tools/SubAgentPanel.tsx | 6 +++++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/web-shell/client/components/messages/AskUserQuestion.tsx b/packages/web-shell/client/components/messages/AskUserQuestion.tsx index 8524449a167..40398cc37d5 100644 --- a/packages/web-shell/client/components/messages/AskUserQuestion.tsx +++ b/packages/web-shell/client/components/messages/AskUserQuestion.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import type { PermissionRequest } from '../../adapters/types'; import { useI18n } from '../../i18n'; +import { localizeToolDisplayName } from './toolFormatting'; import styles from './AskUserQuestion.module.css'; interface Question { @@ -391,7 +392,9 @@ export function AskUserQuestion({ request, onConfirm }: AskUserQuestionProps) { {/* Header line like CLI */}
? - AskUserQuestion + + {localizeToolDisplayName('ask_user_question', t)} + {t('askUser.title', { count: questions.length })} diff --git a/packages/web-shell/client/components/messages/StatsMessage.tsx b/packages/web-shell/client/components/messages/StatsMessage.tsx index 4639d537bb8..d08cc9b2958 100644 --- a/packages/web-shell/client/components/messages/StatsMessage.tsx +++ b/packages/web-shell/client/components/messages/StatsMessage.tsx @@ -4,6 +4,7 @@ import type { DaemonSessionStatsToolByName, } from '@qwen-code/webui/daemon-react-sdk'; import { useI18n } from '../../i18n'; +import { localizeToolDisplayName } from './toolFormatting'; import styles from './StatsMessage.module.css'; const SENTINEL = 'web-shell:session-stats:v1:'; @@ -463,7 +464,7 @@ function ToolStatsCard({ status }: { status: DaemonSessionStatsStatus }) { return (
- {e.name} + {localizeToolDisplayName(e.name, t)} {e.stats.count} ) => string, +): string { if (agent.status !== 'in_progress') return ''; const subs = agent.subTools; if (!subs || subs.length === 0) return ''; const last = subs[subs.length - 1]; if (last.status !== 'in_progress' && last.status !== 'pending') return ''; - let hint = last.toolName; + let hint = localizeToolDisplayName(last.toolName ?? '', t); if (last.title) { const colonIdx = last.title.indexOf(': '); hint += ' ' + (colonIdx > 0 ? last.title.slice(colonIdx + 2) : last.title); diff --git a/packages/web-shell/client/components/messages/tools/ParallelAgentsGroup.tsx b/packages/web-shell/client/components/messages/tools/ParallelAgentsGroup.tsx index 4bb44f6734e..baf12a501be 100644 --- a/packages/web-shell/client/components/messages/tools/ParallelAgentsGroup.tsx +++ b/packages/web-shell/client/components/messages/tools/ParallelAgentsGroup.tsx @@ -104,7 +104,7 @@ export function ParallelAgentsGroup({ {agents.map((agent) => { const agentType = getAgentType(agent); const desc = getAgentDescription(agent); - const toolHint = getAgentCurrentToolHint(agent); + const toolHint = getAgentCurrentToolHint(agent, t); const stats = getAgentStats(agent, now); const status = getAgentDisplayStatus(agent); const isExpanded = expandedId === agent.callId; diff --git a/packages/web-shell/client/components/messages/tools/SubAgentPanel.tsx b/packages/web-shell/client/components/messages/tools/SubAgentPanel.tsx index 3349fb0b47c..017b21ff9a0 100644 --- a/packages/web-shell/client/components/messages/tools/SubAgentPanel.tsx +++ b/packages/web-shell/client/components/messages/tools/SubAgentPanel.tsx @@ -26,6 +26,7 @@ import { formatTokenCount, getAgentType, getAgentDescription, + localizeToolDisplayName, } from '../toolFormatting'; import chromeStyles from './ToolChrome.module.css'; import styles from './SubAgentPanel.module.css'; @@ -109,12 +110,15 @@ const SubToolLine = memo(function SubToolLine({ tool }: { tool: ACPToolCall }) { }); function TaskToolCallLine({ tc }: { tc: TaskToolCall }) { + const { t } = useI18n(); const desc = tc.description || ''; return (
- {tc.name} + + {localizeToolDisplayName(tc.name, t)} + {desc && ( {truncateText(desc, 70)} )} From a6156f87f35556058e298b6074a50686ba3b2235 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Thu, 18 Jun 2026 08:43:45 +0800 Subject: [PATCH 4/5] test(i18n): web-shell tool-name completeness + agent-hint coverage Addresses ci-bot review of #5220: mirror the CLI completeness test on the web-shell side (iterate TOOL_DISPLAY_NAMES, assert every tool resolves to a zh translation) and add a getAgentCurrentToolHint test verifying it localizes the sub-tool wire name (zh + English). Export TOOL_DISPLAY_NAMES for the test. --- .../messages/toolFormatting.test.ts | 27 +++++++++++++++++++ .../components/messages/toolFormatting.ts | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/web-shell/client/components/messages/toolFormatting.test.ts b/packages/web-shell/client/components/messages/toolFormatting.test.ts index f308412e12b..a180494e68e 100644 --- a/packages/web-shell/client/components/messages/toolFormatting.test.ts +++ b/packages/web-shell/client/components/messages/toolFormatting.test.ts @@ -2,9 +2,11 @@ import { describe, expect, it } from 'vitest'; import type { ACPToolCall } from '../../adapters/types'; import { formatToolDisplayName, + getAgentCurrentToolHint, getToolDescription, getToolResultSummary, localizeToolDisplayName, + TOOL_DISPLAY_NAMES, } from './toolFormatting'; import { getTranslator } from '../../i18n'; @@ -212,5 +214,30 @@ describe('toolFormatting', () => { localizeToolDisplayName('mystery_tool', getTranslator('zh-CN')), ).toBe('mystery_tool'); }); + + it('has a zh translation for every tool in the display-name map', () => { + const tZh = getTranslator('zh-CN'); + const untranslated = Object.keys(TOOL_DISPLAY_NAMES).filter( + (wire) => + localizeToolDisplayName(wire, tZh) === formatToolDisplayName(wire), + ); + expect(untranslated).toEqual([]); + }); + + it('localizes the tool name in the agent activity hint', () => { + const agent = tool({ + toolName: 'agent', + status: 'in_progress', + subTools: [ + tool({ toolName: 'run_shell_command', status: 'in_progress' }), + ], + }); + expect(getAgentCurrentToolHint(agent, getTranslator('zh-CN'))).toContain( + '终端', + ); + expect(getAgentCurrentToolHint(agent, getTranslator('en'))).toContain( + 'Shell', + ); + }); }); }); diff --git a/packages/web-shell/client/components/messages/toolFormatting.ts b/packages/web-shell/client/components/messages/toolFormatting.ts index 73762b8995c..d05fa7e7be7 100644 --- a/packages/web-shell/client/components/messages/toolFormatting.ts +++ b/packages/web-shell/client/components/messages/toolFormatting.ts @@ -1,6 +1,6 @@ import type { ACPToolCall } from '../../adapters/types'; -const TOOL_DISPLAY_NAMES: Record = { +export const TOOL_DISPLAY_NAMES: Record = { edit: 'Edit', write_file: 'WriteFile', read_file: 'ReadFile', From 0122df44b43ce0cda823a74ac19c9106415b703c Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Thu, 18 Jun 2026 10:43:35 +0800 Subject: [PATCH 5/5] i18n: refine tool-name translations for accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review of the Chinese badge labels: keep proper tool names / acronyms in English (Agent, Grep, Glob, LSP); fix Shell (was 终端/terminal, which is the device not the interpreter) to 运行命令 and Shell Command to Shell 命令; keep the git term in Enter/ExitWorktree (进入/退出 Worktree) instead of the awkward 工作树. Applies to zh.js, zh-TW.js and web-shell zh-CN; tests and the completeness guards updated with the intentionally-English allowlist. --- packages/cli/src/i18n/index.test.ts | 19 ++++++++++----- packages/cli/src/i18n/locales/zh-TW.js | 16 ++++++------- packages/cli/src/i18n/locales/zh.js | 21 +++++++++-------- .../messages/toolFormatting.test.ts | 17 +++++++++++--- packages/web-shell/client/i18n.tsx | 23 ++++++++++--------- 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/packages/cli/src/i18n/index.test.ts b/packages/cli/src/i18n/index.test.ts index dbb37f7c87a..2e67b31ef4d 100644 --- a/packages/cli/src/i18n/index.test.ts +++ b/packages/cli/src/i18n/index.test.ts @@ -166,9 +166,13 @@ describe('localizeToolDisplayName', () => { await setLanguageAsync('zh'); // The namespaced `toolDisplayName.*` key translates the badge... - expect(localizeToolDisplayName('Shell')).toBe('终端'); + expect(localizeToolDisplayName('Shell')).toBe('运行命令'); expect(localizeToolDisplayName('TodoWrite')).toBe('任务清单'); - expect(localizeToolDisplayName('Lsp')).toBe('语言服务'); + // Proper tool names / acronyms are intentionally kept in English. + expect(localizeToolDisplayName('Agent')).toBe('Agent'); + expect(localizeToolDisplayName('Grep')).toBe('Grep'); + expect(localizeToolDisplayName('Glob')).toBe('Glob'); + expect(localizeToolDisplayName('Lsp')).toBe('LSP'); // ...while a same-spelled standalone UI string keeps its own value. expect(t('Shell')).toBe('Shell'); }); @@ -193,11 +197,14 @@ describe('localizeToolDisplayName', () => { await setLanguageAsync('zh'); // Guards against a new tool landing without a `toolDisplayName.*` entry: - // every English display name must resolve to a different (translated) zh - // string. check-i18n can't catch this because the keys are built - // dynamically, never as `t('toolDisplayName.X')` string literals. + // every English display name (except the intentionally-English ones below) + // must resolve to a different (translated) zh string. check-i18n can't catch + // this because the keys are built dynamically, never as + // `t('toolDisplayName.X')` string literals. + const KEEP_ENGLISH = new Set(['Agent', 'Grep', 'Glob']); const untranslated = Object.values(ToolDisplayNames).filter( - (name) => localizeToolDisplayName(name) === name, + (name) => + !KEEP_ENGLISH.has(name) && localizeToolDisplayName(name) === name, ); expect(untranslated).toEqual([]); }); diff --git a/packages/cli/src/i18n/locales/zh-TW.js b/packages/cli/src/i18n/locales/zh-TW.js index 78bdee75b15..c2a75e008ba 100644 --- a/packages/cli/src/i18n/locales/zh-TW.js +++ b/packages/cli/src/i18n/locales/zh-TW.js @@ -20,20 +20,20 @@ export default { 'toolDisplayName.Edit': '編輯', 'toolDisplayName.WriteFile': '寫入檔案', 'toolDisplayName.ReadFile': '讀取檔案', - 'toolDisplayName.Grep': '搜尋', - 'toolDisplayName.Glob': '比對檔案', - 'toolDisplayName.Shell': '終端', - 'toolDisplayName.Shell Command': '終端指令', + 'toolDisplayName.Grep': 'Grep', + 'toolDisplayName.Glob': 'Glob', + 'toolDisplayName.Shell': '運行命令', + 'toolDisplayName.Shell Command': 'Shell 命令', 'toolDisplayName.TodoWrite': '任務清單', 'toolDisplayName.SaveMemory': '儲存記憶', - 'toolDisplayName.Agent': '智慧體', + 'toolDisplayName.Agent': 'Agent', 'toolDisplayName.Skill': '技能', 'toolDisplayName.EnterPlanMode': '進入計畫模式', 'toolDisplayName.ExitPlanMode': '退出計畫模式', 'toolDisplayName.WebFetch': '網路擷取', 'toolDisplayName.WebSearch': '網路搜尋', 'toolDisplayName.ListFiles': '列出檔案', - 'toolDisplayName.Lsp': '語言服務', + 'toolDisplayName.Lsp': 'LSP', 'toolDisplayName.AskUserQuestion': '詢問使用者', 'toolDisplayName.CronCreate': '建立定時任務', 'toolDisplayName.CronList': '定時任務清單', @@ -49,8 +49,8 @@ export default { 'toolDisplayName.Monitor': '監控', 'toolDisplayName.NotebookEdit': '編輯 Notebook', 'toolDisplayName.ToolSearch': '工具搜尋', - 'toolDisplayName.EnterWorktree': '進入工作樹', - 'toolDisplayName.ExitWorktree': '退出工作樹', + 'toolDisplayName.EnterWorktree': '進入 Worktree', + 'toolDisplayName.ExitWorktree': '退出 Worktree', 'toolDisplayName.Workflow': '工作流程', '↑ to manage attachments': '↑ 管理附件', '← → select, Delete to remove, ↓ to exit': '← → 選擇,Delete 刪除,↓ 退出', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 67f45bc3a92..a79037d51d1 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -13,26 +13,27 @@ export default { // Keyed by `toolDisplayName.` (from core // `ToolDisplayNames`). The namespace prevents collisions with same-spelled // generic UI strings (e.g. a standalone "Shell"). A missing key falls back to - // the English display name via `localizeToolDisplayName`. A product name is - // kept verbatim inside an otherwise-translated label (e.g. `Notebook`). + // the English display name via `localizeToolDisplayName`. Proper tool names / + // acronyms are kept in English (Agent, Grep, Glob, LSP), as is a product name + // inside an otherwise-translated label (e.g. `Notebook`). // ============================================================================ 'toolDisplayName.Edit': '编辑', 'toolDisplayName.WriteFile': '写入文件', 'toolDisplayName.ReadFile': '读取文件', - 'toolDisplayName.Grep': '搜索', - 'toolDisplayName.Glob': '匹配文件', - 'toolDisplayName.Shell': '终端', - 'toolDisplayName.Shell Command': '终端命令', + 'toolDisplayName.Grep': 'Grep', + 'toolDisplayName.Glob': 'Glob', + 'toolDisplayName.Shell': '运行命令', + 'toolDisplayName.Shell Command': 'Shell 命令', 'toolDisplayName.TodoWrite': '任务清单', 'toolDisplayName.SaveMemory': '保存记忆', - 'toolDisplayName.Agent': '智能体', + 'toolDisplayName.Agent': 'Agent', 'toolDisplayName.Skill': '技能', 'toolDisplayName.EnterPlanMode': '进入计划模式', 'toolDisplayName.ExitPlanMode': '退出计划模式', 'toolDisplayName.WebFetch': '网络抓取', 'toolDisplayName.WebSearch': '网络搜索', 'toolDisplayName.ListFiles': '列出文件', - 'toolDisplayName.Lsp': '语言服务', + 'toolDisplayName.Lsp': 'LSP', 'toolDisplayName.AskUserQuestion': '询问用户', 'toolDisplayName.CronCreate': '创建定时任务', 'toolDisplayName.CronList': '定时任务列表', @@ -48,8 +49,8 @@ export default { 'toolDisplayName.Monitor': '监控', 'toolDisplayName.NotebookEdit': '编辑 Notebook', 'toolDisplayName.ToolSearch': '工具搜索', - 'toolDisplayName.EnterWorktree': '进入工作树', - 'toolDisplayName.ExitWorktree': '退出工作树', + 'toolDisplayName.EnterWorktree': '进入 Worktree', + 'toolDisplayName.ExitWorktree': '退出 Worktree', 'toolDisplayName.Workflow': '工作流', // ============================================================================ // Help / UI Components diff --git a/packages/web-shell/client/components/messages/toolFormatting.test.ts b/packages/web-shell/client/components/messages/toolFormatting.test.ts index a180494e68e..77ed5c0f52d 100644 --- a/packages/web-shell/client/components/messages/toolFormatting.test.ts +++ b/packages/web-shell/client/components/messages/toolFormatting.test.ts @@ -199,8 +199,16 @@ describe('toolFormatting', () => { it('translates known tool names in Chinese', () => { const t = getTranslator('zh-CN'); expect(localizeToolDisplayName('todo_write', t)).toBe('任务清单'); - expect(localizeToolDisplayName('grep_search', t)).toBe('搜索'); - expect(localizeToolDisplayName('lsp', t)).toBe('语言服务'); + expect(localizeToolDisplayName('run_shell_command', t)).toBe('运行命令'); + expect(localizeToolDisplayName('read_file', t)).toBe('读取文件'); + }); + + it('keeps proper tool names / acronyms in English', () => { + const t = getTranslator('zh-CN'); + expect(localizeToolDisplayName('agent', t)).toBe('Agent'); + expect(localizeToolDisplayName('grep_search', t)).toBe('Grep'); + expect(localizeToolDisplayName('glob', t)).toBe('Glob'); + expect(localizeToolDisplayName('lsp', t)).toBe('LSP'); }); it('falls back to the English display name when the locale has no entry', () => { @@ -217,8 +225,11 @@ describe('toolFormatting', () => { it('has a zh translation for every tool in the display-name map', () => { const tZh = getTranslator('zh-CN'); + // Tools intentionally shown in English (proper names / acronyms). + const keepEnglish = new Set(['agent', 'grep_search', 'glob', 'search']); const untranslated = Object.keys(TOOL_DISPLAY_NAMES).filter( (wire) => + !keepEnglish.has(wire) && localizeToolDisplayName(wire, tZh) === formatToolDisplayName(wire), ); expect(untranslated).toEqual([]); @@ -233,7 +244,7 @@ describe('toolFormatting', () => { ], }); expect(getAgentCurrentToolHint(agent, getTranslator('zh-CN'))).toContain( - '终端', + '运行命令', ); expect(getAgentCurrentToolHint(agent, getTranslator('en'))).toContain( 'Shell', diff --git a/packages/web-shell/client/i18n.tsx b/packages/web-shell/client/i18n.tsx index 595e87123e7..303d32c18f8 100644 --- a/packages/web-shell/client/i18n.tsx +++ b/packages/web-shell/client/i18n.tsx @@ -892,23 +892,24 @@ const ZH: Messages = { ...EN, // Tool display names (chat-stream badge labels). Keyed by `toolName.`; // a wire name with no entry here falls back to the English display name via - // `localizeToolDisplayName`. A product name (e.g. `Notebook`) stays verbatim. + // `localizeToolDisplayName`. Proper tool names / acronyms stay in English + // (Agent, Grep, Glob, LSP); a product name (e.g. `Notebook`) stays verbatim. 'toolName.edit': '编辑', 'toolName.write_file': '写入文件', 'toolName.read_file': '读取文件', - 'toolName.grep_search': '搜索', - 'toolName.glob': '匹配文件', - 'toolName.run_shell_command': '终端', + 'toolName.grep_search': 'Grep', + 'toolName.glob': 'Glob', + 'toolName.run_shell_command': '运行命令', 'toolName.todo_write': '任务清单', 'toolName.save_memory': '保存记忆', - 'toolName.agent': '智能体', + 'toolName.agent': 'Agent', 'toolName.skill': '技能', 'toolName.enter_plan_mode': '进入计划模式', 'toolName.exit_plan_mode': '退出计划模式', 'toolName.web_fetch': '网络抓取', 'toolName.web_search': '网络搜索', 'toolName.list_directory': '列出文件', - 'toolName.lsp': '语言服务', + 'toolName.lsp': 'LSP', 'toolName.ask_user_question': '询问用户', 'toolName.cron_create': '创建定时任务', 'toolName.cron_list': '定时任务列表', @@ -924,15 +925,15 @@ const ZH: Messages = { 'toolName.monitor': '监控', 'toolName.notebook_edit': '编辑 Notebook', 'toolName.tool_search': '工具搜索', - 'toolName.enter_worktree': '进入工作树', - 'toolName.exit_worktree': '退出工作树', + 'toolName.enter_worktree': '进入 Worktree', + 'toolName.exit_worktree': '退出 Worktree', 'toolName.workflow': '工作流', // web-shell-only wire aliases (see TOOL_DISPLAY_NAMES in toolFormatting.ts) - 'toolName.bash': '终端', - 'toolName.shell': '终端命令', + 'toolName.bash': '运行命令', + 'toolName.shell': 'Shell 命令', 'toolName.read': '读取文件', 'toolName.write': '写入文件', - 'toolName.search': '搜索', + 'toolName.search': 'Grep', 'about.auth': '认证', 'about.baseUrl': 'Base URL', 'about.fastModel': '快速模型',