feat: update description

This commit is contained in:
pomelo-nwu
2025-11-17 21:42:25 +08:00
parent 6cccf9cc59
commit a33a00256d
18 changed files with 852 additions and 215 deletions

View File

@@ -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"

View File

@@ -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] <text to remember>':
'Usage: /memory add [--global|--project] <text to remember>',
'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 <file>':
'Share the current conversation to a markdown or json file. Usage: /chat share <file>',
// ============================================================================
// 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

View File

@@ -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 CodeVS 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] <text to remember>':
'用法:/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 <file>':
'将当前对话分享到 markdown 或 json 文件。用法:/chat share <file>',
// ============================================================================
// 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

View File

@@ -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<MessageActionReturn> => {
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 <tag>',
get description() {
return t(
'Save the current conversation as a checkpoint. Usage: /chat save <tag>',
);
},
kind: CommandKind.BUILT_IN,
action: async (context, args): Promise<SlashCommandActionReturn | void> => {
const tag = args.trim();
@@ -111,7 +116,7 @@ const saveCommand: SlashCommand = {
return {
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat save <tag>',
content: t('Missing tag. Usage: /chat save <tag>'),
};
}
@@ -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 <tag>',
get description() {
return t(
'Resume a conversation from a checkpoint. Usage: /chat resume <tag>',
);
},
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 <tag>',
content: t('Missing tag. Usage: /chat resume <tag>'),
};
}
@@ -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 <tag>',
get description() {
return t('Delete a conversation checkpoint. Usage: /chat delete <tag>');
},
kind: CommandKind.BUILT_IN,
action: async (context, args): Promise<MessageActionReturn> => {
const tag = args.trim();
@@ -245,7 +262,7 @@ const deleteCommand: SlashCommand = {
return {
type: 'message',
messageType: 'error',
content: 'Missing tag. Usage: /chat delete <tag>',
content: t('Missing tag. Usage: /chat delete <tag>'),
};
}
@@ -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 <file>',
get description() {
return t(
'Share the current conversation to a markdown or json file. Usage: /chat share <file>',
);
},
kind: CommandKind.BUILT_IN,
action: async (context, args): Promise<MessageActionReturn> => {
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,

View File

@@ -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 <extension-names>|--all',
get description() {
return t('Update extensions. Usage: update <extension-names>|--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) =>

View File

@@ -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<SlashCommand> => {
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<SlashCommandActionReturn> => {
const { messageType, content } =
@@ -173,7 +182,12 @@ export const ideCommand = async (): Promise<SlashCommand> => {
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<SlashCommand> => {
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<SlashCommand> => {
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(

View File

@@ -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(),
);

View File

@@ -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] <text to remember>',
),
};
}
@@ -150,8 +175,9 @@ export const memoryCommand: SlashCommand = {
return {
type: 'message',
messageType: 'error',
content:
content: t(
'Usage: /memory add [--global|--project] <text to remember>',
),
};
} 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] <text to remember>',
),
};
}
@@ -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 <text to remember>',
content: t('Usage: /memory add --project <text to remember>'),
};
}
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 <text to remember>',
content: t('Usage: /memory add --global <text to remember>'),
};
}
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(),
);

View File

@@ -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,
},
),
};
}

View File

@@ -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<string, McpClient>) => {
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);

View File

