Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions packages/cli/src/i18n/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,59 @@ describe('supported language resolution', () => {
expect(resolveSupportedLanguage('zh-HK')).toBe('zh');
});
});

describe('localizeToolDisplayName', () => {
Comment thread
wenshao marked this conversation as resolved.
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('任务清单');
// 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');
});

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');
});

it('has a zh translation for every core tool display name', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The completeness test validates zh but not zh-TW, which is a distinct supported locale (strictParity: true) with 35 independent toolDisplayName.* entries in zh-TW.js. A missing zh-TW entry would silently fall back to English with no test signal.

Consider adding a parallel test:

it('has a zh-TW translation for every core tool display name', async () => {
  const { setLanguageAsync, localizeToolDisplayName } = await import('./index.js');
  const { ToolDisplayNames } = await import('@qwen-code/qwen-code-core');
  const { SHELL_COMMAND_NAME } = await import('../../ui/constants.js');
  await setLanguageAsync('zh-TW');
  const names = [...Object.values(ToolDisplayNames), SHELL_COMMAND_NAME];
  const untranslated = names.filter(
    (name) => localizeToolDisplayName(name) === name,
  );
  expect(untranslated).toEqual([]);
});

— qwen3.7-max via Qwen Code /review

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 (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) =>
!KEEP_ENGLISH.has(name) && localizeToolDisplayName(name) === name,
);
expect(untranslated).toEqual([]);
});
});
13 changes: 13 additions & 0 deletions packages/cli/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,19 @@ export function t(key: string, params?: Record<string, string>): string {
return interpolate(translation, params);
}

/**
* Locale-aware tool display name for chat-stream badges. Looks up the
* `toolDisplayName.<English display name>` 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
Expand Down
44 changes: 44 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<English display name>` 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
// ============================================================================
Expand Down
43 changes: 43 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<English display name>` (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': 'Grep',
'toolDisplayName.Glob': 'Glob',
'toolDisplayName.Shell': '運行命令',
'toolDisplayName.Shell Command': 'Shell 命令',
'toolDisplayName.TodoWrite': '任務清單',
'toolDisplayName.SaveMemory': '儲存記憶',
'toolDisplayName.Agent': 'Agent',
'toolDisplayName.Skill': '技能',
'toolDisplayName.EnterPlanMode': '進入計畫模式',
'toolDisplayName.ExitPlanMode': '退出計畫模式',
'toolDisplayName.WebFetch': '網路擷取',
'toolDisplayName.WebSearch': '網路搜尋',
'toolDisplayName.ListFiles': '列出檔案',
'toolDisplayName.Lsp': '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': '進入 Worktree',
'toolDisplayName.ExitWorktree': '退出 Worktree',
'toolDisplayName.Workflow': '工作流程',
'↑ to manage attachments': '↑ 管理附件',
'← → select, Delete to remove, ↓ to exit': '← → 選擇,Delete 刪除,↓ 退出',
'Attachments: ': '附件:',
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,51 @@
// Chinese translations for Qwen Code CLI

export default {
// ============================================================================
// Tool display names (chat-stream badge labels)
// ----------------------------------------------------------------------------
// Keyed by `toolDisplayName.<English display name>` (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`. 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': 'Grep',
'toolDisplayName.Glob': 'Glob',
'toolDisplayName.Shell': '运行命令',
'toolDisplayName.Shell Command': 'Shell 命令',
'toolDisplayName.TodoWrite': '任务清单',
'toolDisplayName.SaveMemory': '保存记忆',
'toolDisplayName.Agent': 'Agent',
'toolDisplayName.Skill': '技能',
'toolDisplayName.EnterPlanMode': '进入计划模式',
'toolDisplayName.ExitPlanMode': '退出计划模式',
'toolDisplayName.WebFetch': '网络抓取',
'toolDisplayName.WebSearch': '网络搜索',
'toolDisplayName.ListFiles': '列出文件',
'toolDisplayName.Lsp': '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': '进入 Worktree',
'toolDisplayName.ExitWorktree': '退出 Worktree',
'toolDisplayName.Workflow': '工作流',
// ============================================================================
// Help / UI Components
// ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -57,7 +57,7 @@ const TOOL_DISPLAY_BY_NAME: Record<string, string> = 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()
: '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ToolDisplayNames,
ToolNames,
} from '@qwen-code/qwen-code-core';
import { localizeToolDisplayName } from '../../../i18n/index.js';
import {
useBackgroundTaskViewActions,
useBackgroundTaskViewState,
Expand Down Expand Up @@ -137,7 +138,9 @@ const TOOL_DISPLAY_BY_NAME: Record<string, string> = 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -166,7 +166,7 @@ export const CompactToolGroupDisplay: React.FC<
{compactLabel
? renderSummaryHeader(compactLabel, toolCalls.length)
: renderDefaultHeader(
activeTool.name,
localizeToolDisplayName(activeTool.name),
activeToolDescription,
toolCalls.length,
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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(
Comment thread
wenshao marked this conversation as resolved.
TOOL_DISPLAY_BY_NAME[last.name] ?? last.name,
);
const desc = last.description?.replace(/\s*\n\s*/g, ' ').trim();
return desc ? `${display} ${desc}` : display;
}
Expand Down
41 changes: 41 additions & 0 deletions packages/cli/src/ui/components/messages/ToolMessage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -930,3 +930,44 @@ describe('<ToolMessage />', () => {
expect(output).toContain('- Step 2');
});
});

describe('<ToolMessage /> 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(
<ToolMessage {...localizedProps} />,
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(
<ToolMessage {...localizedProps} />,
StreamingState.Idle,
);
expect(lastFrame() ?? '').toContain('ReadFile');
});
});
3 changes: 2 additions & 1 deletion packages/cli/src/ui/components/messages/ToolMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -800,7 +801,7 @@ const ToolInfo: React.FC<ToolInfo> = ({
strikethrough={status === ToolCallStatus.Canceled}
>
<Text color={nameColor} bold>
{name}
{localizeToolDisplayName(name)}
</Text>{' '}
<Text color={theme.text.secondary}>{description}</Text>
</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -391,7 +392,9 @@ export function AskUserQuestion({ request, onConfirm }: AskUserQuestionProps) {
{/* Header line like CLI */}
<div className={styles.titleLine}>
<span className={styles.icon}>?</span>
<span className={styles.toolName}>AskUserQuestion</span>
<span className={styles.toolName}>
{localizeToolDisplayName('ask_user_question', t)}
</span>
<span className={styles.toolDesc}>
{t('askUser.title', { count: questions.length })}
</span>
Expand Down
Loading
Loading