From a33a00256d59e9116d2246cbd9e288b03297b47d Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Mon, 17 Nov 2025 21:42:25 +0800 Subject: [PATCH] feat: update description --- packages/cli/package.json | 3 +- packages/cli/src/i18n/locales/en.js | 64 ++++ packages/cli/src/i18n/locales/zh.js | 61 ++++ packages/cli/src/ui/commands/chatCommand.ts | 90 +++-- .../cli/src/ui/commands/extensionsCommand.ts | 13 +- packages/cli/src/ui/commands/ideCommand.ts | 32 +- packages/cli/src/ui/commands/mcpCommand.ts | 13 +- packages/cli/src/ui/commands/memoryCommand.ts | 92 +++-- packages/cli/src/ui/commands/modelCommand.ts | 16 +- .../src/ui/components/ConfigInitDisplay.tsx | 12 +- .../ui/components/ContextSummaryDisplay.tsx | 51 ++- packages/cli/src/ui/components/Help.tsx | 84 +++-- .../cli/src/ui/components/ModelDialog.tsx | 5 +- .../messages/ToolConfirmationMessage.tsx | 80 +++-- .../cli/src/ui/components/views/McpStatus.tsx | 94 +++-- packages/cli/src/ui/models/availableModels.ts | 15 +- scripts/check-i18n.ts | 329 ++++++++++++++++++ scripts/copy_files.js | 13 +- 18 files changed, 852 insertions(+), 215 deletions(-) create mode 100644 scripts/check-i18n.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 6dce0ccc..1c1fd520 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -19,7 +19,8 @@ "format": "prettier --write .", "test": "vitest run", "test:ci": "vitest run", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "check-i18n": "tsx ../../scripts/check-i18n.ts" }, "files": [ "dist" diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index eed7f0b7..3c0fc22f 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -109,6 +109,8 @@ export default { 'install required IDE companion for {{ideName}}', 'enable IDE integration': 'enable IDE integration', 'disable IDE integration': 'disable IDE integration', + 'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.': + 'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.', 'Set up GitHub Actions': 'Set up GitHub Actions', 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)': 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)', @@ -220,6 +222,12 @@ export default { 'Project memory is currently empty.': 'Project memory is currently empty.', 'Refreshing memory from source files...': 'Refreshing memory from source files...', + 'Add content to the memory. Use --global for global memory or --project for project memory.': + 'Add content to the memory. Use --global for global memory or --project for project memory.', + 'Usage: /memory add [--global|--project] ': + 'Usage: /memory add [--global|--project] ', + 'Attempting to save to memory {{scope}}: "{{fact}}"': + 'Attempting to save to memory {{scope}}: "{{fact}}"', // ============================================================================ // Commands - MCP @@ -287,6 +295,8 @@ export default { 'Error sharing conversation: {{error}}', 'Conversation shared to {{filePath}}': 'Conversation shared to {{filePath}}', 'No conversation found to share.': 'No conversation found to share.', + 'Share the current conversation to a markdown or json file. Usage: /chat share ': + 'Share the current conversation to a markdown or json file. Usage: /chat share ', // ============================================================================ // Commands - Summary @@ -344,6 +354,27 @@ export default { No: 'No', 'No (esc)': 'No (esc)', 'Yes, allow always for this session': 'Yes, allow always for this session', + 'Modify in progress:': 'Modify in progress:', + 'Save and close external editor to continue': + 'Save and close external editor to continue', + 'Apply this change?': 'Apply this change?', + 'Yes, allow always': 'Yes, allow always', + 'Modify with external editor': 'Modify with external editor', + 'No, suggest changes (esc)': 'No, suggest changes (esc)', + "Allow execution of: '{{command}}'?": "Allow execution of: '{{command}}'?", + 'Yes, allow always ...': 'Yes, allow always ...', + 'Yes, and auto-accept edits': 'Yes, and auto-accept edits', + 'Yes, and manually approve edits': 'Yes, and manually approve edits', + 'No, keep planning (esc)': 'No, keep planning (esc)', + 'URLs to fetch:': 'URLs to fetch:', + 'MCP Server: {{server}}': 'MCP Server: {{server}}', + 'Tool: {{tool}}': 'Tool: {{tool}}', + 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?': + 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?', + 'Yes, always allow tool "{{tool}}" from server "{{server}}"': + 'Yes, always allow tool "{{tool}}" from server "{{server}}"', + 'Yes, always allow all tools from server "{{server}}"': + 'Yes, always allow all tools from server "{{server}}"', // ============================================================================ // Dialogs - Shell Confirmation @@ -427,6 +458,16 @@ export default { 'Waiting for Qwen OAuth authentication...': 'Waiting for Qwen OAuth authentication...', + // ============================================================================ + // Dialogs - Model + // ============================================================================ + 'Select Model': 'Select Model', + '(Press Esc to close)': '(Press Esc to close)', + 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)': + 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)', + 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)': + 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)', + // ============================================================================ // Dialogs - Permissions // ============================================================================ @@ -471,6 +512,29 @@ export default { '{{count}} prompt': '{{count}} prompt', '{{count}} prompts': '{{count}} prompts', '(from {{extensionName}})': '(from {{extensionName}})', + OAuth: 'OAuth', + 'OAuth expired': 'OAuth expired', + 'OAuth not authenticated': 'OAuth not authenticated', + 'tools and prompts will appear when ready': + 'tools and prompts will appear when ready', + '{{count}} tools cached': '{{count}} tools cached', + 'Tools:': 'Tools:', + 'Parameters:': 'Parameters:', + 'Prompts:': 'Prompts:', + Blocked: 'Blocked', + '💡 Tips:': '💡 Tips:', + Use: 'Use', + 'to show server and tool descriptions': + 'to show server and tool descriptions', + 'to show tool parameter schemas': 'to show tool parameter schemas', + 'to hide descriptions': 'to hide descriptions', + 'to authenticate with OAuth-enabled servers': + 'to authenticate with OAuth-enabled servers', + Press: 'Press', + 'to toggle tool descriptions on/off': 'to toggle tool descriptions on/off', + "Starting OAuth authentication for MCP server '{{name}}'...": + "Starting OAuth authentication for MCP server '{{name}}'...", + 'Restarting MCP servers...': 'Restarting MCP servers...', // ============================================================================ // Startup Tips diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 104a8dce..82783e73 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -103,6 +103,8 @@ export default { '安装 {{ideName}} 所需的 IDE 配套工具', 'enable IDE integration': '启用 IDE 集成', 'disable IDE integration': '禁用 IDE 集成', + 'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.': + '您当前环境不支持 IDE 集成。要使用此功能,请在以下支持的 IDE 之一中运行 Qwen Code:VS Code 或 VS Code 分支版本。', 'Set up GitHub Actions': '设置 GitHub Actions', 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)': '配置终端按键绑定以支持多行输入(VS Code、Cursor、Windsurf)', @@ -204,6 +206,12 @@ export default { '项目记忆内容来自 {{path}}:\n\n---\n{{content}}\n---', 'Project memory is currently empty.': '项目记忆当前为空', 'Refreshing memory from source files...': '正在从源文件刷新记忆...', + 'Add content to the memory. Use --global for global memory or --project for project memory.': + '添加内容到记忆。使用 --global 表示全局记忆,使用 --project 表示项目记忆', + 'Usage: /memory add [--global|--project] ': + '用法:/memory add [--global|--project] <要记住的文本>', + 'Attempting to save to memory {{scope}}: "{{fact}}"': + '正在尝试保存到记忆 {{scope}}:"{{fact}}"', // ============================================================================ // Commands - MCP @@ -266,6 +274,8 @@ export default { 'Error sharing conversation: {{error}}': '分享对话时出错:{{error}}', 'Conversation shared to {{filePath}}': '对话已分享到 {{filePath}}', 'No conversation found to share.': '未找到要分享的对话', + 'Share the current conversation to a markdown or json file. Usage: /chat share ': + '将当前对话分享到 markdown 或 json 文件。用法:/chat share ', // ============================================================================ // Commands - Summary @@ -320,6 +330,26 @@ export default { No: '否', 'No (esc)': '否 (esc)', 'Yes, allow always for this session': '是,本次会话总是允许', + 'Modify in progress:': '正在修改:', + 'Save and close external editor to continue': '保存并关闭外部编辑器以继续', + 'Apply this change?': '是否应用此更改?', + 'Yes, allow always': '是,总是允许', + 'Modify with external editor': '使用外部编辑器修改', + 'No, suggest changes (esc)': '否,建议更改 (esc)', + "Allow execution of: '{{command}}'?": "允许执行:'{{command}}'?", + 'Yes, allow always ...': '是,总是允许 ...', + 'Yes, and auto-accept edits': '是,并自动接受编辑', + 'Yes, and manually approve edits': '是,并手动批准编辑', + 'No, keep planning (esc)': '否,继续规划 (esc)', + 'URLs to fetch:': '要获取的 URL:', + 'MCP Server: {{server}}': 'MCP 服务器:{{server}}', + 'Tool: {{tool}}': '工具:{{tool}}', + 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?': + '允许执行来自服务器 "{{server}}" 的 MCP 工具 "{{tool}}"?', + 'Yes, always allow tool "{{tool}}" from server "{{server}}"': + '是,总是允许来自服务器 "{{server}}" 的工具 "{{tool}}"', + 'Yes, always allow all tools from server "{{server}}"': + '是,总是允许来自服务器 "{{server}}" 的所有工具', // ============================================================================ // Dialogs - Shell Confirmation @@ -394,6 +424,16 @@ export default { '按任意键返回认证类型选择', 'Waiting for Qwen OAuth authentication...': '正在等待 Qwen OAuth 认证...', + // ============================================================================ + // Dialogs - Model + // ============================================================================ + 'Select Model': '选择模型', + '(Press Esc to close)': '(按 Esc 关闭)', + 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)': + '来自阿里云 ModelStudio 的最新 Qwen Coder 模型(版本:qwen3-coder-plus-2025-09-23)', + 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)': + '来自阿里云 ModelStudio 的最新 Qwen Vision 模型(版本:qwen3-vl-plus-2025-09-23)', + // ============================================================================ // Dialogs - Permissions // ============================================================================ @@ -438,6 +478,27 @@ export default { '{{count}} prompt': '{{count}} 个提示', '{{count}} prompts': '{{count}} 个提示', '(from {{extensionName}})': '(来自 {{extensionName}})', + OAuth: 'OAuth', + 'OAuth expired': 'OAuth 已过期', + 'OAuth not authenticated': 'OAuth 未认证', + 'tools and prompts will appear when ready': '工具和提示将在就绪时显示', + '{{count}} tools cached': '{{count}} 个工具已缓存', + 'Tools:': '工具:', + 'Parameters:': '参数:', + 'Prompts:': '提示:', + Blocked: '已阻止', + '💡 Tips:': '💡 提示:', + Use: '使用', + 'to show server and tool descriptions': '显示服务器和工具描述', + 'to show tool parameter schemas': '显示工具参数架构', + 'to hide descriptions': '隐藏描述', + 'to authenticate with OAuth-enabled servers': + '使用支持 OAuth 的服务器进行认证', + Press: '按', + 'to toggle tool descriptions on/off': '切换工具描述开关', + "Starting OAuth authentication for MCP server '{{name}}'...": + "正在为 MCP 服务器 '{{name}}' 启动 OAuth 认证...", + 'Restarting MCP servers...': '正在重启 MCP 服务器...', // ============================================================================ // Startup Tips diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 4f3efb79..eaf156da 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -7,7 +7,6 @@ import * as fsPromises from 'node:fs/promises'; import React from 'react'; import { Text } from 'ink'; -import { theme } from '../semantic-colors.js'; import type { CommandContext, SlashCommand, @@ -20,6 +19,7 @@ import path from 'node:path'; import type { HistoryItemWithoutId } from '../types.js'; import { MessageType } from '../types.js'; import type { Content } from '@google/genai'; +import { t } from '../../i18n/index.js'; interface ChatDetail { name: string; @@ -67,7 +67,9 @@ const getSavedChatTags = async ( const listCommand: SlashCommand = { name: 'list', - description: 'List saved conversation checkpoints', + get description() { + return t('List saved conversation checkpoints'); + }, kind: CommandKind.BUILT_IN, action: async (context): Promise => { const chatDetails = await getSavedChatTags(context, false); @@ -75,7 +77,7 @@ const listCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: 'No saved conversation checkpoints found.', + content: t('No saved conversation checkpoints found.'), }; } @@ -83,7 +85,7 @@ const listCommand: SlashCommand = { ...chatDetails.map((chat) => chat.name.length), ); - let message = 'List of saved conversations:\n\n'; + let message = t('List of saved conversations:') + '\n\n'; for (const chat of chatDetails) { const paddedName = chat.name.padEnd(maxNameLength, ' '); const isoString = chat.mtime.toISOString(); @@ -91,7 +93,7 @@ const listCommand: SlashCommand = { const formattedDate = match ? `${match[1]} ${match[2]}` : 'Invalid Date'; message += ` - ${paddedName} (saved on ${formattedDate})\n`; } - message += `\nNote: Newest last, oldest first`; + message += `\n${t('Note: Newest last, oldest first')}`; return { type: 'message', messageType: 'info', @@ -102,8 +104,11 @@ const listCommand: SlashCommand = { const saveCommand: SlashCommand = { name: 'save', - description: - 'Save the current conversation as a checkpoint. Usage: /chat save ', + get description() { + return t( + 'Save the current conversation as a checkpoint. Usage: /chat save ', + ); + }, kind: CommandKind.BUILT_IN, action: async (context, args): Promise => { const tag = args.trim(); @@ -111,7 +116,7 @@ const saveCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Missing tag. Usage: /chat save ', + content: t('Missing tag. Usage: /chat save '), }; } @@ -126,9 +131,12 @@ const saveCommand: SlashCommand = { prompt: React.createElement( Text, null, - 'A checkpoint with the tag ', - React.createElement(Text, { color: theme.text.accent }, tag), - ' already exists. Do you want to overwrite it?', + t( + 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?', + { + tag, + }, + ), ), originalInvocation: { raw: context.invocation?.raw || `/chat save ${tag}`, @@ -142,7 +150,7 @@ const saveCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'No chat client available to save conversation.', + content: t('No chat client available to save conversation.'), }; } @@ -152,13 +160,15 @@ const saveCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: `Conversation checkpoint saved with tag: ${decodeTagName(tag)}.`, + content: t('Conversation checkpoint saved with tag: {{tag}}.', { + tag: decodeTagName(tag), + }), }; } else { return { type: 'message', messageType: 'info', - content: 'No conversation found to save.', + content: t('No conversation found to save.'), }; } }, @@ -167,8 +177,11 @@ const saveCommand: SlashCommand = { const resumeCommand: SlashCommand = { name: 'resume', altNames: ['load'], - description: - 'Resume a conversation from a checkpoint. Usage: /chat resume ', + get description() { + return t( + 'Resume a conversation from a checkpoint. Usage: /chat resume ', + ); + }, kind: CommandKind.BUILT_IN, action: async (context, args) => { const tag = args.trim(); @@ -176,7 +189,7 @@ const resumeCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Missing tag. Usage: /chat resume ', + content: t('Missing tag. Usage: /chat resume '), }; } @@ -188,7 +201,9 @@ const resumeCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: `No saved checkpoint found with tag: ${decodeTagName(tag)}.`, + content: t('No saved checkpoint found with tag: {{tag}}.', { + tag: decodeTagName(tag), + }), }; } @@ -237,7 +252,9 @@ const resumeCommand: SlashCommand = { const deleteCommand: SlashCommand = { name: 'delete', - description: 'Delete a conversation checkpoint. Usage: /chat delete ', + get description() { + return t('Delete a conversation checkpoint. Usage: /chat delete '); + }, kind: CommandKind.BUILT_IN, action: async (context, args): Promise => { const tag = args.trim(); @@ -245,7 +262,7 @@ const deleteCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Missing tag. Usage: /chat delete ', + content: t('Missing tag. Usage: /chat delete '), }; } @@ -257,13 +274,17 @@ const deleteCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: `Conversation checkpoint '${decodeTagName(tag)}' has been deleted.`, + content: t("Conversation checkpoint '{{tag}}' has been deleted.", { + tag: decodeTagName(tag), + }), }; } else { return { type: 'message', messageType: 'error', - content: `Error: No checkpoint found with tag '${decodeTagName(tag)}'.`, + content: t("Error: No checkpoint found with tag '{{tag}}'.", { + tag: decodeTagName(tag), + }), }; } }, @@ -309,8 +330,11 @@ export function serializeHistoryToMarkdown(history: Content[]): string { const shareCommand: SlashCommand = { name: 'share', - description: - 'Share the current conversation to a markdown or json file. Usage: /chat share ', + get description() { + return t( + 'Share the current conversation to a markdown or json file. Usage: /chat share ', + ); + }, kind: CommandKind.BUILT_IN, action: async (context, args): Promise => { let filePathArg = args.trim(); @@ -324,7 +348,7 @@ const shareCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Invalid file format. Only .md and .json are supported.', + content: t('Invalid file format. Only .md and .json are supported.'), }; } @@ -333,7 +357,7 @@ const shareCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'No chat client available to share conversation.', + content: t('No chat client available to share conversation.'), }; } @@ -346,7 +370,7 @@ const shareCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: 'No conversation found to share.', + content: t('No conversation found to share.'), }; } @@ -362,14 +386,18 @@ const shareCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: `Conversation shared to ${filePath}`, + content: t('Conversation shared to {{filePath}}', { + filePath, + }), }; } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); return { type: 'message', messageType: 'error', - content: `Error sharing conversation: ${errorMessage}`, + content: t('Error sharing conversation: {{error}}', { + error: errorMessage, + }), }; } }, @@ -377,7 +405,9 @@ const shareCommand: SlashCommand = { export const chatCommand: SlashCommand = { name: 'chat', - description: 'Manage conversation history.', + get description() { + return t('Manage conversation history.'); + }, kind: CommandKind.BUILT_IN, subCommands: [ listCommand, diff --git a/packages/cli/src/ui/commands/extensionsCommand.ts b/packages/cli/src/ui/commands/extensionsCommand.ts index e4f2c8fb..b02dcf9e 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.ts @@ -19,6 +19,7 @@ import { type SlashCommand, CommandKind, } from './types.js'; +import { t } from '../../i18n/index.js'; async function listAction(context: CommandContext) { context.ui.addItem( @@ -131,14 +132,18 @@ async function updateAction(context: CommandContext, args: string) { const listExtensionsCommand: SlashCommand = { name: 'list', - description: 'List active extensions', + get description() { + return t('List active extensions'); + }, kind: CommandKind.BUILT_IN, action: listAction, }; const updateExtensionsCommand: SlashCommand = { name: 'update', - description: 'Update extensions. Usage: update |--all', + get description() { + return t('Update extensions. Usage: update |--all'); + }, kind: CommandKind.BUILT_IN, action: updateAction, completion: async (context, partialArg) => { @@ -158,7 +163,9 @@ const updateExtensionsCommand: SlashCommand = { export const extensionsCommand: SlashCommand = { name: 'extensions', - description: 'Manage extensions', + get description() { + return t('Manage extensions'); + }, kind: CommandKind.BUILT_IN, subCommands: [listExtensionsCommand, updateExtensionsCommand], action: (context, args) => diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts index e04b4066..ebb7e3dc 100644 --- a/packages/cli/src/ui/commands/ideCommand.ts +++ b/packages/cli/src/ui/commands/ideCommand.ts @@ -26,6 +26,7 @@ import type { } from './types.js'; import { CommandKind } from './types.js'; import { SettingScope } from '../../config/settings.js'; +import { t } from '../../i18n/index.js'; function getIdeStatusMessage(ideClient: IdeClient): { messageType: 'info' | 'error'; @@ -138,27 +139,35 @@ export const ideCommand = async (): Promise => { if (!currentIDE) { return { name: 'ide', - description: 'manage IDE integration', + get description() { + return t('manage IDE integration'); + }, kind: CommandKind.BUILT_IN, action: (): SlashCommandActionReturn => ({ type: 'message', messageType: 'error', - content: `IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.`, + content: t( + 'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.', + ), }) as const, }; } const ideSlashCommand: SlashCommand = { name: 'ide', - description: 'manage IDE integration', + get description() { + return t('manage IDE integration'); + }, kind: CommandKind.BUILT_IN, subCommands: [], }; const statusCommand: SlashCommand = { name: 'status', - description: 'check status of IDE integration', + get description() { + return t('check status of IDE integration'); + }, kind: CommandKind.BUILT_IN, action: async (): Promise => { const { messageType, content } = @@ -173,7 +182,12 @@ export const ideCommand = async (): Promise => { const installCommand: SlashCommand = { name: 'install', - description: `install required IDE companion for ${ideClient.getDetectedIdeDisplayName()}`, + get description() { + const ideName = ideClient.getDetectedIdeDisplayName() ?? 'IDE'; + return t('install required IDE companion for {{ideName}}', { + ideName, + }); + }, kind: CommandKind.BUILT_IN, action: async (context) => { const installer = getIdeInstaller(currentIDE); @@ -246,7 +260,9 @@ export const ideCommand = async (): Promise => { const enableCommand: SlashCommand = { name: 'enable', - description: 'enable IDE integration', + get description() { + return t('enable IDE integration'); + }, kind: CommandKind.BUILT_IN, action: async (context: CommandContext) => { context.services.settings.setValue( @@ -268,7 +284,9 @@ export const ideCommand = async (): Promise => { const disableCommand: SlashCommand = { name: 'disable', - description: 'disable IDE integration', + get description() { + return t('disable IDE integration'); + }, kind: CommandKind.BUILT_IN, action: async (context: CommandContext) => { context.services.settings.setValue( diff --git a/packages/cli/src/ui/commands/mcpCommand.ts b/packages/cli/src/ui/commands/mcpCommand.ts index f0e66829..d8fec717 100644 --- a/packages/cli/src/ui/commands/mcpCommand.ts +++ b/packages/cli/src/ui/commands/mcpCommand.ts @@ -92,7 +92,12 @@ const authCommand: SlashCommand = { context.ui.addItem( { type: 'info', - text: `Starting OAuth authentication for MCP server '${serverName}'...`, + text: t( + "Starting OAuth authentication for MCP server '{{name}}'...", + { + name: serverName, + }, + ), }, Date.now(), ); @@ -208,7 +213,7 @@ const listCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Could not retrieve tool registry.', + content: t('Could not retrieve tool registry.'), }; } @@ -320,14 +325,14 @@ const refreshCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Could not retrieve tool registry.', + content: t('Could not retrieve tool registry.'), }; } context.ui.addItem( { type: 'info', - text: 'Restarting MCP servers...', + text: t('Restarting MCP servers...'), }, Date.now(), ); diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts index 560456f8..013b815d 100644 --- a/packages/cli/src/ui/commands/memoryCommand.ts +++ b/packages/cli/src/ui/commands/memoryCommand.ts @@ -15,15 +15,20 @@ import fs from 'fs/promises'; import { MessageType } from '../types.js'; import type { SlashCommand, SlashCommandActionReturn } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const memoryCommand: SlashCommand = { name: 'memory', - description: 'Commands for interacting with memory.', + get description() { + return t('Commands for interacting with memory.'); + }, kind: CommandKind.BUILT_IN, subCommands: [ { name: 'show', - description: 'Show the current memory contents.', + get description() { + return t('Show the current memory contents.'); + }, kind: CommandKind.BUILT_IN, action: async (context) => { const memoryContent = context.services.config?.getUserMemory() || ''; @@ -31,8 +36,8 @@ export const memoryCommand: SlashCommand = { const messageContent = memoryContent.length > 0 - ? `Current memory content from ${fileCount} file(s):\n\n---\n${memoryContent}\n---` - : 'Memory is currently empty.'; + ? `${t('Current memory content from {{count}} file(s):', { count: String(fileCount) })}\n\n---\n${memoryContent}\n---` + : t('Memory is currently empty.'); context.ui.addItem( { @@ -45,7 +50,9 @@ export const memoryCommand: SlashCommand = { subCommands: [ { name: '--project', - description: 'Show project-level memory contents.', + get description() { + return t('Show project-level memory contents.'); + }, kind: CommandKind.BUILT_IN, action: async (context) => { try { @@ -57,8 +64,14 @@ export const memoryCommand: SlashCommand = { const messageContent = memoryContent.trim().length > 0 - ? `Project memory content from ${projectMemoryPath}:\n\n---\n${memoryContent}\n---` - : 'Project memory is currently empty.'; + ? t( + 'Project memory content from {{path}}:\n\n---\n{{content}}\n---', + { + path: projectMemoryPath, + content: memoryContent, + }, + ) + : t('Project memory is currently empty.'); context.ui.addItem( { @@ -71,7 +84,9 @@ export const memoryCommand: SlashCommand = { context.ui.addItem( { type: MessageType.INFO, - text: 'Project memory file not found or is currently empty.', + text: t( + 'Project memory file not found or is currently empty.', + ), }, Date.now(), ); @@ -80,7 +95,9 @@ export const memoryCommand: SlashCommand = { }, { name: '--global', - description: 'Show global memory contents.', + get description() { + return t('Show global memory contents.'); + }, kind: CommandKind.BUILT_IN, action: async (context) => { try { @@ -96,8 +113,10 @@ export const memoryCommand: SlashCommand = { const messageContent = globalMemoryContent.trim().length > 0 - ? `Global memory content:\n\n---\n${globalMemoryContent}\n---` - : 'Global memory is currently empty.'; + ? t('Global memory content:\n\n---\n{{content}}\n---', { + content: globalMemoryContent, + }) + : t('Global memory is currently empty.'); context.ui.addItem( { @@ -110,7 +129,9 @@ export const memoryCommand: SlashCommand = { context.ui.addItem( { type: MessageType.INFO, - text: 'Global memory file not found or is currently empty.', + text: t( + 'Global memory file not found or is currently empty.', + ), }, Date.now(), ); @@ -121,16 +142,20 @@ export const memoryCommand: SlashCommand = { }, { name: 'add', - description: - 'Add content to the memory. Use --global for global memory or --project for project memory.', + get description() { + return t( + 'Add content to the memory. Use --global for global memory or --project for project memory.', + ); + }, kind: CommandKind.BUILT_IN, action: (context, args): SlashCommandActionReturn | void => { if (!args || args.trim() === '') { return { type: 'message', messageType: 'error', - content: + content: t( 'Usage: /memory add [--global|--project] ', + ), }; } @@ -150,8 +175,9 @@ export const memoryCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: + content: t( 'Usage: /memory add [--global|--project] ', + ), }; } else { // No scope specified, will be handled by the tool @@ -162,8 +188,9 @@ export const memoryCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: + content: t( 'Usage: /memory add [--global|--project] ', + ), }; } @@ -171,7 +198,10 @@ export const memoryCommand: SlashCommand = { context.ui.addItem( { type: MessageType.INFO, - text: `Attempting to save to memory ${scopeText}: "${fact}"`, + text: t('Attempting to save to memory {{scope}}: "{{fact}}"', { + scope: scopeText, + fact, + }), }, Date.now(), ); @@ -185,21 +215,25 @@ export const memoryCommand: SlashCommand = { subCommands: [ { name: '--project', - description: 'Add content to project-level memory.', + get description() { + return t('Add content to project-level memory.'); + }, kind: CommandKind.BUILT_IN, action: (context, args): SlashCommandActionReturn | void => { if (!args || args.trim() === '') { return { type: 'message', messageType: 'error', - content: 'Usage: /memory add --project ', + content: t('Usage: /memory add --project '), }; } context.ui.addItem( { type: MessageType.INFO, - text: `Attempting to save to project memory: "${args.trim()}"`, + text: t('Attempting to save to project memory: "{{text}}"', { + text: args.trim(), + }), }, Date.now(), ); @@ -213,21 +247,25 @@ export const memoryCommand: SlashCommand = { }, { name: '--global', - description: 'Add content to global memory.', + get description() { + return t('Add content to global memory.'); + }, kind: CommandKind.BUILT_IN, action: (context, args): SlashCommandActionReturn | void => { if (!args || args.trim() === '') { return { type: 'message', messageType: 'error', - content: 'Usage: /memory add --global ', + content: t('Usage: /memory add --global '), }; } context.ui.addItem( { type: MessageType.INFO, - text: `Attempting to save to global memory: "${args.trim()}"`, + text: t('Attempting to save to global memory: "{{text}}"', { + text: args.trim(), + }), }, Date.now(), ); @@ -243,13 +281,15 @@ export const memoryCommand: SlashCommand = { }, { name: 'refresh', - description: 'Refresh the memory from the source.', + get description() { + return t('Refresh the memory from the source.'); + }, kind: CommandKind.BUILT_IN, action: async (context) => { context.ui.addItem( { type: MessageType.INFO, - text: 'Refreshing memory from source files...', + text: t('Refreshing memory from source files...'), }, Date.now(), ); diff --git a/packages/cli/src/ui/commands/modelCommand.ts b/packages/cli/src/ui/commands/modelCommand.ts index b97e7c63..a25e96a1 100644 --- a/packages/cli/src/ui/commands/modelCommand.ts +++ b/packages/cli/src/ui/commands/modelCommand.ts @@ -12,10 +12,13 @@ import type { } from './types.js'; import { CommandKind } from './types.js'; import { getAvailableModelsForAuthType } from '../models/availableModels.js'; +import { t } from '../../i18n/index.js'; export const modelCommand: SlashCommand = { name: 'model', - description: 'Switch the model for this session', + get description() { + return t('Switch the model for this session'); + }, kind: CommandKind.BUILT_IN, action: async ( context: CommandContext, @@ -36,7 +39,7 @@ export const modelCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Content generator configuration not available.', + content: t('Content generator configuration not available.'), }; } @@ -45,7 +48,7 @@ export const modelCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Authentication type not available.', + content: t('Authentication type not available.'), }; } @@ -55,7 +58,12 @@ export const modelCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: `No models available for the current authentication type (${authType}).`, + content: t( + 'No models available for the current authentication type ({{authType}}).', + { + authType, + }, + ), }; } diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.tsx index 5cac7412..264eeeaf 100644 --- a/packages/cli/src/ui/components/ConfigInitDisplay.tsx +++ b/packages/cli/src/ui/components/ConfigInitDisplay.tsx @@ -11,15 +11,16 @@ import { useConfig } from '../contexts/ConfigContext.js'; import { type McpClient, MCPServerStatus } from '@qwen-code/qwen-code-core'; import { GeminiSpinner } from './GeminiRespondingSpinner.js'; import { theme } from '../semantic-colors.js'; +import { t } from '../../i18n/index.js'; export const ConfigInitDisplay = () => { const config = useConfig(); - const [message, setMessage] = useState('Initializing...'); + const [message, setMessage] = useState(t('Initializing...')); useEffect(() => { const onChange = (clients?: Map) => { if (!clients || clients.size === 0) { - setMessage(`Initializing...`); + setMessage(t('Initializing...')); return; } let connected = 0; @@ -28,7 +29,12 @@ export const ConfigInitDisplay = () => { connected++; } } - setMessage(`Connecting to MCP servers... (${connected}/${clients.size})`); + setMessage( + t('Connecting to MCP servers... ({{connected}}/{{total}})', { + connected: String(connected), + total: String(clients.size), + }), + ); }; appEvents.on('mcp-client-update', onChange); diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx index dba5c9ac..808c0ac7 100644 --- a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx @@ -13,6 +13,7 @@ import { } from '@qwen-code/qwen-code-core'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; +import { t } from '../../i18n/index.js'; interface ContextSummaryDisplayProps { geminiMdFileCount: number; @@ -50,9 +51,11 @@ export const ContextSummaryDisplay: React.FC = ({ if (openFileCount === 0) { return ''; } - return `${openFileCount} open file${ - openFileCount > 1 ? 's' : '' - } (ctrl+g to view)`; + const fileText = + openFileCount === 1 + ? t('{{count}} open file', { count: String(openFileCount) }) + : t('{{count}} open files', { count: String(openFileCount) }); + return `${fileText} ${t('(ctrl+g to view)')}`; })(); const geminiMdText = (() => { @@ -61,9 +64,15 @@ export const ContextSummaryDisplay: React.FC = ({ } const allNamesTheSame = new Set(contextFileNames).size < 2; const name = allNamesTheSame ? contextFileNames[0] : 'context'; - return `${geminiMdFileCount} ${name} file${ - geminiMdFileCount > 1 ? 's' : '' - }`; + return geminiMdFileCount === 1 + ? t('{{count}} {{name}} file', { + count: String(geminiMdFileCount), + name, + }) + : t('{{count}} {{name}} files', { + count: String(geminiMdFileCount), + name, + }); })(); const mcpText = (() => { @@ -73,15 +82,27 @@ export const ContextSummaryDisplay: React.FC = ({ const parts = []; if (mcpServerCount > 0) { - parts.push( - `${mcpServerCount} MCP server${mcpServerCount > 1 ? 's' : ''}`, - ); + const serverText = + mcpServerCount === 1 + ? t('{{count}} MCP server', { count: String(mcpServerCount) }) + : t('{{count}} MCP servers', { count: String(mcpServerCount) }); + parts.push(serverText); } if (blockedMcpServerCount > 0) { - let blockedText = `${blockedMcpServerCount} Blocked`; + let blockedText = t('{{count}} Blocked', { + count: String(blockedMcpServerCount), + }); if (mcpServerCount === 0) { - blockedText += ` MCP server${blockedMcpServerCount > 1 ? 's' : ''}`; + const serverText = + blockedMcpServerCount === 1 + ? t('{{count}} MCP server', { + count: String(blockedMcpServerCount), + }) + : t('{{count}} MCP servers', { + count: String(blockedMcpServerCount), + }); + blockedText += ` ${serverText}`; } parts.push(blockedText); } @@ -89,9 +110,9 @@ export const ContextSummaryDisplay: React.FC = ({ // Add ctrl+t hint when MCP servers are available if (mcpServers && Object.keys(mcpServers).length > 0) { if (showToolDescriptions) { - text += ' (ctrl+t to toggle)'; + text += ` ${t('(ctrl+t to toggle)')}`; } else { - text += ' (ctrl+t to view)'; + text += ` ${t('(ctrl+t to view)')}`; } } return text; @@ -102,7 +123,7 @@ export const ContextSummaryDisplay: React.FC = ({ if (isNarrow) { return ( - Using: + {t('Using:')} {summaryParts.map((part, index) => ( {' '}- {part} @@ -115,7 +136,7 @@ export const ContextSummaryDisplay: React.FC = ({ return ( - Using: {summaryParts.join(' | ')} + {t('Using:')} {summaryParts.join(' | ')} ); diff --git a/packages/cli/src/ui/components/Help.tsx b/packages/cli/src/ui/components/Help.tsx index d73df720..f1f6fcf9 100644 --- a/packages/cli/src/ui/components/Help.tsx +++ b/packages/cli/src/ui/components/Help.tsx @@ -8,6 +8,7 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { type SlashCommand, CommandKind } from '../commands/types.js'; +import { t } from '../../i18n/index.js'; interface Help { commands: readonly SlashCommand[]; @@ -23,46 +24,41 @@ export const Help: React.FC = ({ commands }) => ( > {/* Basics */} - Basics: + {t('Basics:')} - Add context + {t('Add context')} - : Use{' '} - - @ - {' '} - to specify files for context (e.g.,{' '} - - @src/myFile.ts - - ) to target specific files or folders. + :{' '} + {t( + 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.', + { + symbol: t('@'), + example: t('@src/myFile.ts'), + }, + )} - Shell mode + {t('Shell mode')} - : Execute shell commands via{' '} - - ! - {' '} - (e.g.,{' '} - - !npm run start - - ) or use natural language (e.g.{' '} - - start server - - ). + :{' '} + {t( + 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).', + { + symbol: t('!'), + example1: t('!npm run start'), + example2: t('start server'), + }, + )} {/* Commands */} - Commands: + {t('Commands:')} {commands .filter((command) => command.description && !command.hidden) @@ -97,81 +93,81 @@ export const Help: React.FC = ({ commands }) => ( {' '} !{' '} - - shell command + - {t('shell command')} - [MCP] - Model Context Protocol - command (from external servers) + [MCP] -{' '} + {t('Model Context Protocol command (from external servers)')} {/* Shortcuts */} - Keyboard Shortcuts: + {t('Keyboard Shortcuts:')} Alt+Left/Right {' '} - - Jump through words in the input + - {t('Jump through words in the input')} Ctrl+C {' '} - - Close dialogs, cancel requests, or quit application + - {t('Close dialogs, cancel requests, or quit application')} {process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'} {' '} + -{' '} {process.platform === 'linux' - ? '- New line (Alt+Enter works for certain linux distros)' - : '- New line'} + ? t('New line (Alt+Enter works for certain linux distros)') + : t('New line')} Ctrl+L {' '} - - Clear the screen + - {t('Clear the screen')} {process.platform === 'darwin' ? 'Ctrl+X / Meta+Enter' : 'Ctrl+X'} {' '} - - Open input in external editor + - {t('Open input in external editor')} Enter {' '} - - Send message + - {t('Send message')} Esc {' '} - - Cancel operation / Clear input (double press) + - {t('Cancel operation / Clear input (double press)')} Shift+Tab {' '} - - Cycle approval modes + - {t('Cycle approval modes')} Up/Down {' '} - - Cycle through your prompt history + - {t('Cycle through your prompt history')} - For a full list of shortcuts, see{' '} - - docs/keyboard-shortcuts.md - + {t('For a full list of shortcuts, see {{docPath}}', { + docPath: t('docs/keyboard-shortcuts.md'), + })} ); diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index e5dac5fc..55b3300b 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -20,6 +20,7 @@ import { getAvailableModelsForAuthType, MAINLINE_CODER, } from '../models/availableModels.js'; +import { t } from '../../i18n/index.js'; interface ModelDialogProps { onClose: () => void; @@ -87,7 +88,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { padding={1} width="100%" > - Select Model + {t('Select Model')} - (Press Esc to close) + {t('(Press Esc to close)')} ); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 6fea96cb..40934553 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -24,6 +24,7 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { useSettings } from '../../contexts/SettingsContext.js'; import { theme } from '../../semantic-colors.js'; +import { t } from '../../../i18n/index.js'; export interface ToolConfirmationMessageProps { confirmationDetails: ToolCallConfirmationDetails; @@ -105,17 +106,17 @@ export const ToolConfirmationMessage: React.FC< const compactOptions: Array> = [ { key: 'proceed-once', - label: 'Yes, allow once', + label: t('Yes, allow once'), value: ToolConfirmationOutcome.ProceedOnce, }, { key: 'proceed-always', - label: 'Allow always', + label: t('Allow always'), value: ToolConfirmationOutcome.ProceedAlways, }, { key: 'cancel', - label: 'No', + label: t('No'), value: ToolConfirmationOutcome.Cancel, }, ]; @@ -123,7 +124,7 @@ export const ToolConfirmationMessage: React.FC< return ( - Do you want to proceed? + {t('Do you want to proceed?')} - Modify in progress: + {t('Modify in progress:')} - Save and close external editor to continue + {t('Save and close external editor to continue')} ); } - question = `Apply this change?`; + question = t('Apply this change?'); options.push({ - label: 'Yes, allow once', + label: t('Yes, allow once'), value: ToolConfirmationOutcome.ProceedOnce, key: 'Yes, allow once', }); if (isTrustedFolder) { options.push({ - label: 'Yes, allow always', + label: t('Yes, allow always'), value: ToolConfirmationOutcome.ProceedAlways, key: 'Yes, allow always', }); } if ((!config.getIdeMode() || !isDiffingEnabled) && preferredEditor) { options.push({ - label: 'Modify with external editor', + label: t('Modify with external editor'), value: ToolConfirmationOutcome.ModifyWithEditor, key: 'Modify with external editor', }); } options.push({ - label: 'No, suggest changes (esc)', + label: t('No, suggest changes (esc)'), value: ToolConfirmationOutcome.Cancel, key: 'No, suggest changes (esc)', }); @@ -232,21 +233,23 @@ export const ToolConfirmationMessage: React.FC< const executionProps = confirmationDetails as ToolExecuteConfirmationDetails; - question = `Allow execution of: '${executionProps.rootCommand}'?`; + question = t("Allow execution of: '{{command}}'?", { + command: executionProps.rootCommand, + }); options.push({ - label: 'Yes, allow once', + label: t('Yes, allow once'), value: ToolConfirmationOutcome.ProceedOnce, key: 'Yes, allow once', }); if (isTrustedFolder) { options.push({ - label: `Yes, allow always ...`, + label: t('Yes, allow always ...'), value: ToolConfirmationOutcome.ProceedAlways, - key: `Yes, allow always ...`, + key: 'Yes, allow always ...', }); } options.push({ - label: 'No, suggest changes (esc)', + label: t('No, suggest changes (esc)'), value: ToolConfirmationOutcome.Cancel, key: 'No, suggest changes (esc)', }); @@ -275,17 +278,17 @@ export const ToolConfirmationMessage: React.FC< question = planProps.title; options.push({ key: 'proceed-always', - label: 'Yes, and auto-accept edits', + label: t('Yes, and auto-accept edits'), value: ToolConfirmationOutcome.ProceedAlways, }); options.push({ key: 'proceed-once', - label: 'Yes, and manually approve edits', + label: t('Yes, and manually approve edits'), value: ToolConfirmationOutcome.ProceedOnce, }); options.push({ key: 'cancel', - label: 'No, keep planning (esc)', + label: t('No, keep planning (esc)'), value: ToolConfirmationOutcome.Cancel, }); @@ -305,21 +308,21 @@ export const ToolConfirmationMessage: React.FC< infoProps.urls && !(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt); - question = `Do you want to proceed?`; + question = t('Do you want to proceed?'); options.push({ - label: 'Yes, allow once', + label: t('Yes, allow once'), value: ToolConfirmationOutcome.ProceedOnce, key: 'Yes, allow once', }); if (isTrustedFolder) { options.push({ - label: 'Yes, allow always', + label: t('Yes, allow always'), value: ToolConfirmationOutcome.ProceedAlways, key: 'Yes, allow always', }); } options.push({ - label: 'No, suggest changes (esc)', + label: t('No, suggest changes (esc)'), value: ToolConfirmationOutcome.Cancel, key: 'No, suggest changes (esc)', }); @@ -331,7 +334,7 @@ export const ToolConfirmationMessage: React.FC< {displayUrls && infoProps.urls && infoProps.urls.length > 0 && ( - URLs to fetch: + {t('URLs to fetch:')} {infoProps.urls.map((url) => ( {' '} @@ -348,31 +351,46 @@ export const ToolConfirmationMessage: React.FC< bodyContent = ( - MCP Server: {mcpProps.serverName} - Tool: {mcpProps.toolName} + + {t('MCP Server: {{server}}', { server: mcpProps.serverName })} + + + {t('Tool: {{tool}}', { tool: mcpProps.toolName })} + ); - question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`; + question = t( + 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?', + { + tool: mcpProps.toolName, + server: mcpProps.serverName, + }, + ); options.push({ - label: 'Yes, allow once', + label: t('Yes, allow once'), value: ToolConfirmationOutcome.ProceedOnce, key: 'Yes, allow once', }); if (isTrustedFolder) { options.push({ - label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`, + label: t('Yes, always allow tool "{{tool}}" from server "{{server}}"', { + tool: mcpProps.toolName, + server: mcpProps.serverName, + }), value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated key: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`, }); options.push({ - label: `Yes, always allow all tools from server "${mcpProps.serverName}"`, + label: t('Yes, always allow all tools from server "{{server}}"', { + server: mcpProps.serverName, + }), value: ToolConfirmationOutcome.ProceedAlwaysServer, key: `Yes, always allow all tools from server "${mcpProps.serverName}"`, }); } options.push({ - label: 'No, suggest changes (esc)', + label: t('No, suggest changes (esc)'), value: ToolConfirmationOutcome.Cancel, key: 'No, suggest changes (esc)', }); diff --git a/packages/cli/src/ui/components/views/McpStatus.tsx b/packages/cli/src/ui/components/views/McpStatus.tsx index b1dcfb50..eac11b57 100644 --- a/packages/cli/src/ui/components/views/McpStatus.tsx +++ b/packages/cli/src/ui/components/views/McpStatus.tsx @@ -14,6 +14,7 @@ import type { JsonMcpPrompt, JsonMcpTool, } from '../../types.js'; +import { t } from '../../../i18n/index.js'; interface McpStatusProps { servers: Record; @@ -47,13 +48,13 @@ export const McpStatus: React.FC = ({ if (serverNames.length === 0 && blockedServers.length === 0) { return ( - No MCP servers configured. + {t('No MCP servers configured.')} - Please view MCP documentation in your browser:{' '} + {t('Please view MCP documentation in your browser:')}{' '} https://goo.gle/gemini-cli-docs-mcp {' '} - or use the cli /docs command + {t('or use the cli /docs command')} ); @@ -64,17 +65,19 @@ export const McpStatus: React.FC = ({ {discoveryInProgress && ( - ⏳ MCP servers are starting up ({connectingServers.length}{' '} - initializing)... + {t('⏳ MCP servers are starting up ({{count}} initializing)...', { + count: String(connectingServers.length), + })} - Note: First startup may take longer. Tool availability will update - automatically. + {t( + 'Note: First startup may take longer. Tool availability will update automatically.', + )} )} - Configured MCP servers: + {t('Configured MCP servers:')} {serverNames.map((serverName) => { @@ -100,50 +103,61 @@ export const McpStatus: React.FC = ({ switch (status) { case MCPServerStatus.CONNECTED: statusIndicator = '🟢'; - statusText = 'Ready'; + statusText = t('Ready'); statusColor = theme.status.success; break; case MCPServerStatus.CONNECTING: statusIndicator = '🔄'; - statusText = 'Starting... (first startup may take longer)'; + statusText = t('Starting... (first startup may take longer)'); statusColor = theme.status.warning; break; case MCPServerStatus.DISCONNECTED: default: statusIndicator = '🔴'; - statusText = 'Disconnected'; + statusText = t('Disconnected'); statusColor = theme.status.error; break; } let serverDisplayName = serverName; if (server.extensionName) { - serverDisplayName += ` (from ${server.extensionName})`; + serverDisplayName += ` ${t('(from {{extensionName}})', { + extensionName: server.extensionName, + })}`; } const toolCount = serverTools.length; const promptCount = serverPrompts.length; const parts = []; if (toolCount > 0) { - parts.push(`${toolCount} ${toolCount === 1 ? 'tool' : 'tools'}`); + parts.push( + toolCount === 1 + ? t('{{count}} tool', { count: String(toolCount) }) + : t('{{count}} tools', { count: String(toolCount) }), + ); } if (promptCount > 0) { parts.push( - `${promptCount} ${promptCount === 1 ? 'prompt' : 'prompts'}`, + promptCount === 1 + ? t('{{count}} prompt', { count: String(promptCount) }) + : t('{{count}} prompts', { count: String(promptCount) }), ); } const serverAuthStatus = authStatus[serverName]; let authStatusNode: React.ReactNode = null; if (serverAuthStatus === 'authenticated') { - authStatusNode = (OAuth); + authStatusNode = ({t('OAuth')}); } else if (serverAuthStatus === 'expired') { authStatusNode = ( - (OAuth expired) + ({t('OAuth expired')}) ); } else if (serverAuthStatus === 'unauthenticated') { authStatusNode = ( - (OAuth not authenticated) + + {' '} + ({t('OAuth not authenticated')}) + ); } @@ -162,10 +176,12 @@ export const McpStatus: React.FC = ({ {authStatusNode} {status === MCPServerStatus.CONNECTING && ( - (tools and prompts will appear when ready) + ({t('tools and prompts will appear when ready')}) )} {status === MCPServerStatus.DISCONNECTED && toolCount > 0 && ( - ({toolCount} tools cached) + + ({t('{{count}} tools cached', { count: String(toolCount) })}) + )} {showDescriptions && server?.description && ( @@ -176,7 +192,7 @@ export const McpStatus: React.FC = ({ {serverTools.length > 0 && ( - Tools: + {t('Tools:')} {serverTools.map((tool) => { const schemaContent = showSchema && @@ -204,7 +220,9 @@ export const McpStatus: React.FC = ({ )} {schemaContent && ( - Parameters: + + {t('Parameters:')} + {schemaContent} @@ -218,7 +236,7 @@ export const McpStatus: React.FC = ({ {serverPrompts.length > 0 && ( - Prompts: + {t('Prompts:')} {serverPrompts.map((prompt) => ( @@ -244,35 +262,41 @@ export const McpStatus: React.FC = ({ 🔴 {server.name} - {server.extensionName ? ` (from ${server.extensionName})` : ''} + {server.extensionName + ? ` ${t('(from {{extensionName}})', { + extensionName: server.extensionName, + })}` + : ''} - - Blocked + - {t('Blocked')} ))} {showTips && ( - 💡 Tips: + {t('💡 Tips:')} - {' '}- Use /mcp desc to show - server and tool descriptions + {' '}- {t('Use')} /mcp desc{' '} + {t('to show server and tool descriptions')} - {' '}- Use /mcp schema to - show tool parameter schemas + {' '}- {t('Use')}{' '} + /mcp schema{' '} + {t('to show tool parameter schemas')} - {' '}- Use /mcp nodesc to - hide descriptions + {' '}- {t('Use')}{' '} + /mcp nodesc{' '} + {t('to hide descriptions')} - {' '}- Use{' '} + {' '}- {t('Use')}{' '} /mcp auth <server-name>{' '} - to authenticate with OAuth-enabled servers + {t('to authenticate with OAuth-enabled servers')} - {' '}- Press Ctrl+T to - toggle tool descriptions on/off + {' '}- {t('Press')} Ctrl+T{' '} + {t('to toggle tool descriptions on/off')} )} diff --git a/packages/cli/src/ui/models/availableModels.ts b/packages/cli/src/ui/models/availableModels.ts index 312c9bdc..9a04101f 100644 --- a/packages/cli/src/ui/models/availableModels.ts +++ b/packages/cli/src/ui/models/availableModels.ts @@ -5,6 +5,7 @@ */ import { AuthType, DEFAULT_QWEN_MODEL } from '@qwen-code/qwen-code-core'; +import { t } from '../../i18n/index.js'; export type AvailableModel = { id: string; @@ -20,14 +21,20 @@ export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [ { id: MAINLINE_CODER, label: MAINLINE_CODER, - description: - 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)', + get description() { + return t( + 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)', + ); + }, }, { id: MAINLINE_VLM, label: MAINLINE_VLM, - description: - 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)', + get description() { + return t( + 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)', + ); + }, isVision: true, }, ]; diff --git a/scripts/check-i18n.ts b/scripts/check-i18n.ts new file mode 100644 index 00000000..a6c2ab07 --- /dev/null +++ b/scripts/check-i18n.ts @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { glob } from 'glob'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +interface CheckResult { + success: boolean; + errors: string[]; + warnings: string[]; + stats: { + totalKeys: number; + translatedKeys: number; + unusedKeys: string[]; + }; +} + +/** + * Load translations from JS file + */ +async function loadTranslationsFile( + filePath: string, +): Promise> { + try { + // Dynamic import for ES modules + const module = await import(filePath); + return module.default || module; + } catch (error) { + // Fallback: try reading as JSON if JS import fails + try { + const content = fs.readFileSync( + filePath.replace('.js', '.json'), + 'utf-8', + ); + return JSON.parse(content); + } catch { + throw error; + } + } +} + +/** + * Extract string literal from code, handling escaped quotes + */ +function extractStringLiteral( + content: string, + startPos: number, + quote: string, +): { value: string; endPos: number } | null { + let pos = startPos + 1; // Skip opening quote + let value = ''; + let escaped = false; + + while (pos < content.length) { + const char = content[pos]; + + if (escaped) { + if (char === '\\') { + value += '\\'; + } else if (char === quote) { + value += quote; + } else if (char === 'n') { + value += '\n'; + } else if (char === 't') { + value += '\t'; + } else if (char === 'r') { + value += '\r'; + } else { + value += char; + } + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === quote) { + return { value, endPos: pos }; + } else { + value += char; + } + + pos++; + } + + return null; // String not closed +} + +/** + * Extract all t() calls from source files + */ +async function extractUsedKeys(sourceDir: string): Promise> { + const usedKeys = new Set(); + + // Find all TypeScript/TSX files + const files = await glob('**/*.{ts,tsx}', { + cwd: sourceDir, + ignore: [ + '**/node_modules/**', + '**/dist/**', + '**/*.test.ts', + '**/*.test.tsx', + ], + }); + + for (const file of files) { + const filePath = path.join(sourceDir, file); + const content = fs.readFileSync(filePath, 'utf-8'); + + // Find all t( calls + const tCallRegex = /t\s*\(/g; + let match; + while ((match = tCallRegex.exec(content)) !== null) { + const startPos = match.index + match[0].length; + let pos = startPos; + + // Skip whitespace + while (pos < content.length && /\s/.test(content[pos])) { + pos++; + } + + if (pos >= content.length) continue; + + const char = content[pos]; + if (char === "'" || char === '"') { + const result = extractStringLiteral(content, pos, char); + if (result) { + usedKeys.add(result.value); + } + } + } + } + + return usedKeys; +} + +/** + * Check key-value consistency in en.js + */ +function checkKeyValueConsistency( + enTranslations: Record, +): string[] { + const errors: string[] = []; + + for (const [key, value] of Object.entries(enTranslations)) { + if (key !== value) { + errors.push(`Key-value mismatch: "${key}" !== "${value}"`); + } + } + + return errors; +} + +/** + * Check if en.js and zh.js have matching keys + */ +function checkKeyMatching( + enTranslations: Record, + zhTranslations: Record, +): string[] { + const errors: string[] = []; + const enKeys = new Set(Object.keys(enTranslations)); + const zhKeys = new Set(Object.keys(zhTranslations)); + + // Check for keys in en but not in zh + for (const key of enKeys) { + if (!zhKeys.has(key)) { + errors.push(`Missing translation in zh.js: "${key}"`); + } + } + + // Check for keys in zh but not in en + for (const key of zhKeys) { + if (!enKeys.has(key)) { + errors.push(`Extra key in zh.js (not in en.js): "${key}"`); + } + } + + return errors; +} + +/** + * Find unused translation keys + */ +function findUnusedKeys(allKeys: Set, usedKeys: Set): string[] { + const unused: string[] = []; + + for (const key of allKeys) { + if (!usedKeys.has(key)) { + unused.push(key); + } + } + + return unused.sort(); +} + +/** + * Main check function + */ +async function checkI18n(): Promise { + const errors: string[] = []; + const warnings: string[] = []; + + const localesDir = path.join(__dirname, '../packages/cli/src/i18n/locales'); + const sourceDir = path.join(__dirname, '../packages/cli/src'); + + const enPath = path.join(localesDir, 'en.js'); + const zhPath = path.join(localesDir, 'zh.js'); + + // Load translation files + let enTranslations: Record; + let zhTranslations: Record; + + try { + enTranslations = await loadTranslationsFile(enPath); + } catch (error) { + errors.push( + `Failed to load en.js: ${error instanceof Error ? error.message : String(error)}`, + ); + return { + success: false, + errors, + warnings, + stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] }, + }; + } + + try { + zhTranslations = await loadTranslationsFile(zhPath); + } catch (error) { + errors.push( + `Failed to load zh.js: ${error instanceof Error ? error.message : String(error)}`, + ); + return { + success: false, + errors, + warnings, + stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] }, + }; + } + + // Check key-value consistency in en.js + const consistencyErrors = checkKeyValueConsistency(enTranslations); + errors.push(...consistencyErrors); + + // Check key matching between en and zh + const matchingErrors = checkKeyMatching(enTranslations, zhTranslations); + errors.push(...matchingErrors); + + // Extract used keys from source code + const usedKeys = await extractUsedKeys(sourceDir); + + // Find unused keys + const enKeys = new Set(Object.keys(enTranslations)); + const unusedKeys = findUnusedKeys(enKeys, usedKeys); + + if (unusedKeys.length > 0) { + warnings.push(`Found ${unusedKeys.length} unused translation keys`); + } + + const totalKeys = Object.keys(enTranslations).length; + const translatedKeys = Object.keys(zhTranslations).length; + + return { + success: errors.length === 0, + errors, + warnings, + stats: { + totalKeys, + translatedKeys, + unusedKeys, + }, + }; +} + +// Run checks +checkI18n() + .then((result) => { + console.log('\n=== i18n Check Results ===\n'); + + console.log(`Total keys: ${result.stats.totalKeys}`); + console.log(`Translated keys: ${result.stats.translatedKeys}`); + console.log( + `Translation coverage: ${((result.stats.translatedKeys / result.stats.totalKeys) * 100).toFixed(1)}%\n`, + ); + + if (result.warnings.length > 0) { + console.log('⚠️ Warnings:'); + result.warnings.forEach((warning) => console.log(` - ${warning}`)); + if ( + result.stats.unusedKeys.length > 0 && + result.stats.unusedKeys.length <= 10 + ) { + console.log('\nUnused keys:'); + result.stats.unusedKeys.forEach((key) => console.log(` - "${key}"`)); + } else if (result.stats.unusedKeys.length > 10) { + console.log( + `\nUnused keys (showing first 10 of ${result.stats.unusedKeys.length}):`, + ); + result.stats.unusedKeys + .slice(0, 10) + .forEach((key) => console.log(` - "${key}"`)); + } + console.log(); + } + + if (result.errors.length > 0) { + console.log('❌ Errors:'); + result.errors.forEach((error) => console.log(` - ${error}`)); + console.log(); + process.exit(1); + } + + if (result.success) { + console.log('✅ All checks passed!\n'); + process.exit(0); + } + }) + .catch((error) => { + console.error('❌ Fatal error:', error); + process.exit(1); + }); diff --git a/scripts/copy_files.js b/scripts/copy_files.js index 6405a7df..9f1d318e 100644 --- a/scripts/copy_files.js +++ b/scripts/copy_files.js @@ -28,7 +28,7 @@ const targetDir = path.join('dist', 'src'); const extensionsToCopy = ['.md', '.json', '.sb']; -function copyFilesRecursive(source, target) { +function copyFilesRecursive(source, target, rootSourceDir) { if (!fs.existsSync(target)) { fs.mkdirSync(target, { recursive: true }); } @@ -40,14 +40,15 @@ function copyFilesRecursive(source, target) { const targetPath = path.join(target, item.name); if (item.isDirectory()) { - copyFilesRecursive(sourcePath, targetPath); + copyFilesRecursive(sourcePath, targetPath, rootSourceDir); } else { const ext = path.extname(item.name); // Copy standard extensions, or .js files in i18n/locales directory + // Use path.relative for precise matching to avoid false positives + const relativePath = path.relative(rootSourceDir, sourcePath); + const normalizedPath = relativePath.replace(/\\/g, '/'); const isLocaleJs = - ext === '.js' && - (sourcePath.includes('i18n/locales') || - sourcePath.includes(path.join('i18n', 'locales'))); + ext === '.js' && normalizedPath.startsWith('i18n/locales/'); if (extensionsToCopy.includes(ext) || isLocaleJs) { fs.copyFileSync(sourcePath, targetPath); } @@ -60,7 +61,7 @@ if (!fs.existsSync(sourceDir)) { process.exit(1); } -copyFilesRecursive(sourceDir, targetDir); +copyFilesRecursive(sourceDir, targetDir, sourceDir); // Copy example extensions into the bundle. const packageName = path.basename(process.cwd());