@@ -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<ContextSummaryDisplayProps> = ({
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<ContextSummaryDisplayProps> = ({
}
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<ContextSummaryDisplayProps> = ({
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<ContextSummaryDisplayProps> = ({
// 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<ContextSummaryDisplayProps> = ({
if (isNarrow) {
return (
<Box flexDirection="column">
<Text color={theme.text.secondary}>Using:</Text>
<Text color={theme.text.secondary}>{t('Using:')}</Text>
{summaryParts.map((part, index) => (
<Text key={index} color={theme.text.secondary}>
{' '}- {part}
@@ -115,7 +136,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
return (
<Box>
<Text color={theme.text.secondary}>
Using: {summaryParts.join(' | ')}
{t('Using:')} {summaryParts.join(' | ')}
</Text>
</Box>
);

View File

@@ -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<Help> = ({ commands }) => (
>
{/* Basics */}
<Text bold color={theme.text.primary}>
Basics:
{t('Basics:')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Add context
{t('Add context')}
</Text>
: Use{' '}
<Text bold color={theme.text.accent}>
@
</Text>{' '}
to specify files for context (e.g.,{' '}
<Text bold color={theme.text.accent}>
@src/myFile.ts
</Text>
) 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'),
},
)}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Shell mode
{t('Shell mode')}
</Text>
: Execute shell commands via{' '}
<Text bold color={theme.text.accent}>
!
</Text>{' '}
(e.g.,{' '}
<Text bold color={theme.text.accent}>
!npm run start
</Text>
) or use natural language (e.g.{' '}
<Text bold color={theme.text.accent}>
start server
</Text>
).
:{' '}
{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'),
},
)}
</Text>
<Box height={1} />
{/* Commands */}
<Text bold color={theme.text.primary}>
Commands:
{t('Commands:')}
</Text>
{commands
.filter((command) => command.description && !command.hidden)
@@ -97,81 +93,81 @@ export const Help: React.FC<Help> = ({ commands }) => (
{' '}
!{' '}
</Text>
- shell command
- {t('shell command')}
</Text>
<Text color={theme.text.primary}>
<Text color={theme.text.secondary}>[MCP]</Text> - Model Context Protocol
command (from external servers)
<Text color={theme.text.secondary}>[MCP]</Text> -{' '}
{t('Model Context Protocol command (from external servers)')}
</Text>
<Box height={1} />
{/* Shortcuts */}
<Text bold color={theme.text.primary}>
Keyboard Shortcuts:
{t('Keyboard Shortcuts:')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Alt+Left/Right
</Text>{' '}
- Jump through words in the input
- {t('Jump through words in the input')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Ctrl+C
</Text>{' '}
- Close dialogs, cancel requests, or quit application
- {t('Close dialogs, cancel requests, or quit application')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
{process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'}
</Text>{' '}
-{' '}
{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')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Ctrl+L
</Text>{' '}
- Clear the screen
- {t('Clear the screen')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
{process.platform === 'darwin' ? 'Ctrl+X / Meta+Enter' : 'Ctrl+X'}
</Text>{' '}
- Open input in external editor
- {t('Open input in external editor')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Enter
</Text>{' '}
- Send message
- {t('Send message')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Esc
</Text>{' '}
- Cancel operation / Clear input (double press)
- {t('Cancel operation / Clear input (double press)')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Shift+Tab
</Text>{' '}
- Cycle approval modes
- {t('Cycle approval modes')}
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Up/Down
</Text>{' '}
- Cycle through your prompt history
- {t('Cycle through your prompt history')}
</Text>
<Box height={1} />
<Text color={theme.text.primary}>
For a full list of shortcuts, see{' '}
<Text bold color={theme.text.accent}>
docs/keyboard-shortcuts.md
</Text>
{t('For a full list of shortcuts, see {{docPath}}', {
docPath: t('docs/keyboard-shortcuts.md'),
})}
</Text>
</Box>
);

View File

@@ -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%"
>
<Text bold>Select Model</Text>
<Text bold>{t('Select Model')}</Text>
<Box marginTop={1}>
<DescriptiveRadioButtonSelect
items={MODEL_OPTIONS}
@@ -97,7 +98,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
/>
</Box>
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>(Press Esc to close)</Text>
<Text color={theme.text.secondary}>{t('(Press Esc to close)')}</Text>
</Box>
</Box>
);

View File

@@ -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<RadioSelectItem<ToolConfirmationOutcome>> = [
{
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 (
<Box flexDirection="column">
<Box>
<Text wrap="truncate">Do you want to proceed?</Text>
<Text wrap="truncate">{t('Do you want to proceed?')}</Text>
</Box>
<Box>
<RadioButtonSelect
@@ -185,37 +186,37 @@ export const ToolConfirmationMessage: React.FC<
padding={1}
overflow="hidden"
>
<Text color={theme.text.primary}>Modify in progress: </Text>
<Text color={theme.text.primary}>{t('Modify in progress:')} </Text>
<Text color={theme.status.success}>
Save and close external editor to continue
{t('Save and close external editor to continue')}
</Text>
</Box>
);
}
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<
</Text>
{displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color={theme.text.primary}>URLs to fetch:</Text>
<Text color={theme.text.primary}>{t('URLs to fetch:')}</Text>
{infoProps.urls.map((url) => (
<Text key={url}>
{' '}
@@ -348,31 +351,46 @@ export const ToolConfirmationMessage: React.FC<
bodyContent = (
<Box flexDirection="column" paddingX={1} marginLeft={1}>
<Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text>
<Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text>
<Text color={theme.text.link}>
{t('MCP Server: {{server}}', { server: mcpProps.serverName })}
</Text>
<Text color={theme.text.link}>
{t('Tool: {{tool}}', { tool: mcpProps.toolName })}
</Text>
</Box>
);
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)',
});

View File

@@ -14,6 +14,7 @@ import type {
JsonMcpPrompt,
JsonMcpTool,
} from '../../types.js';
import { t } from '../../../i18n/index.js';
interface McpStatusProps {
servers: Record<string, MCPServerConfig>;
@@ -47,13 +48,13 @@ export const McpStatus: React.FC<McpStatusProps> = ({
if (serverNames.length === 0 && blockedServers.length === 0) {
return (
<Box flexDirection="column">
<Text>No MCP servers configured.</Text>
<Text>{t('No MCP servers configured.')}</Text>
<Text>
Please view MCP documentation in your browser:{' '}
{t('Please view MCP documentation in your browser:')}{' '}
<Text color={theme.text.link}>
https://goo.gle/gemini-cli-docs-mcp
</Text>{' '}
or use the cli /docs command
{t('or use the cli /docs command')}
</Text>
</Box>
);
@@ -64,17 +65,19 @@ export const McpStatus: React.FC<McpStatusProps> = ({
{discoveryInProgress && (
<Box flexDirection="column" marginBottom={1}>
<Text color={theme.status.warning}>
MCP servers are starting up ({connectingServers.length}{' '}
initializing)...
{t('⏳ MCP servers are starting up ({{count}} initializing)...', {
count: String(connectingServers.length),
})}
</Text>
<Text color={theme.text.primary}>
Note: First startup may take longer. Tool availability will update
automatically.
{t(
'Note: First startup may take longer. Tool availability will update automatically.',
)}
</Text>
</Box>
)}
<Text bold>Configured MCP servers:</Text>
<Text bold>{t('Configured MCP servers:')}</Text>
<Box height={1} />
{serverNames.map((serverName) => {
@@ -100,50 +103,61 @@ export const McpStatus: React.FC<McpStatusProps> = ({
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 = <Text> (OAuth)</Text>;
authStatusNode = <Text> ({t('OAuth')})</Text>;
} else if (serverAuthStatus === 'expired') {
authStatusNode = (
<Text color={theme.status.error}> (OAuth expired)</Text>
<Text color={theme.status.error}> ({t('OAuth expired')})</Text>
);
} else if (serverAuthStatus === 'unauthenticated') {
authStatusNode = (
<Text color={theme.status.warning}> (OAuth not authenticated)</Text>
<Text color={theme.status.warning}>
{' '}
({t('OAuth not authenticated')})
</Text>
);
}
@@ -162,10 +176,12 @@ export const McpStatus: React.FC<McpStatusProps> = ({
{authStatusNode}
</Box>
{status === MCPServerStatus.CONNECTING && (
<Text> (tools and prompts will appear when ready)</Text>
<Text> ({t('tools and prompts will appear when ready')})</Text>
)}
{status === MCPServerStatus.DISCONNECTED && toolCount > 0 && (
<Text> ({toolCount} tools cached)</Text>
<Text>
({t('{{count}} tools cached', { count: String(toolCount) })})
</Text>
)}
{showDescriptions && server?.description && (
@@ -176,7 +192,7 @@ export const McpStatus: React.FC<McpStatusProps> = ({
{serverTools.length > 0 && (
<Box flexDirection="column" marginLeft={2}>
<Text color={theme.text.primary}>Tools:</Text>
<Text color={theme.text.primary}>{t('Tools:')}</Text>
{serverTools.map((tool) => {
const schemaContent =
showSchema &&
@@ -204,7 +220,9 @@ export const McpStatus: React.FC<McpStatusProps> = ({
)}
{schemaContent && (
<Box flexDirection="column" marginLeft={4}>
<Text color={theme.text.secondary}>Parameters:</Text>
<Text color={theme.text.secondary}>
{t('Parameters:')}
</Text>
<Text color={theme.text.secondary}>
{schemaContent}
</Text>
@@ -218,7 +236,7 @@ export const McpStatus: React.FC<McpStatusProps> = ({
{serverPrompts.length > 0 && (
<Box flexDirection="column" marginLeft={2}>
<Text color={theme.text.primary}>Prompts:</Text>
<Text color={theme.text.primary}>{t('Prompts:')}</Text>
{serverPrompts.map((prompt) => (
<Box key={prompt.name} flexDirection="column">
<Text>
@@ -244,35 +262,41 @@ export const McpStatus: React.FC<McpStatusProps> = ({
<Text color={theme.status.error}>🔴 </Text>
<Text bold>
{server.name}
{server.extensionName ? ` (from ${server.extensionName})` : ''}
{server.extensionName
? ` ${t('(from {{extensionName}})', {
extensionName: server.extensionName,
})}`
: ''}
</Text>
<Text> - Blocked</Text>
<Text> - {t('Blocked')}</Text>
</Box>
))}
{showTips && (
<Box flexDirection="column" marginTop={1}>
<Text color={theme.text.accent}>💡 Tips:</Text>
<Text color={theme.text.accent}>{t('💡 Tips:')}</Text>
<Text>
{' '}- Use <Text color={theme.text.accent}>/mcp desc</Text> to show
server and tool descriptions
{' '}- {t('Use')} <Text color={theme.text.accent}>/mcp desc</Text>{' '}
{t('to show server and tool descriptions')}
</Text>
<Text>
{' '}- Use <Text color={theme.text.accent}>/mcp schema</Text> to
show tool parameter schemas
{' '}- {t('Use')}{' '}
<Text color={theme.text.accent}>/mcp schema</Text>{' '}
{t('to show tool parameter schemas')}
</Text>
<Text>
{' '}- Use <Text color={theme.text.accent}>/mcp nodesc</Text> to
hide descriptions
{' '}- {t('Use')}{' '}
<Text color={theme.text.accent}>/mcp nodesc</Text>{' '}
{t('to hide descriptions')}
</Text>
<Text>
{' '}- Use{' '}
{' '}- {t('Use')}{' '}
<Text color={theme.text.accent}>/mcp auth &lt;server-name&gt;</Text>{' '}
to authenticate with OAuth-enabled servers
{t('to authenticate with OAuth-enabled servers')}
</Text>
<Text>
{' '}- Press <Text color={theme.text.accent}>Ctrl+T</Text> to
toggle tool descriptions on/off
{' '}- {t('Press')} <Text color={theme.text.accent}>Ctrl+T</Text>{' '}
{t('to toggle tool descriptions on/off')}
</Text>
</Box>
)}

View File

@@ -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,
},
];

329
scripts/check-i18n.ts Normal file
View File

@@ -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<Record<string, string>> {
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<Set<string>> {
const usedKeys = new Set<string>();
// 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, string>,
): 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<string, string>,
zhTranslations: Record<string, string>,
): 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<string>, usedKeys: Set<string>): 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<CheckResult> {
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<string, string>;
let zhTranslations: Record<string, string>;
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);
});

View File

@@ -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());