From 48b77541c3b4d55c63fa6dad198a766570ac1fc7 Mon Sep 17 00:00:00 2001 From: pomelo Date: Fri, 21 Nov 2025 15:44:37 +0800 Subject: [PATCH] feat(i18n): Add Internationalization Support for UI and LLM Output (#1058) --- docs/cli/commands.md | 10 + docs/cli/language.md | 71 ++ package.json | 1 + packages/cli/package.json | 3 +- packages/cli/src/config/config.ts | 15 + packages/cli/src/config/settingsSchema.ts | 17 + packages/cli/src/core/initializer.ts | 9 +- packages/cli/src/core/theme.ts | 5 +- packages/cli/src/i18n/index.ts | 232 ++++ packages/cli/src/i18n/locales/en.js | 1129 +++++++++++++++++ packages/cli/src/i18n/locales/zh.js | 1052 +++++++++++++++ .../cli/src/services/BuiltinCommandLoader.ts | 2 + packages/cli/src/ui/AppContainer.tsx | 9 +- packages/cli/src/ui/auth/AuthDialog.tsx | 27 +- packages/cli/src/ui/auth/AuthInProgress.tsx | 7 +- packages/cli/src/ui/auth/useAuth.ts | 17 +- packages/cli/src/ui/commands/aboutCommand.ts | 5 +- packages/cli/src/ui/commands/agentsCommand.ts | 13 +- .../src/ui/commands/approvalModeCommand.ts | 5 +- packages/cli/src/ui/commands/authCommand.ts | 5 +- packages/cli/src/ui/commands/bugCommand.ts | 5 +- packages/cli/src/ui/commands/chatCommand.ts | 90 +- packages/cli/src/ui/commands/clearCommand.ts | 9 +- .../cli/src/ui/commands/compressCommand.ts | 15 +- packages/cli/src/ui/commands/copyCommand.ts | 5 +- .../cli/src/ui/commands/directoryCommand.tsx | 53 +- packages/cli/src/ui/commands/docsCommand.ts | 19 +- packages/cli/src/ui/commands/editorCommand.ts | 5 +- .../cli/src/ui/commands/extensionsCommand.ts | 13 +- packages/cli/src/ui/commands/helpCommand.ts | 5 +- packages/cli/src/ui/commands/ideCommand.ts | 32 +- packages/cli/src/ui/commands/initCommand.ts | 7 +- .../cli/src/ui/commands/languageCommand.ts | 458 +++++++ packages/cli/src/ui/commands/mcpCommand.ts | 71 +- packages/cli/src/ui/commands/memoryCommand.ts | 92 +- packages/cli/src/ui/commands/modelCommand.ts | 16 +- .../cli/src/ui/commands/permissionsCommand.ts | 5 +- packages/cli/src/ui/commands/quitCommand.ts | 9 +- .../cli/src/ui/commands/settingsCommand.ts | 5 +- .../cli/src/ui/commands/setupGithubCommand.ts | 5 +- packages/cli/src/ui/commands/statsCommand.ts | 15 +- .../cli/src/ui/commands/summaryCommand.ts | 36 +- .../src/ui/commands/terminalSetupCommand.ts | 15 +- packages/cli/src/ui/commands/themeCommand.ts | 5 +- packages/cli/src/ui/commands/toolsCommand.ts | 7 +- packages/cli/src/ui/commands/vimCommand.ts | 5 +- packages/cli/src/ui/components/AboutBox.tsx | 3 +- .../src/ui/components/ApprovalModeDialog.tsx | 22 +- .../src/ui/components/AutoAcceptIndicator.tsx | 13 +- packages/cli/src/ui/components/Composer.tsx | 13 +- .../src/ui/components/ConfigInitDisplay.tsx | 12 +- .../ui/components/ContextSummaryDisplay.tsx | 51 +- .../ui/components/EditorSettingsDialog.tsx | 12 +- packages/cli/src/ui/components/Help.tsx | 84 +- .../src/ui/components/InputPrompt.test.tsx | 1 + .../cli/src/ui/components/InputPrompt.tsx | 9 +- .../src/ui/components/LoadingIndicator.tsx | 8 +- .../cli/src/ui/components/ModelDialog.tsx | 5 +- .../src/ui/components/ModelStatsDisplay.tsx | 29 +- .../cli/src/ui/components/OpenAIKeyPrompt.tsx | 21 +- .../cli/src/ui/components/ProQuotaDialog.tsx | 7 +- .../ui/components/QuitConfirmationDialog.tsx | 11 +- .../src/ui/components/QwenOAuthProgress.tsx | 34 +- .../ui/components/SessionSummaryDisplay.tsx | 6 +- .../cli/src/ui/components/SettingsDialog.tsx | 18 +- .../ui/components/ShellConfirmationDialog.tsx | 13 +- .../cli/src/ui/components/StatsDisplay.tsx | 42 +- .../cli/src/ui/components/ThemeDialog.tsx | 11 +- packages/cli/src/ui/components/Tips.tsx | 11 +- .../src/ui/components/ToolStatsDisplay.tsx | 29 +- .../src/ui/components/WelcomeBackDialog.tsx | 29 +- .../SettingsDialog.test.tsx.snap | 40 +- .../messages/CompressionMessage.tsx | 21 +- .../messages/ToolConfirmationMessage.tsx | 80 +- .../ui/components/shared/ScopeSelector.tsx | 5 +- .../subagents/create/AgentCreationWizard.tsx | 89 +- .../subagents/create/CreationSummary.tsx | 56 +- .../subagents/create/DescriptionInput.tsx | 17 +- .../create/GenerationMethodSelector.tsx | 9 +- .../subagents/create/LocationSelector.tsx | 9 +- .../subagents/create/ToolSelector.tsx | 23 +- .../subagents/manage/ActionSelectionStep.tsx | 33 +- .../subagents/manage/AgentDeleteStep.tsx | 8 +- .../subagents/manage/AgentEditStep.tsx | 19 +- .../subagents/manage/AgentSelectionStep.tsx | 23 +- .../subagents/manage/AgentViewerStep.tsx | 13 +- .../subagents/manage/AgentsManagerDialog.tsx | 29 +- .../cli/src/ui/components/views/McpStatus.tsx | 94 +- .../cli/src/ui/components/views/ToolsList.tsx | 5 +- packages/cli/src/ui/hooks/usePhraseCycler.ts | 17 +- packages/cli/src/ui/hooks/useThemeCommand.ts | 17 +- packages/cli/src/ui/models/availableModels.ts | 15 +- packages/cli/src/ui/utils/terminalSetup.ts | 73 +- packages/cli/src/utils/settingsUtils.ts | 7 +- packages/cli/src/utils/systemInfoFields.ts | 29 +- scripts/check-i18n.ts | 457 +++++++ scripts/copy_files.js | 19 +- scripts/prepare-package.js | 39 +- 98 files changed, 4740 insertions(+), 636 deletions(-) create mode 100644 docs/cli/language.md create mode 100644 packages/cli/src/i18n/index.ts create mode 100644 packages/cli/src/i18n/locales/en.js create mode 100644 packages/cli/src/i18n/locales/zh.js create mode 100644 packages/cli/src/ui/commands/languageCommand.ts create mode 100644 scripts/check-i18n.ts diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 82342e0a..983c6e5e 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -195,6 +195,16 @@ Slash commands provide meta-level control over the CLI itself. - **`/init`** - **Description:** Analyzes the current directory and creates a `QWEN.md` context file by default (or the filename specified by `contextFileName`). If a non-empty file already exists, no changes are made. The command seeds an empty file and prompts the model to populate it with project-specific instructions. +- [**`/language`**](./language.md) + - **Description:** View or change the language setting for both UI and LLM output. + - **Sub-commands:** + - **`ui`**: Set the UI language (zh-CN or en-US) + - **`output`**: Set the LLM output language + - **Usage:** `/language [ui|output] [language]` + - **Examples:** + - `/language ui zh-CN` (set UI language to Simplified Chinese) + - `/language output English` (set LLM output language to English) + ### Custom Commands For a quick start, see the [example](#example-a-pure-function-refactoring-command) below. diff --git a/docs/cli/language.md b/docs/cli/language.md new file mode 100644 index 00000000..7fb1e7f0 --- /dev/null +++ b/docs/cli/language.md @@ -0,0 +1,71 @@ +# Language Command + +The `/language` command allows you to customize the language settings for both the Qwen Code user interface (UI) and the language model's output. This command supports two distinct functionalities: + +1. Setting the UI language for the Qwen Code interface +2. Setting the output language for the language model (LLM) + +## UI Language Settings + +To change the UI language of Qwen Code, use the `ui` subcommand: + +``` +/language ui [zh-CN|en-US] +``` + +### Available UI Languages + +- **zh-CN**: Simplified Chinese (简体中文) +- **en-US**: English + +### Examples + +``` +/language ui zh-CN # Set UI language to Simplified Chinese +/language ui en-US # Set UI language to English +``` + +### UI Language Subcommands + +You can also use direct subcommands for convenience: + +- `/language ui zh-CN` or `/language ui zh` or `/language ui 中文` +- `/language ui en-US` or `/language ui en` or `/language ui english` + +## LLM Output Language Settings + +To set the language for the language model's responses, use the `output` subcommand: + +``` +/language output +``` + +This command generates a language rule file that instructs the LLM to respond in the specified language. The rule file is saved to `~/.qwen/output-language.md`. + +### Examples + +``` +/language output 中文 # Set LLM output language to Chinese +/language output English # Set LLM output language to English +/language output 日本語 # Set LLM output language to Japanese +``` + +## Viewing Current Settings + +When used without arguments, the `/language` command displays the current language settings: + +``` +/language +``` + +This will show: + +- Current UI language +- Current LLM output language (if set) +- Available subcommands + +## Notes + +- UI language changes take effect immediately and reload all command descriptions +- LLM output language settings are persisted in a rule file that is automatically included in the model's context +- To request additional UI language packs, please open an issue on GitHub diff --git a/package.json b/package.json index 4cab7369..c96865aa 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "lint:all": "node scripts/lint.js", "format": "prettier --experimental-cli --write .", "typecheck": "npm run typecheck --workspaces --if-present", + "check-i18n": "npm run check-i18n --workspace=packages/cli", "preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci", "prepare": "husky && npm run bundle", "prepare:package": "node scripts/prepare-package.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index 0ba4c0ef..6d3e7f51 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -26,7 +26,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/config/config.ts b/packages/cli/src/config/config.ts index dc07c473..5be05d39 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -23,6 +23,7 @@ import { WriteFileTool, resolveTelemetrySettings, FatalConfigError, + Storage, InputFormat, OutputFormat, } from '@qwen-code/qwen-code-core'; @@ -602,6 +603,20 @@ export async function loadCliConfig( (e) => e.contextFiles, ); + // Automatically load output-language.md if it exists + const outputLanguageFilePath = path.join( + Storage.getGlobalQwenDir(), + 'output-language.md', + ); + if (fs.existsSync(outputLanguageFilePath)) { + extensionContextFilePaths.push(outputLanguageFilePath); + if (debugMode) { + logger.debug( + `Found output-language.md, adding to context files: ${outputLanguageFilePath}`, + ); + } + } + const fileService = new FileDiscoveryService(cwd); const fileFiltering = { diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index e0ece3ac..d95f4dbb 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -176,6 +176,23 @@ const SETTINGS_SCHEMA = { description: 'Enable debug logging of keystrokes to the console.', showInDialog: true, }, + language: { + type: 'enum', + label: 'Language', + category: 'General', + requiresRestart: false, + default: 'auto', + description: + 'The language for the user interface. Use "auto" to detect from system settings. ' + + 'You can also use custom language codes (e.g., "es", "fr") by placing JS language files ' + + 'in ~/.qwen/locales/ (e.g., ~/.qwen/locales/es.js).', + showInDialog: true, + options: [ + { value: 'auto', label: 'Auto (detect from system)' }, + { value: 'en', label: 'English' }, + { value: 'zh', label: '中文 (Chinese)' }, + ], + }, }, }, output: { diff --git a/packages/cli/src/core/initializer.ts b/packages/cli/src/core/initializer.ts index 870632d7..407dea44 100644 --- a/packages/cli/src/core/initializer.ts +++ b/packages/cli/src/core/initializer.ts @@ -14,6 +14,7 @@ import { import { type LoadedSettings, SettingScope } from '../config/settings.js'; import { performInitialAuth } from './auth.js'; import { validateTheme } from './theme.js'; +import { initializeI18n } from '../i18n/index.js'; export interface InitializationResult { authError: string | null; @@ -33,6 +34,13 @@ export async function initializeApp( config: Config, settings: LoadedSettings, ): Promise { + // Initialize i18n system + const languageSetting = + process.env['QWEN_CODE_LANG'] || + settings.merged.general?.language || + 'auto'; + await initializeI18n(languageSetting); + const authType = settings.merged.security?.auth?.selectedType; const authError = await performInitialAuth(config, authType); @@ -44,7 +52,6 @@ export async function initializeApp( undefined, ); } - const themeError = validateTheme(settings); const shouldOpenAuthDialog = diff --git a/packages/cli/src/core/theme.ts b/packages/cli/src/core/theme.ts index ed2805a5..7acb4abd 100644 --- a/packages/cli/src/core/theme.ts +++ b/packages/cli/src/core/theme.ts @@ -6,6 +6,7 @@ import { themeManager } from '../ui/themes/theme-manager.js'; import { type LoadedSettings } from '../config/settings.js'; +import { t } from '../i18n/index.js'; /** * Validates the configured theme. @@ -15,7 +16,9 @@ import { type LoadedSettings } from '../config/settings.js'; export function validateTheme(settings: LoadedSettings): string | null { const effectiveTheme = settings.merged.ui?.theme; if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) { - return `Theme "${effectiveTheme}" not found.`; + return t('Theme "{{themeName}}" not found.', { + themeName: effectiveTheme, + }); } return null; } diff --git a/packages/cli/src/i18n/index.ts b/packages/cli/src/i18n/index.ts new file mode 100644 index 00000000..2cad8dec --- /dev/null +++ b/packages/cli/src/i18n/index.ts @@ -0,0 +1,232 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { homedir } from 'node:os'; + +export type SupportedLanguage = 'en' | 'zh' | string; // Allow custom language codes + +// State +let currentLanguage: SupportedLanguage = 'en'; +let translations: Record = {}; + +// Cache +type TranslationDict = Record; +const translationCache: Record = {}; +const loadingPromises: Record> = {}; + +// Path helpers +const getBuiltinLocalesDir = (): string => { + const __filename = fileURLToPath(import.meta.url); + return path.join(path.dirname(__filename), 'locales'); +}; + +const getUserLocalesDir = (): string => + path.join(homedir(), '.qwen', 'locales'); + +/** + * Get the path to the user's custom locales directory. + * Users can place custom language packs (e.g., es.js, fr.js) in this directory. + * @returns The path to ~/.qwen/locales + */ +export function getUserLocalesDirectory(): string { + return getUserLocalesDir(); +} + +const getLocalePath = ( + lang: SupportedLanguage, + useUserDir: boolean = false, +): string => { + const baseDir = useUserDir ? getUserLocalesDir() : getBuiltinLocalesDir(); + return path.join(baseDir, `${lang}.js`); +}; + +// Language detection +export function detectSystemLanguage(): SupportedLanguage { + const envLang = process.env['QWEN_CODE_LANG'] || process.env['LANG']; + if (envLang?.startsWith('zh')) return 'zh'; + if (envLang?.startsWith('en')) return 'en'; + + try { + const locale = Intl.DateTimeFormat().resolvedOptions().locale; + if (locale.startsWith('zh')) return 'zh'; + } catch { + // Fallback to default + } + + return 'en'; +} + +// Translation loading +async function loadTranslationsAsync( + lang: SupportedLanguage, +): Promise { + if (translationCache[lang]) { + return translationCache[lang]; + } + + const existingPromise = loadingPromises[lang]; + if (existingPromise) { + return existingPromise; + } + + const loadPromise = (async () => { + // Try user directory first (for custom language packs), then builtin directory + const searchDirs = [ + { dir: getUserLocalesDir(), isUser: true }, + { dir: getBuiltinLocalesDir(), isUser: false }, + ]; + + for (const { dir, isUser } of searchDirs) { + // Ensure directory exists + if (!fs.existsSync(dir)) { + continue; + } + + const jsPath = getLocalePath(lang, isUser); + if (!fs.existsSync(jsPath)) { + continue; + } + + try { + // Convert file path to file:// URL for cross-platform compatibility + const fileUrl = pathToFileURL(jsPath).href; + try { + const module = await import(fileUrl); + const result = module.default || module; + if ( + result && + typeof result === 'object' && + Object.keys(result).length > 0 + ) { + translationCache[lang] = result; + return result; + } else { + throw new Error('Module loaded but result is empty or invalid'); + } + } catch { + // For builtin locales, try alternative import method (relative path) + if (!isUser) { + try { + const module = await import(`./locales/${lang}.js`); + const result = module.default || module; + if ( + result && + typeof result === 'object' && + Object.keys(result).length > 0 + ) { + translationCache[lang] = result; + return result; + } + } catch { + // Continue to next directory + } + } + // If import failed, continue to next directory + continue; + } + } catch (error) { + // Log warning but continue to next directory + if (isUser) { + console.warn( + `Failed to load translations from user directory for ${lang}:`, + error, + ); + } else { + console.warn(`Failed to load JS translations for ${lang}:`, error); + if (error instanceof Error) { + console.warn(`Error details: ${error.message}`); + console.warn(`Stack: ${error.stack}`); + } + } + // Continue to next directory + continue; + } + } + + // Return empty object if both directories fail + // Cache it to avoid repeated failed attempts + translationCache[lang] = {}; + return {}; + })(); + + loadingPromises[lang] = loadPromise; + + // Clean up promise after completion to allow retry on next call if needed + loadPromise.finally(() => { + delete loadingPromises[lang]; + }); + + return loadPromise; +} + +function loadTranslations(lang: SupportedLanguage): TranslationDict { + // Only return from cache (JS files require async loading) + return translationCache[lang] || {}; +} + +// String interpolation +function interpolate( + template: string, + params?: Record, +): string { + if (!params) return template; + return template.replace( + /\{\{(\w+)\}\}/g, + (match, key) => params[key] ?? match, + ); +} + +// Language setting helpers +function resolveLanguage(lang: SupportedLanguage | 'auto'): SupportedLanguage { + return lang === 'auto' ? detectSystemLanguage() : lang; +} + +// Public API +export function setLanguage(lang: SupportedLanguage | 'auto'): void { + const resolvedLang = resolveLanguage(lang); + currentLanguage = resolvedLang; + + // Try to load translations synchronously (from cache only) + const loaded = loadTranslations(resolvedLang); + translations = loaded; + + // Warn if translations are empty and JS file exists (requires async loading) + if (Object.keys(loaded).length === 0) { + const userJsPath = getLocalePath(resolvedLang, true); + const builtinJsPath = getLocalePath(resolvedLang, false); + if (fs.existsSync(userJsPath) || fs.existsSync(builtinJsPath)) { + console.warn( + `Language file for ${resolvedLang} requires async loading. ` + + `Use setLanguageAsync() instead, or call initializeI18n() first.`, + ); + } + } +} + +export async function setLanguageAsync( + lang: SupportedLanguage | 'auto', +): Promise { + currentLanguage = resolveLanguage(lang); + translations = await loadTranslationsAsync(currentLanguage); +} + +export function getCurrentLanguage(): SupportedLanguage { + return currentLanguage; +} + +export function t(key: string, params?: Record): string { + const translation = translations[key] ?? key; + return interpolate(translation, params); +} + +export async function initializeI18n( + lang?: SupportedLanguage | 'auto', +): Promise { + await setLanguageAsync(lang ?? 'auto'); +} diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js new file mode 100644 index 00000000..3ab57edb --- /dev/null +++ b/packages/cli/src/i18n/locales/en.js @@ -0,0 +1,1129 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +// English translations for Qwen Code CLI +// The key serves as both the translation key and the default English text + +export default { + // ============================================================================ + // Help / UI Components + // ============================================================================ + 'Basics:': 'Basics:', + 'Add context': 'Add context', + 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.': + 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.', + '@': '@', + '@src/myFile.ts': '@src/myFile.ts', + 'Shell mode': 'Shell mode', + 'YOLO mode': 'YOLO mode', + 'plan mode': 'plan mode', + 'auto-accept edits': 'auto-accept edits', + 'Accepting edits': 'Accepting edits', + '(shift + tab to cycle)': '(shift + tab to cycle)', + 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': + 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).', + '!': '!', + '!npm run start': '!npm run start', + 'start server': 'start server', + 'Commands:': 'Commands:', + 'shell command': 'shell command', + 'Model Context Protocol command (from external servers)': + 'Model Context Protocol command (from external servers)', + 'Keyboard Shortcuts:': 'Keyboard Shortcuts:', + 'Jump through words in the input': 'Jump through words in the input', + 'Close dialogs, cancel requests, or quit application': + 'Close dialogs, cancel requests, or quit application', + 'New line': 'New line', + 'New line (Alt+Enter works for certain linux distros)': + 'New line (Alt+Enter works for certain linux distros)', + 'Clear the screen': 'Clear the screen', + 'Open input in external editor': 'Open input in external editor', + 'Send message': 'Send message', + 'Initializing...': 'Initializing...', + 'Connecting to MCP servers... ({{connected}}/{{total}})': + 'Connecting to MCP servers... ({{connected}}/{{total}})', + 'Type your message or @path/to/file': 'Type your message or @path/to/file', + "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.": + "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.", + 'Cancel operation / Clear input (double press)': + 'Cancel operation / Clear input (double press)', + 'Cycle approval modes': 'Cycle approval modes', + 'Cycle through your prompt history': 'Cycle through your prompt history', + 'For a full list of shortcuts, see {{docPath}}': + 'For a full list of shortcuts, see {{docPath}}', + 'docs/keyboard-shortcuts.md': 'docs/keyboard-shortcuts.md', + 'for help on Qwen Code': 'for help on Qwen Code', + 'show version info': 'show version info', + 'submit a bug report': 'submit a bug report', + 'About Qwen Code': 'About Qwen Code', + + // ============================================================================ + // System Information Fields + // ============================================================================ + 'CLI Version': 'CLI Version', + 'Git Commit': 'Git Commit', + Model: 'Model', + Sandbox: 'Sandbox', + 'OS Platform': 'OS Platform', + 'OS Arch': 'OS Arch', + 'OS Release': 'OS Release', + 'Node.js Version': 'Node.js Version', + 'NPM Version': 'NPM Version', + 'Session ID': 'Session ID', + 'Auth Method': 'Auth Method', + 'Base URL': 'Base URL', + 'Memory Usage': 'Memory Usage', + 'IDE Client': 'IDE Client', + + // ============================================================================ + // Commands - General + // ============================================================================ + 'Analyzes the project and creates a tailored QWEN.md file.': + 'Analyzes the project and creates a tailored QWEN.md file.', + 'list available Qwen Code tools. Usage: /tools [desc]': + 'list available Qwen Code tools. Usage: /tools [desc]', + 'Available Qwen Code CLI tools:': 'Available Qwen Code CLI tools:', + 'No tools available': 'No tools available', + 'View or change the approval mode for tool usage': + 'View or change the approval mode for tool usage', + 'View or change the language setting': 'View or change the language setting', + 'change the theme': 'change the theme', + 'Select Theme': 'Select Theme', + Preview: 'Preview', + '(Use Enter to select, Tab to configure scope)': + '(Use Enter to select, Tab to configure scope)', + '(Use Enter to apply scope, Tab to select theme)': + '(Use Enter to apply scope, Tab to select theme)', + 'Theme configuration unavailable due to NO_COLOR env variable.': + 'Theme configuration unavailable due to NO_COLOR env variable.', + 'Theme "{{themeName}}" not found.': 'Theme "{{themeName}}" not found.', + 'Theme "{{themeName}}" not found in selected scope.': + 'Theme "{{themeName}}" not found in selected scope.', + 'clear the screen and conversation history': + 'clear the screen and conversation history', + 'Compresses the context by replacing it with a summary.': + 'Compresses the context by replacing it with a summary.', + 'open full Qwen Code documentation in your browser': + 'open full Qwen Code documentation in your browser', + 'Configuration not available.': 'Configuration not available.', + 'change the auth method': 'change the auth method', + 'Show quit confirmation dialog': 'Show quit confirmation dialog', + 'Copy the last result or code snippet to clipboard': + 'Copy the last result or code snippet to clipboard', + + // ============================================================================ + // Commands - Agents + // ============================================================================ + 'Manage subagents for specialized task delegation.': + 'Manage subagents for specialized task delegation.', + 'Manage existing subagents (view, edit, delete).': + 'Manage existing subagents (view, edit, delete).', + 'Create a new subagent with guided setup.': + 'Create a new subagent with guided setup.', + + // ============================================================================ + // Agents - Management Dialog + // ============================================================================ + Agents: 'Agents', + 'Choose Action': 'Choose Action', + 'Edit {{name}}': 'Edit {{name}}', + 'Edit Tools: {{name}}': 'Edit Tools: {{name}}', + 'Edit Color: {{name}}': 'Edit Color: {{name}}', + 'Delete {{name}}': 'Delete {{name}}', + 'Unknown Step': 'Unknown Step', + 'Esc to close': 'Esc to close', + 'Enter to select, ↑↓ to navigate, Esc to close': + 'Enter to select, ↑↓ to navigate, Esc to close', + 'Esc to go back': 'Esc to go back', + 'Enter to confirm, Esc to cancel': 'Enter to confirm, Esc to cancel', + 'Enter to select, ↑↓ to navigate, Esc to go back': + 'Enter to select, ↑↓ to navigate, Esc to go back', + 'Invalid step: {{step}}': 'Invalid step: {{step}}', + 'No subagents found.': 'No subagents found.', + "Use '/agents create' to create your first subagent.": + "Use '/agents create' to create your first subagent.", + '(built-in)': '(built-in)', + '(overridden by project level agent)': '(overridden by project level agent)', + 'Project Level ({{path}})': 'Project Level ({{path}})', + 'User Level ({{path}})': 'User Level ({{path}})', + 'Built-in Agents': 'Built-in Agents', + 'Using: {{count}} agents': 'Using: {{count}} agents', + 'View Agent': 'View Agent', + 'Edit Agent': 'Edit Agent', + 'Delete Agent': 'Delete Agent', + Back: 'Back', + 'No agent selected': 'No agent selected', + 'File Path: ': 'File Path: ', + 'Tools: ': 'Tools: ', + 'Color: ': 'Color: ', + 'Description:': 'Description:', + 'System Prompt:': 'System Prompt:', + 'Open in editor': 'Open in editor', + 'Edit tools': 'Edit tools', + 'Edit color': 'Edit color', + '❌ Error:': '❌ Error:', + 'Are you sure you want to delete agent "{{name}}"?': + 'Are you sure you want to delete agent "{{name}}"?', + // ============================================================================ + // Agents - Creation Wizard + // ============================================================================ + 'Project Level (.qwen/agents/)': 'Project Level (.qwen/agents/)', + 'User Level (~/.qwen/agents/)': 'User Level (~/.qwen/agents/)', + '✅ Subagent Created Successfully!': '✅ Subagent Created Successfully!', + 'Subagent "{{name}}" has been saved to {{level}} level.': + 'Subagent "{{name}}" has been saved to {{level}} level.', + 'Name: ': 'Name: ', + 'Location: ': 'Location: ', + '❌ Error saving subagent:': '❌ Error saving subagent:', + 'Warnings:': 'Warnings:', + 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent': + 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent', + 'Name "{{name}}" exists at user level - project level will take precedence': + 'Name "{{name}}" exists at user level - project level will take precedence', + 'Name "{{name}}" exists at project level - existing subagent will take precedence': + 'Name "{{name}}" exists at project level - existing subagent will take precedence', + 'Description is over {{length}} characters': + 'Description is over {{length}} characters', + 'System prompt is over {{length}} characters': + 'System prompt is over {{length}} characters', + // Agents - Creation Wizard Steps + 'Step {{n}}: Choose Location': 'Step {{n}}: Choose Location', + 'Step {{n}}: Choose Generation Method': + 'Step {{n}}: Choose Generation Method', + 'Generate with Qwen Code (Recommended)': + 'Generate with Qwen Code (Recommended)', + 'Manual Creation': 'Manual Creation', + 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)': + 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)', + 'e.g., Expert code reviewer that reviews code based on best practices...': + 'e.g., Expert code reviewer that reviews code based on best practices...', + 'Generating subagent configuration...': + 'Generating subagent configuration...', + 'Failed to generate subagent: {{error}}': + 'Failed to generate subagent: {{error}}', + 'Step {{n}}: Describe Your Subagent': 'Step {{n}}: Describe Your Subagent', + 'Step {{n}}: Enter Subagent Name': 'Step {{n}}: Enter Subagent Name', + 'Step {{n}}: Enter System Prompt': 'Step {{n}}: Enter System Prompt', + 'Step {{n}}: Enter Description': 'Step {{n}}: Enter Description', + // Agents - Tool Selection + 'Step {{n}}: Select Tools': 'Step {{n}}: Select Tools', + 'All Tools (Default)': 'All Tools (Default)', + 'All Tools': 'All Tools', + 'Read-only Tools': 'Read-only Tools', + 'Read & Edit Tools': 'Read & Edit Tools', + 'Read & Edit & Execution Tools': 'Read & Edit & Execution Tools', + 'All tools selected, including MCP tools': + 'All tools selected, including MCP tools', + 'Selected tools:': 'Selected tools:', + 'Read-only tools:': 'Read-only tools:', + 'Edit tools:': 'Edit tools:', + 'Execution tools:': 'Execution tools:', + 'Step {{n}}: Choose Background Color': 'Step {{n}}: Choose Background Color', + 'Step {{n}}: Confirm and Save': 'Step {{n}}: Confirm and Save', + // Agents - Navigation & Instructions + 'Esc to cancel': 'Esc to cancel', + 'Press Enter to save, e to save and edit, Esc to go back': + 'Press Enter to save, e to save and edit, Esc to go back', + 'Press Enter to continue, {{navigation}}Esc to {{action}}': + 'Press Enter to continue, {{navigation}}Esc to {{action}}', + cancel: 'cancel', + 'go back': 'go back', + '↑↓ to navigate, ': '↑↓ to navigate, ', + 'Enter a clear, unique name for this subagent.': + 'Enter a clear, unique name for this subagent.', + 'e.g., Code Reviewer': 'e.g., Code Reviewer', + 'Name cannot be empty.': 'Name cannot be empty.', + "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.": + "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.", + 'e.g., You are an expert code reviewer...': + 'e.g., You are an expert code reviewer...', + 'System prompt cannot be empty.': 'System prompt cannot be empty.', + 'Describe when and how this subagent should be used.': + 'Describe when and how this subagent should be used.', + 'e.g., Reviews code for best practices and potential bugs.': + 'e.g., Reviews code for best practices and potential bugs.', + 'Description cannot be empty.': 'Description cannot be empty.', + 'Failed to launch editor: {{error}}': 'Failed to launch editor: {{error}}', + 'Failed to save and edit subagent: {{error}}': + 'Failed to save and edit subagent: {{error}}', + + // ============================================================================ + // Commands - General (continued) + // ============================================================================ + 'View and edit Qwen Code settings': 'View and edit Qwen Code settings', + Settings: 'Settings', + '(Use Enter to select{{tabText}})': '(Use Enter to select{{tabText}})', + ', Tab to change focus': ', Tab to change focus', + 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': + 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.', + // ============================================================================ + // Settings Labels + // ============================================================================ + 'Vim Mode': 'Vim Mode', + 'Disable Auto Update': 'Disable Auto Update', + 'Enable Prompt Completion': 'Enable Prompt Completion', + 'Debug Keystroke Logging': 'Debug Keystroke Logging', + Language: 'Language', + 'Output Format': 'Output Format', + 'Hide Window Title': 'Hide Window Title', + 'Show Status in Title': 'Show Status in Title', + 'Hide Tips': 'Hide Tips', + 'Hide Banner': 'Hide Banner', + 'Hide Context Summary': 'Hide Context Summary', + 'Hide CWD': 'Hide CWD', + 'Hide Sandbox Status': 'Hide Sandbox Status', + 'Hide Model Info': 'Hide Model Info', + 'Hide Footer': 'Hide Footer', + 'Show Memory Usage': 'Show Memory Usage', + 'Show Line Numbers': 'Show Line Numbers', + 'Show Citations': 'Show Citations', + 'Custom Witty Phrases': 'Custom Witty Phrases', + 'Enable Welcome Back': 'Enable Welcome Back', + 'Disable Loading Phrases': 'Disable Loading Phrases', + 'Screen Reader Mode': 'Screen Reader Mode', + 'IDE Mode': 'IDE Mode', + 'Max Session Turns': 'Max Session Turns', + 'Skip Next Speaker Check': 'Skip Next Speaker Check', + 'Skip Loop Detection': 'Skip Loop Detection', + 'Skip Startup Context': 'Skip Startup Context', + 'Enable OpenAI Logging': 'Enable OpenAI Logging', + 'OpenAI Logging Directory': 'OpenAI Logging Directory', + Timeout: 'Timeout', + 'Max Retries': 'Max Retries', + 'Disable Cache Control': 'Disable Cache Control', + 'Memory Discovery Max Dirs': 'Memory Discovery Max Dirs', + 'Load Memory From Include Directories': + 'Load Memory From Include Directories', + 'Respect .gitignore': 'Respect .gitignore', + 'Respect .qwenignore': 'Respect .qwenignore', + 'Enable Recursive File Search': 'Enable Recursive File Search', + 'Disable Fuzzy Search': 'Disable Fuzzy Search', + 'Enable Interactive Shell': 'Enable Interactive Shell', + 'Show Color': 'Show Color', + 'Auto Accept': 'Auto Accept', + 'Use Ripgrep': 'Use Ripgrep', + 'Use Builtin Ripgrep': 'Use Builtin Ripgrep', + 'Enable Tool Output Truncation': 'Enable Tool Output Truncation', + 'Tool Output Truncation Threshold': 'Tool Output Truncation Threshold', + 'Tool Output Truncation Lines': 'Tool Output Truncation Lines', + 'Folder Trust': 'Folder Trust', + 'Vision Model Preview': 'Vision Model Preview', + // Settings enum options + 'Auto (detect from system)': 'Auto (detect from system)', + Text: 'Text', + JSON: 'JSON', + Plan: 'Plan', + Default: 'Default', + 'Auto Edit': 'Auto Edit', + YOLO: 'YOLO', + 'toggle vim mode on/off': 'toggle vim mode on/off', + 'check session stats. Usage: /stats [model|tools]': + 'check session stats. Usage: /stats [model|tools]', + 'Show model-specific usage statistics.': + 'Show model-specific usage statistics.', + 'Show tool-specific usage statistics.': + 'Show tool-specific usage statistics.', + 'exit the cli': 'exit the cli', + 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers': + 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers', + 'Manage workspace directories': 'Manage workspace directories', + 'Add directories to the workspace. Use comma to separate multiple paths': + 'Add directories to the workspace. Use comma to separate multiple paths', + 'Show all directories in the workspace': + 'Show all directories in the workspace', + 'set external editor preference': 'set external editor preference', + 'Manage extensions': 'Manage extensions', + 'List active extensions': 'List active extensions', + 'Update extensions. Usage: update |--all': + 'Update extensions. Usage: update |--all', + 'manage IDE integration': 'manage IDE integration', + 'check status of IDE integration': 'check status of IDE integration', + 'install required IDE companion for {{ideName}}': + '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, Trae)': + 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)', + 'Please restart your terminal for the changes to take effect.': + 'Please restart your terminal for the changes to take effect.', + 'Failed to configure terminal: {{error}}': + 'Failed to configure terminal: {{error}}', + 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.': + 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.', + '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.': + '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.', + 'File: {{file}}': 'File: {{file}}', + 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.': + 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.', + 'Error: {{error}}': 'Error: {{error}}', + 'Shift+Enter binding already exists': 'Shift+Enter binding already exists', + 'Ctrl+Enter binding already exists': 'Ctrl+Enter binding already exists', + 'Existing keybindings detected. Will not modify to avoid conflicts.': + 'Existing keybindings detected. Will not modify to avoid conflicts.', + 'Please check and modify manually if needed: {{file}}': + 'Please check and modify manually if needed: {{file}}', + 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.': + 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.', + 'Modified: {{file}}': 'Modified: {{file}}', + '{{terminalName}} keybindings already configured.': + '{{terminalName}} keybindings already configured.', + 'Failed to configure {{terminalName}}.': + 'Failed to configure {{terminalName}}.', + 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).': + 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).', + 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.': + 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.', + 'Terminal "{{terminal}}" is not supported yet.': + 'Terminal "{{terminal}}" is not supported yet.', + + // ============================================================================ + // Commands - Language + // ============================================================================ + 'Invalid language. Available: en-US, zh-CN': + 'Invalid language. Available: en-US, zh-CN', + 'Language subcommands do not accept additional arguments.': + 'Language subcommands do not accept additional arguments.', + 'Current UI language: {{lang}}': 'Current UI language: {{lang}}', + 'Current LLM output language: {{lang}}': + 'Current LLM output language: {{lang}}', + 'LLM output language not set': 'LLM output language not set', + 'Set UI language': 'Set UI language', + 'Set LLM output language': 'Set LLM output language', + 'Usage: /language ui [zh-CN|en-US]': 'Usage: /language ui [zh-CN|en-US]', + 'Usage: /language output ': 'Usage: /language output ', + 'Example: /language output 中文': 'Example: /language output 中文', + 'Example: /language output English': 'Example: /language output English', + 'Example: /language output 日本語': 'Example: /language output 日本語', + 'UI language changed to {{lang}}': 'UI language changed to {{lang}}', + 'LLM output language rule file generated at {{path}}': + 'LLM output language rule file generated at {{path}}', + 'Please restart the application for the changes to take effect.': + 'Please restart the application for the changes to take effect.', + 'Failed to generate LLM output language rule file: {{error}}': + 'Failed to generate LLM output language rule file: {{error}}', + 'Invalid command. Available subcommands:': + 'Invalid command. Available subcommands:', + 'Available subcommands:': 'Available subcommands:', + 'To request additional UI language packs, please open an issue on GitHub.': + 'To request additional UI language packs, please open an issue on GitHub.', + 'Available options:': 'Available options:', + ' - zh-CN: Simplified Chinese': ' - zh-CN: Simplified Chinese', + ' - en-US: English': ' - en-US: English', + 'Set UI language to Simplified Chinese (zh-CN)': + 'Set UI language to Simplified Chinese (zh-CN)', + 'Set UI language to English (en-US)': 'Set UI language to English (en-US)', + + // ============================================================================ + // Commands - Approval Mode + // ============================================================================ + 'Approval Mode': 'Approval Mode', + 'Current approval mode: {{mode}}': 'Current approval mode: {{mode}}', + 'Available approval modes:': 'Available approval modes:', + 'Approval mode changed to: {{mode}}': 'Approval mode changed to: {{mode}}', + 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})': + 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})', + 'Usage: /approval-mode [--session|--user|--project]': + 'Usage: /approval-mode [--session|--user|--project]', + + 'Scope subcommands do not accept additional arguments.': + 'Scope subcommands do not accept additional arguments.', + 'Plan mode - Analyze only, do not modify files or execute commands': + 'Plan mode - Analyze only, do not modify files or execute commands', + 'Default mode - Require approval for file edits or shell commands': + 'Default mode - Require approval for file edits or shell commands', + 'Auto-edit mode - Automatically approve file edits': + 'Auto-edit mode - Automatically approve file edits', + 'YOLO mode - Automatically approve all tools': + 'YOLO mode - Automatically approve all tools', + '{{mode}} mode': '{{mode}} mode', + 'Settings service is not available; unable to persist the approval mode.': + 'Settings service is not available; unable to persist the approval mode.', + 'Failed to save approval mode: {{error}}': + 'Failed to save approval mode: {{error}}', + 'Failed to change approval mode: {{error}}': + 'Failed to change approval mode: {{error}}', + 'Apply to current session only (temporary)': + 'Apply to current session only (temporary)', + 'Persist for this project/workspace': 'Persist for this project/workspace', + 'Persist for this user on this machine': + 'Persist for this user on this machine', + 'Analyze only, do not modify files or execute commands': + 'Analyze only, do not modify files or execute commands', + 'Require approval for file edits or shell commands': + 'Require approval for file edits or shell commands', + 'Automatically approve file edits': 'Automatically approve file edits', + 'Automatically approve all tools': 'Automatically approve all tools', + 'Workspace approval mode exists and takes priority. User-level change will have no effect.': + 'Workspace approval mode exists and takes priority. User-level change will have no effect.', + '(Use Enter to select, Tab to change focus)': + '(Use Enter to select, Tab to change focus)', + 'Apply To': 'Apply To', + 'User Settings': 'User Settings', + 'Workspace Settings': 'Workspace Settings', + + // ============================================================================ + // Commands - Memory + // ============================================================================ + 'Commands for interacting with memory.': + 'Commands for interacting with memory.', + 'Show the current memory contents.': 'Show the current memory contents.', + 'Show project-level memory contents.': 'Show project-level memory contents.', + 'Show global memory contents.': 'Show global memory contents.', + 'Add content to project-level memory.': + 'Add content to project-level memory.', + 'Add content to global memory.': 'Add content to global memory.', + 'Refresh the memory from the source.': 'Refresh the memory from the source.', + 'Usage: /memory add --project ': + 'Usage: /memory add --project ', + 'Usage: /memory add --global ': + 'Usage: /memory add --global ', + 'Attempting to save to project memory: "{{text}}"': + 'Attempting to save to project memory: "{{text}}"', + 'Attempting to save to global memory: "{{text}}"': + 'Attempting to save to global memory: "{{text}}"', + 'Current memory content from {{count}} file(s):': + 'Current memory content from {{count}} file(s):', + 'Memory is currently empty.': 'Memory is currently empty.', + 'Project memory file not found or is currently empty.': + 'Project memory file not found or is currently empty.', + 'Global memory file not found or is currently empty.': + 'Global memory file not found or is currently empty.', + 'Global memory is currently empty.': 'Global memory is currently empty.', + 'Global memory content:\n\n---\n{{content}}\n---': + 'Global memory content:\n\n---\n{{content}}\n---', + 'Project memory content from {{path}}:\n\n---\n{{content}}\n---': + 'Project memory content from {{path}}:\n\n---\n{{content}}\n---', + '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 + // ============================================================================ + 'Authenticate with an OAuth-enabled MCP server': + 'Authenticate with an OAuth-enabled MCP server', + 'List configured MCP servers and tools': + 'List configured MCP servers and tools', + 'Restarts MCP servers.': 'Restarts MCP servers.', + 'Config not loaded.': 'Config not loaded.', + 'Could not retrieve tool registry.': 'Could not retrieve tool registry.', + 'No MCP servers configured with OAuth authentication.': + 'No MCP servers configured with OAuth authentication.', + 'MCP servers with OAuth authentication:': + 'MCP servers with OAuth authentication:', + 'Use /mcp auth to authenticate.': + 'Use /mcp auth to authenticate.', + "MCP server '{{name}}' not found.": "MCP server '{{name}}' not found.", + "Successfully authenticated and refreshed tools for '{{name}}'.": + "Successfully authenticated and refreshed tools for '{{name}}'.", + "Failed to authenticate with MCP server '{{name}}': {{error}}": + "Failed to authenticate with MCP server '{{name}}': {{error}}", + "Re-discovering tools from '{{name}}'...": + "Re-discovering tools from '{{name}}'...", + + // ============================================================================ + // Commands - Chat + // ============================================================================ + 'Manage conversation history.': 'Manage conversation history.', + 'List saved conversation checkpoints': 'List saved conversation checkpoints', + 'No saved conversation checkpoints found.': + 'No saved conversation checkpoints found.', + 'List of saved conversations:': 'List of saved conversations:', + 'Note: Newest last, oldest first': 'Note: Newest last, oldest first', + 'Save the current conversation as a checkpoint. Usage: /chat save ': + 'Save the current conversation as a checkpoint. Usage: /chat save ', + 'Missing tag. Usage: /chat save ': + 'Missing tag. Usage: /chat save ', + 'Delete a conversation checkpoint. Usage: /chat delete ': + 'Delete a conversation checkpoint. Usage: /chat delete ', + 'Missing tag. Usage: /chat delete ': + 'Missing tag. Usage: /chat delete ', + "Conversation checkpoint '{{tag}}' has been deleted.": + "Conversation checkpoint '{{tag}}' has been deleted.", + "Error: No checkpoint found with tag '{{tag}}'.": + "Error: No checkpoint found with tag '{{tag}}'.", + 'Resume a conversation from a checkpoint. Usage: /chat resume ': + 'Resume a conversation from a checkpoint. Usage: /chat resume ', + 'Missing tag. Usage: /chat resume ': + 'Missing tag. Usage: /chat resume ', + 'No saved checkpoint found with tag: {{tag}}.': + 'No saved checkpoint found with tag: {{tag}}.', + 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?': + 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?', + 'No chat client available to save conversation.': + 'No chat client available to save conversation.', + 'Conversation checkpoint saved with tag: {{tag}}.': + 'Conversation checkpoint saved with tag: {{tag}}.', + 'No conversation found to save.': 'No conversation found to save.', + 'No chat client available to share conversation.': + 'No chat client available to share conversation.', + 'Invalid file format. Only .md and .json are supported.': + 'Invalid file format. Only .md and .json are supported.', + 'Error sharing conversation: {{error}}': + '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 + // ============================================================================ + 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md': + 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md', + 'No chat client available to generate summary.': + 'No chat client available to generate summary.', + 'Already generating summary, wait for previous request to complete': + 'Already generating summary, wait for previous request to complete', + 'No conversation found to summarize.': 'No conversation found to summarize.', + 'Failed to generate project context summary: {{error}}': + 'Failed to generate project context summary: {{error}}', + + // ============================================================================ + // Commands - Model + // ============================================================================ + 'Switch the model for this session': 'Switch the model for this session', + 'Content generator configuration not available.': + 'Content generator configuration not available.', + 'Authentication type not available.': 'Authentication type not available.', + 'No models available for the current authentication type ({{authType}}).': + 'No models available for the current authentication type ({{authType}}).', + + // ============================================================================ + // Commands - Clear + // ============================================================================ + 'Clearing terminal and resetting chat.': + 'Clearing terminal and resetting chat.', + 'Clearing terminal.': 'Clearing terminal.', + + // ============================================================================ + // Commands - Compress + // ============================================================================ + 'Already compressing, wait for previous request to complete': + 'Already compressing, wait for previous request to complete', + 'Failed to compress chat history.': 'Failed to compress chat history.', + 'Failed to compress chat history: {{error}}': + 'Failed to compress chat history: {{error}}', + 'Compressing chat history': 'Compressing chat history', + 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.': + 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.', + 'Compression was not beneficial for this history size.': + 'Compression was not beneficial for this history size.', + 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.': + 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.', + 'Could not compress chat history due to a token counting error.': + 'Could not compress chat history due to a token counting error.', + 'Chat history is already compressed.': 'Chat history is already compressed.', + + // ============================================================================ + // Commands - Directory + // ============================================================================ + 'Configuration is not available.': 'Configuration is not available.', + 'Please provide at least one path to add.': + 'Please provide at least one path to add.', + 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.': + 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.', + "Error adding '{{path}}': {{error}}": "Error adding '{{path}}': {{error}}", + 'Successfully added GEMINI.md files from the following directories if there are:\n- {{directories}}': + 'Successfully added GEMINI.md files from the following directories if there are:\n- {{directories}}', + 'Error refreshing memory: {{error}}': 'Error refreshing memory: {{error}}', + 'Successfully added directories:\n- {{directories}}': + 'Successfully added directories:\n- {{directories}}', + 'Current workspace directories:\n{{directories}}': + 'Current workspace directories:\n{{directories}}', + + // ============================================================================ + // Commands - Docs + // ============================================================================ + 'Please open the following URL in your browser to view the documentation:\n{{url}}': + 'Please open the following URL in your browser to view the documentation:\n{{url}}', + 'Opening documentation in your browser: {{url}}': + 'Opening documentation in your browser: {{url}}', + + // ============================================================================ + // Dialogs - Tool Confirmation + // ============================================================================ + 'Do you want to proceed?': 'Do you want to proceed?', + 'Yes, allow once': 'Yes, allow once', + 'Allow always': 'Allow always', + 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 + // ============================================================================ + 'Shell Command Execution': 'Shell Command Execution', + 'A custom command wants to run the following shell commands:': + 'A custom command wants to run the following shell commands:', + + // ============================================================================ + // Dialogs - Quit Confirmation + // ============================================================================ + 'What would you like to do before exiting?': + 'What would you like to do before exiting?', + 'Quit immediately (/quit)': 'Quit immediately (/quit)', + 'Generate summary and quit (/summary)': + 'Generate summary and quit (/summary)', + 'Save conversation and quit (/chat save)': + 'Save conversation and quit (/chat save)', + 'Cancel (stay in application)': 'Cancel (stay in application)', + + // ============================================================================ + // Dialogs - Pro Quota + // ============================================================================ + 'Pro quota limit reached for {{model}}.': + 'Pro quota limit reached for {{model}}.', + 'Change auth (executes the /auth command)': + 'Change auth (executes the /auth command)', + 'Continue with {{model}}': 'Continue with {{model}}', + + // ============================================================================ + // Dialogs - Welcome Back + // ============================================================================ + 'Current Plan:': 'Current Plan:', + 'Progress: {{done}}/{{total}} tasks completed': + 'Progress: {{done}}/{{total}} tasks completed', + ', {{inProgress}} in progress': ', {{inProgress}} in progress', + 'Pending Tasks:': 'Pending Tasks:', + 'What would you like to do?': 'What would you like to do?', + 'Choose how to proceed with your session:': + 'Choose how to proceed with your session:', + 'Start new chat session': 'Start new chat session', + 'Continue previous conversation': 'Continue previous conversation', + '👋 Welcome back! (Last updated: {{timeAgo}})': + '👋 Welcome back! (Last updated: {{timeAgo}})', + '🎯 Overall Goal:': '🎯 Overall Goal:', + + // ============================================================================ + // Dialogs - Auth + // ============================================================================ + 'Get started': 'Get started', + 'How would you like to authenticate for this project?': + 'How would you like to authenticate for this project?', + 'OpenAI API key is required to use OpenAI authentication.': + 'OpenAI API key is required to use OpenAI authentication.', + 'You must select an auth method to proceed. Press Ctrl+C again to exit.': + 'You must select an auth method to proceed. Press Ctrl+C again to exit.', + '(Use Enter to Set Auth)': '(Use Enter to Set Auth)', + 'Terms of Services and Privacy Notice for Qwen Code': + 'Terms of Services and Privacy Notice for Qwen Code', + 'Qwen OAuth': 'Qwen OAuth', + OpenAI: 'OpenAI', + 'Failed to login. Message: {{message}}': + 'Failed to login. Message: {{message}}', + 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.': + 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.', + 'Qwen OAuth authentication timed out. Please try again.': + 'Qwen OAuth authentication timed out. Please try again.', + 'Qwen OAuth authentication cancelled.': + 'Qwen OAuth authentication cancelled.', + 'Qwen OAuth Authentication': 'Qwen OAuth Authentication', + 'Please visit this URL to authorize:': 'Please visit this URL to authorize:', + 'Or scan the QR code below:': 'Or scan the QR code below:', + 'Waiting for authorization': 'Waiting for authorization', + 'Time remaining:': 'Time remaining:', + '(Press ESC or CTRL+C to cancel)': '(Press ESC or CTRL+C to cancel)', + 'Qwen OAuth Authentication Timeout': 'Qwen OAuth Authentication Timeout', + 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.': + 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.', + 'Press any key to return to authentication type selection.': + 'Press any key to return to authentication type selection.', + 'Waiting for Qwen OAuth authentication...': + 'Waiting for Qwen OAuth authentication...', + 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.': + 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.', + 'Authentication timed out. Please try again.': + 'Authentication timed out. Please try again.', + 'Waiting for auth... (Press ESC or CTRL+C to cancel)': + 'Waiting for auth... (Press ESC or CTRL+C to cancel)', + 'Failed to authenticate. Message: {{message}}': + 'Failed to authenticate. Message: {{message}}', + 'Authenticated successfully with {{authType}} credentials.': + 'Authenticated successfully with {{authType}} credentials.', + 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}': + 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}', + 'OpenAI Configuration Required': 'OpenAI Configuration Required', + 'Please enter your OpenAI configuration. You can get an API key from': + 'Please enter your OpenAI configuration. You can get an API key from', + 'API Key:': 'API Key:', + 'Invalid credentials: {{errorMessage}}': + 'Invalid credentials: {{errorMessage}}', + 'Failed to validate credentials': 'Failed to validate credentials', + 'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel': + 'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel', + + // ============================================================================ + // 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 + // ============================================================================ + 'Manage folder trust settings': 'Manage folder trust settings', + + // ============================================================================ + // Status Bar + // ============================================================================ + 'Using:': 'Using:', + '{{count}} open file': '{{count}} open file', + '{{count}} open files': '{{count}} open files', + '(ctrl+g to view)': '(ctrl+g to view)', + '{{count}} {{name}} file': '{{count}} {{name}} file', + '{{count}} {{name}} files': '{{count}} {{name}} files', + '{{count}} MCP server': '{{count}} MCP server', + '{{count}} MCP servers': '{{count}} MCP servers', + '{{count}} Blocked': '{{count}} Blocked', + '(ctrl+t to view)': '(ctrl+t to view)', + '(ctrl+t to toggle)': '(ctrl+t to toggle)', + 'Press Ctrl+C again to exit.': 'Press Ctrl+C again to exit.', + 'Press Ctrl+D again to exit.': 'Press Ctrl+D again to exit.', + 'Press Esc again to clear.': 'Press Esc again to clear.', + + // ============================================================================ + // MCP Status + // ============================================================================ + 'No MCP servers configured.': 'No MCP servers configured.', + 'Please view MCP documentation in your browser:': + 'Please view MCP documentation in your browser:', + 'or use the cli /docs command': 'or use the cli /docs command', + '⏳ MCP servers are starting up ({{count}} initializing)...': + '⏳ MCP servers are starting up ({{count}} initializing)...', + 'Note: First startup may take longer. Tool availability will update automatically.': + 'Note: First startup may take longer. Tool availability will update automatically.', + 'Configured MCP servers:': 'Configured MCP servers:', + Ready: 'Ready', + 'Starting... (first startup may take longer)': + 'Starting... (first startup may take longer)', + Disconnected: 'Disconnected', + '{{count}} tool': '{{count}} tool', + '{{count}} tools': '{{count}} tools', + '{{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 + // ============================================================================ + 'Tips for getting started:': 'Tips for getting started:', + '1. Ask questions, edit files, or run commands.': + '1. Ask questions, edit files, or run commands.', + '2. Be specific for the best results.': + '2. Be specific for the best results.', + 'files to customize your interactions with Qwen Code.': + 'files to customize your interactions with Qwen Code.', + 'for more information.': 'for more information.', + + // ============================================================================ + // Exit Screen / Stats + // ============================================================================ + 'Agent powering down. Goodbye!': 'Agent powering down. Goodbye!', + 'Interaction Summary': 'Interaction Summary', + 'Session ID:': 'Session ID:', + 'Tool Calls:': 'Tool Calls:', + 'Success Rate:': 'Success Rate:', + 'User Agreement:': 'User Agreement:', + reviewed: 'reviewed', + 'Code Changes:': 'Code Changes:', + Performance: 'Performance', + 'Wall Time:': 'Wall Time:', + 'Agent Active:': 'Agent Active:', + 'API Time:': 'API Time:', + 'Tool Time:': 'Tool Time:', + 'Session Stats': 'Session Stats', + 'Model Usage': 'Model Usage', + Reqs: 'Reqs', + 'Input Tokens': 'Input Tokens', + 'Output Tokens': 'Output Tokens', + 'Savings Highlight:': 'Savings Highlight:', + 'of input tokens were served from the cache, reducing costs.': + 'of input tokens were served from the cache, reducing costs.', + 'Tip: For a full token breakdown, run `/stats model`.': + 'Tip: For a full token breakdown, run `/stats model`.', + 'Model Stats For Nerds': 'Model Stats For Nerds', + 'Tool Stats For Nerds': 'Tool Stats For Nerds', + Metric: 'Metric', + API: 'API', + Requests: 'Requests', + Errors: 'Errors', + 'Avg Latency': 'Avg Latency', + Tokens: 'Tokens', + Total: 'Total', + Prompt: 'Prompt', + Cached: 'Cached', + Thoughts: 'Thoughts', + Tool: 'Tool', + Output: 'Output', + 'No API calls have been made in this session.': + 'No API calls have been made in this session.', + 'Tool Name': 'Tool Name', + Calls: 'Calls', + 'Success Rate': 'Success Rate', + 'Avg Duration': 'Avg Duration', + 'User Decision Summary': 'User Decision Summary', + 'Total Reviewed Suggestions:': 'Total Reviewed Suggestions:', + ' » Accepted:': ' » Accepted:', + ' » Rejected:': ' » Rejected:', + ' » Modified:': ' » Modified:', + ' Overall Agreement Rate:': ' Overall Agreement Rate:', + 'No tool calls have been made in this session.': + 'No tool calls have been made in this session.', + 'Session start time is unavailable, cannot calculate stats.': + 'Session start time is unavailable, cannot calculate stats.', + + // ============================================================================ + // Loading Phrases + // ============================================================================ + 'Waiting for user confirmation...': 'Waiting for user confirmation...', + '(esc to cancel, {{time}})': '(esc to cancel, {{time}})', + "I'm Feeling Lucky": "I'm Feeling Lucky", + 'Shipping awesomeness... ': 'Shipping awesomeness... ', + 'Painting the serifs back on...': 'Painting the serifs back on...', + 'Navigating the slime mold...': 'Navigating the slime mold...', + 'Consulting the digital spirits...': 'Consulting the digital spirits...', + 'Reticulating splines...': 'Reticulating splines...', + 'Warming up the AI hamsters...': 'Warming up the AI hamsters...', + 'Asking the magic conch shell...': 'Asking the magic conch shell...', + 'Generating witty retort...': 'Generating witty retort...', + 'Polishing the algorithms...': 'Polishing the algorithms...', + "Don't rush perfection (or my code)...": + "Don't rush perfection (or my code)...", + 'Brewing fresh bytes...': 'Brewing fresh bytes...', + 'Counting electrons...': 'Counting electrons...', + 'Engaging cognitive processors...': 'Engaging cognitive processors...', + 'Checking for syntax errors in the universe...': + 'Checking for syntax errors in the universe...', + 'One moment, optimizing humor...': 'One moment, optimizing humor...', + 'Shuffling punchlines...': 'Shuffling punchlines...', + 'Untangling neural nets...': 'Untangling neural nets...', + 'Compiling brilliance...': 'Compiling brilliance...', + 'Loading wit.exe...': 'Loading wit.exe...', + 'Summoning the cloud of wisdom...': 'Summoning the cloud of wisdom...', + 'Preparing a witty response...': 'Preparing a witty response...', + "Just a sec, I'm debugging reality...": + "Just a sec, I'm debugging reality...", + 'Confuzzling the options...': 'Confuzzling the options...', + 'Tuning the cosmic frequencies...': 'Tuning the cosmic frequencies...', + 'Crafting a response worthy of your patience...': + 'Crafting a response worthy of your patience...', + 'Compiling the 1s and 0s...': 'Compiling the 1s and 0s...', + 'Resolving dependencies... and existential crises...': + 'Resolving dependencies... and existential crises...', + 'Defragmenting memories... both RAM and personal...': + 'Defragmenting memories... both RAM and personal...', + 'Rebooting the humor module...': 'Rebooting the humor module...', + 'Caching the essentials (mostly cat memes)...': + 'Caching the essentials (mostly cat memes)...', + 'Optimizing for ludicrous speed': 'Optimizing for ludicrous speed', + "Swapping bits... don't tell the bytes...": + "Swapping bits... don't tell the bytes...", + 'Garbage collecting... be right back...': + 'Garbage collecting... be right back...', + 'Assembling the interwebs...': 'Assembling the interwebs...', + 'Converting coffee into code...': 'Converting coffee into code...', + 'Updating the syntax for reality...': 'Updating the syntax for reality...', + 'Rewiring the synapses...': 'Rewiring the synapses...', + 'Looking for a misplaced semicolon...': + 'Looking for a misplaced semicolon...', + "Greasin' the cogs of the machine...": "Greasin' the cogs of the machine...", + 'Pre-heating the servers...': 'Pre-heating the servers...', + 'Calibrating the flux capacitor...': 'Calibrating the flux capacitor...', + 'Engaging the improbability drive...': 'Engaging the improbability drive...', + 'Channeling the Force...': 'Channeling the Force...', + 'Aligning the stars for optimal response...': + 'Aligning the stars for optimal response...', + 'So say we all...': 'So say we all...', + 'Loading the next great idea...': 'Loading the next great idea...', + "Just a moment, I'm in the zone...": "Just a moment, I'm in the zone...", + 'Preparing to dazzle you with brilliance...': + 'Preparing to dazzle you with brilliance...', + "Just a tick, I'm polishing my wit...": + "Just a tick, I'm polishing my wit...", + "Hold tight, I'm crafting a masterpiece...": + "Hold tight, I'm crafting a masterpiece...", + "Just a jiffy, I'm debugging the universe...": + "Just a jiffy, I'm debugging the universe...", + "Just a moment, I'm aligning the pixels...": + "Just a moment, I'm aligning the pixels...", + "Just a sec, I'm optimizing the humor...": + "Just a sec, I'm optimizing the humor...", + "Just a moment, I'm tuning the algorithms...": + "Just a moment, I'm tuning the algorithms...", + 'Warp speed engaged...': 'Warp speed engaged...', + 'Mining for more Dilithium crystals...': + 'Mining for more Dilithium crystals...', + "Don't panic...": "Don't panic...", + 'Following the white rabbit...': 'Following the white rabbit...', + 'The truth is in here... somewhere...': + 'The truth is in here... somewhere...', + 'Blowing on the cartridge...': 'Blowing on the cartridge...', + 'Loading... Do a barrel roll!': 'Loading... Do a barrel roll!', + 'Waiting for the respawn...': 'Waiting for the respawn...', + 'Finishing the Kessel Run in less than 12 parsecs...': + 'Finishing the Kessel Run in less than 12 parsecs...', + "The cake is not a lie, it's just still loading...": + "The cake is not a lie, it's just still loading...", + 'Fiddling with the character creation screen...': + 'Fiddling with the character creation screen...', + "Just a moment, I'm finding the right meme...": + "Just a moment, I'm finding the right meme...", + "Pressing 'A' to continue...": "Pressing 'A' to continue...", + 'Herding digital cats...': 'Herding digital cats...', + 'Polishing the pixels...': 'Polishing the pixels...', + 'Finding a suitable loading screen pun...': + 'Finding a suitable loading screen pun...', + 'Distracting you with this witty phrase...': + 'Distracting you with this witty phrase...', + 'Almost there... probably...': 'Almost there... probably...', + 'Our hamsters are working as fast as they can...': + 'Our hamsters are working as fast as they can...', + 'Giving Cloudy a pat on the head...': 'Giving Cloudy a pat on the head...', + 'Petting the cat...': 'Petting the cat...', + 'Rickrolling my boss...': 'Rickrolling my boss...', + 'Never gonna give you up, never gonna let you down...': + 'Never gonna give you up, never gonna let you down...', + 'Slapping the bass...': 'Slapping the bass...', + 'Tasting the snozberries...': 'Tasting the snozberries...', + "I'm going the distance, I'm going for speed...": + "I'm going the distance, I'm going for speed...", + 'Is this the real life? Is this just fantasy?...': + 'Is this the real life? Is this just fantasy?...', + "I've got a good feeling about this...": + "I've got a good feeling about this...", + 'Poking the bear...': 'Poking the bear...', + 'Doing research on the latest memes...': + 'Doing research on the latest memes...', + 'Figuring out how to make this more witty...': + 'Figuring out how to make this more witty...', + 'Hmmm... let me think...': 'Hmmm... let me think...', + 'What do you call a fish with no eyes? A fsh...': + 'What do you call a fish with no eyes? A fsh...', + 'Why did the computer go to therapy? It had too many bytes...': + 'Why did the computer go to therapy? It had too many bytes...', + "Why don't programmers like nature? It has too many bugs...": + "Why don't programmers like nature? It has too many bugs...", + 'Why do programmers prefer dark mode? Because light attracts bugs...': + 'Why do programmers prefer dark mode? Because light attracts bugs...', + 'Why did the developer go broke? Because they used up all their cache...': + 'Why did the developer go broke? Because they used up all their cache...', + "What can you do with a broken pencil? Nothing, it's pointless...": + "What can you do with a broken pencil? Nothing, it's pointless...", + 'Applying percussive maintenance...': 'Applying percussive maintenance...', + 'Searching for the correct USB orientation...': + 'Searching for the correct USB orientation...', + 'Ensuring the magic smoke stays inside the wires...': + 'Ensuring the magic smoke stays inside the wires...', + 'Rewriting in Rust for no particular reason...': + 'Rewriting in Rust for no particular reason...', + 'Trying to exit Vim...': 'Trying to exit Vim...', + 'Spinning up the hamster wheel...': 'Spinning up the hamster wheel...', + "That's not a bug, it's an undocumented feature...": + "That's not a bug, it's an undocumented feature...", + 'Engage.': 'Engage.', + "I'll be back... with an answer.": "I'll be back... with an answer.", + 'My other process is a TARDIS...': 'My other process is a TARDIS...', + 'Communing with the machine spirit...': + 'Communing with the machine spirit...', + 'Letting the thoughts marinate...': 'Letting the thoughts marinate...', + 'Just remembered where I put my keys...': + 'Just remembered where I put my keys...', + 'Pondering the orb...': 'Pondering the orb...', + "I've seen things you people wouldn't believe... like a user who reads loading messages.": + "I've seen things you people wouldn't believe... like a user who reads loading messages.", + 'Initiating thoughtful gaze...': 'Initiating thoughtful gaze...', + "What's a computer's favorite snack? Microchips.": + "What's a computer's favorite snack? Microchips.", + "Why do Java developers wear glasses? Because they don't C#.": + "Why do Java developers wear glasses? Because they don't C#.", + 'Charging the laser... pew pew!': 'Charging the laser... pew pew!', + 'Dividing by zero... just kidding!': 'Dividing by zero... just kidding!', + 'Looking for an adult superviso... I mean, processing.': + 'Looking for an adult superviso... I mean, processing.', + 'Making it go beep boop.': 'Making it go beep boop.', + 'Buffering... because even AIs need a moment.': + 'Buffering... because even AIs need a moment.', + 'Entangling quantum particles for a faster response...': + 'Entangling quantum particles for a faster response...', + 'Polishing the chrome... on the algorithms.': + 'Polishing the chrome... on the algorithms.', + 'Are you not entertained? (Working on it!)': + 'Are you not entertained? (Working on it!)', + 'Summoning the code gremlins... to help, of course.': + 'Summoning the code gremlins... to help, of course.', + 'Just waiting for the dial-up tone to finish...': + 'Just waiting for the dial-up tone to finish...', + 'Recalibrating the humor-o-meter.': 'Recalibrating the humor-o-meter.', + 'My other loading screen is even funnier.': + 'My other loading screen is even funnier.', + "Pretty sure there's a cat walking on the keyboard somewhere...": + "Pretty sure there's a cat walking on the keyboard somewhere...", + 'Enhancing... Enhancing... Still loading.': + 'Enhancing... Enhancing... Still loading.', + "It's not a bug, it's a feature... of this loading screen.": + "It's not a bug, it's a feature... of this loading screen.", + 'Have you tried turning it off and on again? (The loading screen, not me.)': + 'Have you tried turning it off and on again? (The loading screen, not me.)', + 'Constructing additional pylons...': 'Constructing additional pylons...', +}; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js new file mode 100644 index 00000000..474753ae --- /dev/null +++ b/packages/cli/src/i18n/locales/zh.js @@ -0,0 +1,1052 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +// Chinese translations for Qwen Code CLI + +export default { + // ============================================================================ + // Help / UI Components + // ============================================================================ + 'Basics:': '基础功能:', + 'Add context': '添加上下文', + 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.': + '使用 {{symbol}} 指定文件作为上下文(例如,{{example}}),用于定位特定文件或文件夹', + '@': '@', + '@src/myFile.ts': '@src/myFile.ts', + 'Shell mode': 'Shell 模式', + 'YOLO mode': 'YOLO 模式', + 'plan mode': '规划模式', + 'auto-accept edits': '自动接受编辑', + 'Accepting edits': '接受编辑', + '(shift + tab to cycle)': '(shift + tab 切换)', + 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': + '通过 {{symbol}} 执行 shell 命令(例如,{{example1}})或使用自然语言(例如,{{example2}})', + '!': '!', + '!npm run start': '!npm run start', + 'start server': 'start server', + 'Commands:': '命令:', + 'shell command': 'shell 命令', + 'Model Context Protocol command (from external servers)': + '模型上下文协议命令(来自外部服务器)', + 'Keyboard Shortcuts:': '键盘快捷键:', + 'Jump through words in the input': '在输入中按单词跳转', + 'Close dialogs, cancel requests, or quit application': + '关闭对话框、取消请求或退出应用程序', + 'New line': '换行', + 'New line (Alt+Enter works for certain linux distros)': + '换行(某些 Linux 发行版支持 Alt+Enter)', + 'Clear the screen': '清屏', + 'Open input in external editor': '在外部编辑器中打开输入', + 'Send message': '发送消息', + 'Initializing...': '正在初始化...', + 'Connecting to MCP servers... ({{connected}}/{{total}})': + '正在连接到 MCP 服务器... ({{connected}}/{{total}})', + 'Type your message or @path/to/file': '输入您的消息或 @ 文件路径', + "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.": + "按 'i' 进入插入模式,按 'Esc' 进入普通模式", + 'Cancel operation / Clear input (double press)': + '取消操作 / 清空输入(双击)', + 'Cycle approval modes': '循环切换审批模式', + 'Cycle through your prompt history': '循环浏览提示历史', + 'For a full list of shortcuts, see {{docPath}}': + '完整快捷键列表,请参阅 {{docPath}}', + 'docs/keyboard-shortcuts.md': 'docs/keyboard-shortcuts.md', + 'for help on Qwen Code': '获取 Qwen Code 帮助', + 'show version info': '显示版本信息', + 'submit a bug report': '提交错误报告', + 'About Qwen Code': '关于 Qwen Code', + + // ============================================================================ + // System Information Fields + // ============================================================================ + 'CLI Version': 'CLI 版本', + 'Git Commit': 'Git 提交', + Model: '模型', + Sandbox: '沙箱', + 'OS Platform': '操作系统平台', + 'OS Arch': '操作系统架构', + 'OS Release': '操作系统版本', + 'Node.js Version': 'Node.js 版本', + 'NPM Version': 'NPM 版本', + 'Session ID': '会话 ID', + 'Auth Method': '认证方式', + 'Base URL': '基础 URL', + 'Memory Usage': '内存使用', + 'IDE Client': 'IDE 客户端', + + // ============================================================================ + // Commands - General + // ============================================================================ + 'Analyzes the project and creates a tailored QWEN.md file.': + '分析项目并创建定制的 QWEN.md 文件', + 'list available Qwen Code tools. Usage: /tools [desc]': + '列出可用的 Qwen Code 工具。用法:/tools [desc]', + 'Available Qwen Code CLI tools:': '可用的 Qwen Code CLI 工具:', + 'No tools available': '没有可用工具', + 'View or change the approval mode for tool usage': + '查看或更改工具使用的审批模式', + 'View or change the language setting': '查看或更改语言设置', + 'change the theme': '更改主题', + 'Select Theme': '选择主题', + Preview: '预览', + '(Use Enter to select, Tab to configure scope)': + '(使用 Enter 选择,Tab 配置作用域)', + '(Use Enter to apply scope, Tab to select theme)': + '(使用 Enter 应用作用域,Tab 选择主题)', + 'Theme configuration unavailable due to NO_COLOR env variable.': + '由于 NO_COLOR 环境变量,主题配置不可用。', + 'Theme "{{themeName}}" not found.': '未找到主题 "{{themeName}}"。', + 'Theme "{{themeName}}" not found in selected scope.': + '在所选作用域中未找到主题 "{{themeName}}"。', + 'clear the screen and conversation history': '清屏并清除对话历史', + 'Compresses the context by replacing it with a summary.': + '通过用摘要替换来压缩上下文', + 'open full Qwen Code documentation in your browser': + '在浏览器中打开完整的 Qwen Code 文档', + 'Configuration not available.': '配置不可用', + 'change the auth method': '更改认证方法', + 'Show quit confirmation dialog': '显示退出确认对话框', + 'Copy the last result or code snippet to clipboard': + '将最后的结果或代码片段复制到剪贴板', + + // ============================================================================ + // Commands - Agents + // ============================================================================ + 'Manage subagents for specialized task delegation.': + '管理用于专门任务委派的子代理', + 'Manage existing subagents (view, edit, delete).': + '管理现有子代理(查看、编辑、删除)', + 'Create a new subagent with guided setup.': '通过引导式设置创建新的子代理', + + // ============================================================================ + // Agents - Management Dialog + // ============================================================================ + Agents: '代理', + 'Choose Action': '选择操作', + 'Edit {{name}}': '编辑 {{name}}', + 'Edit Tools: {{name}}': '编辑工具: {{name}}', + 'Edit Color: {{name}}': '编辑颜色: {{name}}', + 'Delete {{name}}': '删除 {{name}}', + 'Unknown Step': '未知步骤', + 'Esc to close': '按 Esc 关闭', + 'Enter to select, ↑↓ to navigate, Esc to close': + 'Enter 选择,↑↓ 导航,Esc 关闭', + 'Esc to go back': '按 Esc 返回', + 'Enter to confirm, Esc to cancel': 'Enter 确认,Esc 取消', + 'Enter to select, ↑↓ to navigate, Esc to go back': + 'Enter 选择,↑↓ 导航,Esc 返回', + 'Invalid step: {{step}}': '无效步骤: {{step}}', + 'No subagents found.': '未找到子代理。', + "Use '/agents create' to create your first subagent.": + "使用 '/agents create' 创建您的第一个子代理。", + '(built-in)': '(内置)', + '(overridden by project level agent)': '(已被项目级代理覆盖)', + 'Project Level ({{path}})': '项目级 ({{path}})', + 'User Level ({{path}})': '用户级 ({{path}})', + 'Built-in Agents': '内置代理', + 'Using: {{count}} agents': '使用中: {{count}} 个代理', + 'View Agent': '查看代理', + 'Edit Agent': '编辑代理', + 'Delete Agent': '删除代理', + Back: '返回', + 'No agent selected': '未选择代理', + 'File Path: ': '文件路径: ', + 'Tools: ': '工具: ', + 'Color: ': '颜色: ', + 'Description:': '描述:', + 'System Prompt:': '系统提示:', + 'Open in editor': '在编辑器中打开', + 'Edit tools': '编辑工具', + 'Edit color': '编辑颜色', + '❌ Error:': '❌ 错误:', + 'Are you sure you want to delete agent "{{name}}"?': + '您确定要删除代理 "{{name}}" 吗?', + // ============================================================================ + // Agents - Creation Wizard + // ============================================================================ + 'Project Level (.qwen/agents/)': '项目级 (.qwen/agents/)', + 'User Level (~/.qwen/agents/)': '用户级 (~/.qwen/agents/)', + '✅ Subagent Created Successfully!': '✅ 子代理创建成功!', + 'Subagent "{{name}}" has been saved to {{level}} level.': + '子代理 "{{name}}" 已保存到 {{level}} 级别。', + 'Name: ': '名称: ', + 'Location: ': '位置: ', + '❌ Error saving subagent:': '❌ 保存子代理时出错:', + 'Warnings:': '警告:', + 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent': + '名称 "{{name}}" 在 {{level}} 级别已存在 - 将覆盖现有子代理', + 'Name "{{name}}" exists at user level - project level will take precedence': + '名称 "{{name}}" 在用户级别存在 - 项目级别将优先', + 'Name "{{name}}" exists at project level - existing subagent will take precedence': + '名称 "{{name}}" 在项目级别存在 - 现有子代理将优先', + 'Description is over {{length}} characters': '描述超过 {{length}} 个字符', + 'System prompt is over {{length}} characters': + '系统提示超过 {{length}} 个字符', + // Agents - Creation Wizard Steps + 'Step {{n}}: Choose Location': '步骤 {{n}}: 选择位置', + 'Step {{n}}: Choose Generation Method': '步骤 {{n}}: 选择生成方式', + 'Generate with Qwen Code (Recommended)': '使用 Qwen Code 生成(推荐)', + 'Manual Creation': '手动创建', + 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)': + '描述此子代理应该做什么以及何时使用它。(为了获得最佳效果,请全面描述)', + 'e.g., Expert code reviewer that reviews code based on best practices...': + '例如:专业的代码审查员,根据最佳实践审查代码...', + 'Generating subagent configuration...': '正在生成子代理配置...', + 'Failed to generate subagent: {{error}}': '生成子代理失败: {{error}}', + 'Step {{n}}: Describe Your Subagent': '步骤 {{n}}: 描述您的子代理', + 'Step {{n}}: Enter Subagent Name': '步骤 {{n}}: 输入子代理名称', + 'Step {{n}}: Enter System Prompt': '步骤 {{n}}: 输入系统提示', + 'Step {{n}}: Enter Description': '步骤 {{n}}: 输入描述', + // Agents - Tool Selection + 'Step {{n}}: Select Tools': '步骤 {{n}}: 选择工具', + 'All Tools (Default)': '所有工具(默认)', + 'All Tools': '所有工具', + 'Read-only Tools': '只读工具', + 'Read & Edit Tools': '读取和编辑工具', + 'Read & Edit & Execution Tools': '读取、编辑和执行工具', + 'All tools selected, including MCP tools': '已选择所有工具,包括 MCP 工具', + 'Selected tools:': '已选择的工具:', + 'Read-only tools:': '只读工具:', + 'Edit tools:': '编辑工具:', + 'Execution tools:': '执行工具:', + 'Step {{n}}: Choose Background Color': '步骤 {{n}}: 选择背景颜色', + 'Step {{n}}: Confirm and Save': '步骤 {{n}}: 确认并保存', + // Agents - Navigation & Instructions + 'Esc to cancel': '按 Esc 取消', + 'Press Enter to save, e to save and edit, Esc to go back': + '按 Enter 保存,e 保存并编辑,Esc 返回', + 'Press Enter to continue, {{navigation}}Esc to {{action}}': + '按 Enter 继续,{{navigation}}Esc {{action}}', + cancel: '取消', + 'go back': '返回', + '↑↓ to navigate, ': '↑↓ 导航,', + 'Enter a clear, unique name for this subagent.': + '为此子代理输入一个清晰、唯一的名称。', + 'e.g., Code Reviewer': '例如:代码审查员', + 'Name cannot be empty.': '名称不能为空。', + "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.": + '编写定义此子代理行为的系统提示。为了获得最佳效果,请全面描述。', + 'e.g., You are an expert code reviewer...': + '例如:您是一位专业的代码审查员...', + 'System prompt cannot be empty.': '系统提示不能为空。', + 'Describe when and how this subagent should be used.': + '描述何时以及如何使用此子代理。', + 'e.g., Reviews code for best practices and potential bugs.': + '例如:审查代码以查找最佳实践和潜在错误。', + 'Description cannot be empty.': '描述不能为空。', + 'Failed to launch editor: {{error}}': '启动编辑器失败: {{error}}', + 'Failed to save and edit subagent: {{error}}': + '保存并编辑子代理失败: {{error}}', + + // ============================================================================ + // Commands - General (continued) + // ============================================================================ + 'View and edit Qwen Code settings': '查看和编辑 Qwen Code 设置', + Settings: '设置', + '(Use Enter to select{{tabText}})': '(使用 Enter 选择{{tabText}})', + ', Tab to change focus': ',Tab 切换焦点', + 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': + '要查看更改,必须重启 Qwen Code。按 r 退出并立即应用更改。', + // ============================================================================ + // Settings Labels + // ============================================================================ + 'Vim Mode': 'Vim 模式', + 'Disable Auto Update': '禁用自动更新', + 'Enable Prompt Completion': '启用提示补全', + 'Debug Keystroke Logging': '调试按键记录', + Language: '语言', + 'Output Format': '输出格式', + 'Hide Window Title': '隐藏窗口标题', + 'Show Status in Title': '在标题中显示状态', + 'Hide Tips': '隐藏提示', + 'Hide Banner': '隐藏横幅', + 'Hide Context Summary': '隐藏上下文摘要', + 'Hide CWD': '隐藏当前工作目录', + 'Hide Sandbox Status': '隐藏沙箱状态', + 'Hide Model Info': '隐藏模型信息', + 'Hide Footer': '隐藏页脚', + 'Show Memory Usage': '显示内存使用', + 'Show Line Numbers': '显示行号', + 'Show Citations': '显示引用', + 'Custom Witty Phrases': '自定义诙谐短语', + 'Enable Welcome Back': '启用欢迎回来', + 'Disable Loading Phrases': '禁用加载短语', + 'Screen Reader Mode': '屏幕阅读器模式', + 'IDE Mode': 'IDE 模式', + 'Max Session Turns': '最大会话轮次', + 'Skip Next Speaker Check': '跳过下一个说话者检查', + 'Skip Loop Detection': '跳过循环检测', + 'Skip Startup Context': '跳过启动上下文', + 'Enable OpenAI Logging': '启用 OpenAI 日志', + 'OpenAI Logging Directory': 'OpenAI 日志目录', + Timeout: '超时', + 'Max Retries': '最大重试次数', + 'Disable Cache Control': '禁用缓存控制', + 'Memory Discovery Max Dirs': '内存发现最大目录数', + 'Load Memory From Include Directories': '从包含目录加载内存', + 'Respect .gitignore': '遵守 .gitignore', + 'Respect .qwenignore': '遵守 .qwenignore', + 'Enable Recursive File Search': '启用递归文件搜索', + 'Disable Fuzzy Search': '禁用模糊搜索', + 'Enable Interactive Shell': '启用交互式 Shell', + 'Show Color': '显示颜色', + 'Auto Accept': '自动接受', + 'Use Ripgrep': '使用 Ripgrep', + 'Use Builtin Ripgrep': '使用内置 Ripgrep', + 'Enable Tool Output Truncation': '启用工具输出截断', + 'Tool Output Truncation Threshold': '工具输出截断阈值', + 'Tool Output Truncation Lines': '工具输出截断行数', + 'Folder Trust': '文件夹信任', + 'Vision Model Preview': '视觉模型预览', + // Settings enum options + 'Auto (detect from system)': '自动(从系统检测)', + Text: '文本', + JSON: 'JSON', + Plan: '规划', + Default: '默认', + 'Auto Edit': '自动编辑', + YOLO: 'YOLO', + 'toggle vim mode on/off': '切换 vim 模式开关', + 'check session stats. Usage: /stats [model|tools]': + '检查会话统计信息。用法:/stats [model|tools]', + 'Show model-specific usage statistics.': '显示模型相关的使用统计信息', + 'Show tool-specific usage statistics.': '显示工具相关的使用统计信息', + 'exit the cli': '退出命令行界面', + 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers': + '列出已配置的 MCP 服务器和工具,或使用支持 OAuth 的服务器进行身份验证', + 'Manage workspace directories': '管理工作区目录', + 'Add directories to the workspace. Use comma to separate multiple paths': + '将目录添加到工作区。使用逗号分隔多个路径', + 'Show all directories in the workspace': '显示工作区中的所有目录', + 'set external editor preference': '设置外部编辑器首选项', + 'Manage extensions': '管理扩展', + 'List active extensions': '列出活动扩展', + 'Update extensions. Usage: update |--all': + '更新扩展。用法:update |--all', + 'manage IDE integration': '管理 IDE 集成', + 'check status of IDE integration': '检查 IDE 集成状态', + 'install required IDE companion for {{ideName}}': + '安装 {{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, Trae)': + '配置终端按键绑定以支持多行输入(VS Code、Cursor、Windsurf、Trae)', + 'Please restart your terminal for the changes to take effect.': + '请重启终端以使更改生效。', + 'Failed to configure terminal: {{error}}': '配置终端失败:{{error}}', + 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.': + '无法确定 {{terminalName}} 在 Windows 上的配置路径:未设置 APPDATA 环境变量。', + '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.': + '{{terminalName}} keybindings.json 存在但不是有效的 JSON 数组。请手动修复文件或删除它以允许自动配置。', + 'File: {{file}}': '文件:{{file}}', + 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.': + '解析 {{terminalName}} keybindings.json 失败。文件包含无效的 JSON。请手动修复文件或删除它以允许自动配置。', + 'Error: {{error}}': '错误:{{error}}', + 'Shift+Enter binding already exists': 'Shift+Enter 绑定已存在', + 'Ctrl+Enter binding already exists': 'Ctrl+Enter 绑定已存在', + 'Existing keybindings detected. Will not modify to avoid conflicts.': + '检测到现有按键绑定。为避免冲突,不会修改。', + 'Please check and modify manually if needed: {{file}}': + '如有需要,请手动检查并修改:{{file}}', + 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.': + '已为 {{terminalName}} 添加 Shift+Enter 和 Ctrl+Enter 按键绑定。', + 'Modified: {{file}}': '已修改:{{file}}', + '{{terminalName}} keybindings already configured.': + '{{terminalName}} 按键绑定已配置。', + 'Failed to configure {{terminalName}}.': '配置 {{terminalName}} 失败。', + 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).': + '您的终端已配置为支持多行输入(Shift+Enter 和 Ctrl+Enter)的最佳体验。', + 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.': + '无法检测终端类型。支持的终端:VS Code、Cursor、Windsurf 和 Trae。', + 'Terminal "{{terminal}}" is not supported yet.': + '终端 "{{terminal}}" 尚未支持。', + + // ============================================================================ + // Commands - Language + // ============================================================================ + 'Invalid language. Available: en-US, zh-CN': + '无效的语言。可用选项:en-US, zh-CN', + 'Language subcommands do not accept additional arguments.': + '语言子命令不接受额外参数', + 'Current UI language: {{lang}}': '当前 UI 语言:{{lang}}', + 'Current LLM output language: {{lang}}': '当前 LLM 输出语言:{{lang}}', + 'LLM output language not set': '未设置 LLM 输出语言', + 'Set UI language': '设置 UI 语言', + 'Set LLM output language': '设置 LLM 输出语言', + 'Usage: /language ui [zh-CN|en-US]': '用法:/language ui [zh-CN|en-US]', + 'Usage: /language output ': '用法:/language output <语言>', + 'Example: /language output 中文': '示例:/language output 中文', + 'Example: /language output English': '示例:/language output English', + 'Example: /language output 日本語': '示例:/language output 日本語', + 'UI language changed to {{lang}}': 'UI 语言已更改为 {{lang}}', + 'LLM output language rule file generated at {{path}}': + 'LLM 输出语言规则文件已生成于 {{path}}', + 'Please restart the application for the changes to take effect.': + '请重启应用程序以使更改生效。', + 'Failed to generate LLM output language rule file: {{error}}': + '生成 LLM 输出语言规则文件失败:{{error}}', + 'Invalid command. Available subcommands:': '无效的命令。可用的子命令:', + 'Available subcommands:': '可用的子命令:', + 'To request additional UI language packs, please open an issue on GitHub.': + '如需请求其他 UI 语言包,请在 GitHub 上提交 issue', + 'Available options:': '可用选项:', + ' - zh-CN: Simplified Chinese': ' - zh-CN: 简体中文', + ' - en-US: English': ' - en-US: English', + 'Set UI language to Simplified Chinese (zh-CN)': + '将 UI 语言设置为简体中文 (zh-CN)', + 'Set UI language to English (en-US)': '将 UI 语言设置为英语 (en-US)', + + // ============================================================================ + // Commands - Approval Mode + // ============================================================================ + 'Approval Mode': '审批模式', + 'Current approval mode: {{mode}}': '当前审批模式:{{mode}}', + 'Available approval modes:': '可用的审批模式:', + 'Approval mode changed to: {{mode}}': '审批模式已更改为:{{mode}}', + 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})': + '审批模式已更改为:{{mode}}(已保存到{{scope}}设置{{location}})', + 'Usage: /approval-mode [--session|--user|--project]': + '用法:/approval-mode [--session|--user|--project]', + + 'Scope subcommands do not accept additional arguments.': + '作用域子命令不接受额外参数', + 'Plan mode - Analyze only, do not modify files or execute commands': + '规划模式 - 仅分析,不修改文件或执行命令', + 'Default mode - Require approval for file edits or shell commands': + '默认模式 - 需要批准文件编辑或 shell 命令', + 'Auto-edit mode - Automatically approve file edits': + '自动编辑模式 - 自动批准文件编辑', + 'YOLO mode - Automatically approve all tools': 'YOLO 模式 - 自动批准所有工具', + '{{mode}} mode': '{{mode}} 模式', + 'Settings service is not available; unable to persist the approval mode.': + '设置服务不可用;无法持久化审批模式。', + 'Failed to save approval mode: {{error}}': '保存审批模式失败:{{error}}', + 'Failed to change approval mode: {{error}}': '更改审批模式失败:{{error}}', + 'Apply to current session only (temporary)': '仅应用于当前会话(临时)', + 'Persist for this project/workspace': '持久化到此项目/工作区', + 'Persist for this user on this machine': '持久化到此机器上的此用户', + 'Analyze only, do not modify files or execute commands': + '仅分析,不修改文件或执行命令', + 'Require approval for file edits or shell commands': + '需要批准文件编辑或 shell 命令', + 'Automatically approve file edits': '自动批准文件编辑', + 'Automatically approve all tools': '自动批准所有工具', + 'Workspace approval mode exists and takes priority. User-level change will have no effect.': + '工作区审批模式已存在并具有优先级。用户级别的更改将无效。', + '(Use Enter to select, Tab to change focus)': + '(使用 Enter 选择,Tab 切换焦点)', + 'Apply To': '应用于', + 'User Settings': '用户设置', + 'Workspace Settings': '工作区设置', + + // ============================================================================ + // Commands - Memory + // ============================================================================ + 'Commands for interacting with memory.': '用于与记忆交互的命令', + 'Show the current memory contents.': '显示当前记忆内容', + 'Show project-level memory contents.': '显示项目级记忆内容', + 'Show global memory contents.': '显示全局记忆内容', + 'Add content to project-level memory.': '添加内容到项目级记忆', + 'Add content to global memory.': '添加内容到全局记忆', + 'Refresh the memory from the source.': '从源刷新记忆', + 'Usage: /memory add --project ': + '用法:/memory add --project <要记住的文本>', + 'Usage: /memory add --global ': + '用法:/memory add --global <要记住的文本>', + 'Attempting to save to project memory: "{{text}}"': + '正在尝试保存到项目记忆:"{{text}}"', + 'Attempting to save to global memory: "{{text}}"': + '正在尝试保存到全局记忆:"{{text}}"', + 'Current memory content from {{count}} file(s):': + '来自 {{count}} 个文件的当前记忆内容:', + 'Memory is currently empty.': '记忆当前为空', + 'Project memory file not found or is currently empty.': + '项目记忆文件未找到或当前为空', + 'Global memory file not found or is currently empty.': + '全局记忆文件未找到或当前为空', + 'Global memory is currently empty.': '全局记忆当前为空', + 'Global memory content:\n\n---\n{{content}}\n---': + '全局记忆内容:\n\n---\n{{content}}\n---', + 'Project memory content from {{path}}:\n\n---\n{{content}}\n---': + '项目记忆内容来自 {{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 + // ============================================================================ + 'Authenticate with an OAuth-enabled MCP server': + '使用支持 OAuth 的 MCP 服务器进行认证', + 'List configured MCP servers and tools': '列出已配置的 MCP 服务器和工具', + 'Restarts MCP servers.': '重启 MCP 服务器', + 'Config not loaded.': '配置未加载', + 'Could not retrieve tool registry.': '无法检索工具注册表', + 'No MCP servers configured with OAuth authentication.': + '未配置支持 OAuth 认证的 MCP 服务器', + 'MCP servers with OAuth authentication:': '支持 OAuth 认证的 MCP 服务器:', + 'Use /mcp auth to authenticate.': + '使用 /mcp auth 进行认证', + "MCP server '{{name}}' not found.": "未找到 MCP 服务器 '{{name}}'", + "Successfully authenticated and refreshed tools for '{{name}}'.": + "成功认证并刷新了 '{{name}}' 的工具", + "Failed to authenticate with MCP server '{{name}}': {{error}}": + "认证 MCP 服务器 '{{name}}' 失败:{{error}}", + "Re-discovering tools from '{{name}}'...": + "正在重新发现 '{{name}}' 的工具...", + + // ============================================================================ + // Commands - Chat + // ============================================================================ + 'Manage conversation history.': '管理对话历史', + 'List saved conversation checkpoints': '列出已保存的对话检查点', + 'No saved conversation checkpoints found.': '未找到已保存的对话检查点', + 'List of saved conversations:': '已保存的对话列表:', + 'Note: Newest last, oldest first': '注意:最新的在最后,最旧的在最前', + 'Save the current conversation as a checkpoint. Usage: /chat save ': + '将当前对话保存为检查点。用法:/chat save ', + 'Missing tag. Usage: /chat save ': '缺少标签。用法:/chat save ', + 'Delete a conversation checkpoint. Usage: /chat delete ': + '删除对话检查点。用法:/chat delete ', + 'Missing tag. Usage: /chat delete ': + '缺少标签。用法:/chat delete ', + "Conversation checkpoint '{{tag}}' has been deleted.": + "对话检查点 '{{tag}}' 已删除", + "Error: No checkpoint found with tag '{{tag}}'.": + "错误:未找到标签为 '{{tag}}' 的检查点", + 'Resume a conversation from a checkpoint. Usage: /chat resume ': + '从检查点恢复对话。用法:/chat resume ', + 'Missing tag. Usage: /chat resume ': + '缺少标签。用法:/chat resume ', + 'No saved checkpoint found with tag: {{tag}}.': + '未找到标签为 {{tag}} 的已保存检查点', + 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?': + '标签为 {{tag}} 的检查点已存在。您要覆盖它吗?', + 'No chat client available to save conversation.': + '没有可用的聊天客户端来保存对话', + 'Conversation checkpoint saved with tag: {{tag}}.': + '对话检查点已保存,标签:{{tag}}', + 'No conversation found to save.': '未找到要保存的对话', + 'No chat client available to share conversation.': + '没有可用的聊天客户端来分享对话', + 'Invalid file format. Only .md and .json are supported.': + '无效的文件格式。仅支持 .md 和 .json 文件', + '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 + // ============================================================================ + 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md': + '生成项目摘要并保存到 .qwen/PROJECT_SUMMARY.md', + 'No chat client available to generate summary.': + '没有可用的聊天客户端来生成摘要', + 'Already generating summary, wait for previous request to complete': + '正在生成摘要,请等待上一个请求完成', + 'No conversation found to summarize.': '未找到要总结的对话', + 'Failed to generate project context summary: {{error}}': + '生成项目上下文摘要失败:{{error}}', + + // ============================================================================ + // Commands - Model + // ============================================================================ + 'Switch the model for this session': '切换此会话的模型', + 'Content generator configuration not available.': '内容生成器配置不可用', + 'Authentication type not available.': '认证类型不可用', + 'No models available for the current authentication type ({{authType}}).': + '当前认证类型 ({{authType}}) 没有可用的模型', + + // ============================================================================ + // Commands - Clear + // ============================================================================ + 'Clearing terminal and resetting chat.': '正在清屏并重置聊天', + 'Clearing terminal.': '正在清屏', + + // ============================================================================ + // Commands - Compress + // ============================================================================ + 'Already compressing, wait for previous request to complete': + '正在压缩中,请等待上一个请求完成', + 'Failed to compress chat history.': '压缩聊天历史失败', + 'Failed to compress chat history: {{error}}': '压缩聊天历史失败:{{error}}', + 'Compressing chat history': '正在压缩聊天历史', + 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.': + '聊天历史已从 {{originalTokens}} 个 token 压缩到 {{newTokens}} 个 token。', + 'Compression was not beneficial for this history size.': + '对于此历史记录大小,压缩没有益处。', + 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.': + '聊天历史压缩未能减小大小。这可能表明压缩提示存在问题。', + 'Could not compress chat history due to a token counting error.': + '由于 token 计数错误,无法压缩聊天历史。', + 'Chat history is already compressed.': '聊天历史已经压缩。', + + // ============================================================================ + // Commands - Directory + // ============================================================================ + 'Configuration is not available.': '配置不可用。', + 'Please provide at least one path to add.': '请提供至少一个要添加的路径。', + 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.': + '/directory add 命令在限制性沙箱配置文件中不受支持。请改为在启动会话时使用 --include-directories。', + "Error adding '{{path}}': {{error}}": "添加 '{{path}}' 时出错:{{error}}", + 'Successfully added GEMINI.md files from the following directories if there are:\n- {{directories}}': + '如果存在,已成功从以下目录添加 GEMINI.md 文件:\n- {{directories}}', + 'Error refreshing memory: {{error}}': '刷新内存时出错:{{error}}', + 'Successfully added directories:\n- {{directories}}': + '成功添加目录:\n- {{directories}}', + 'Current workspace directories:\n{{directories}}': + '当前工作区目录:\n{{directories}}', + + // ============================================================================ + // Commands - Docs + // ============================================================================ + 'Please open the following URL in your browser to view the documentation:\n{{url}}': + '请在浏览器中打开以下 URL 以查看文档:\n{{url}}', + 'Opening documentation in your browser: {{url}}': + '正在浏览器中打开文档:{{url}}', + + // ============================================================================ + // Dialogs - Tool Confirmation + // ============================================================================ + 'Do you want to proceed?': '是否继续?', + 'Yes, allow once': '是,允许一次', + 'Allow always': '总是允许', + 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 + // ============================================================================ + 'Shell Command Execution': 'Shell 命令执行', + 'A custom command wants to run the following shell commands:': + '自定义命令想要运行以下 shell 命令:', + + // ============================================================================ + // Dialogs - Quit Confirmation + // ============================================================================ + 'What would you like to do before exiting?': '退出前您想要做什么?', + 'Quit immediately (/quit)': '立即退出 (/quit)', + 'Generate summary and quit (/summary)': '生成摘要并退出 (/summary)', + 'Save conversation and quit (/chat save)': '保存对话并退出 (/chat save)', + 'Cancel (stay in application)': '取消(留在应用程序中)', + + // ============================================================================ + // Dialogs - Pro Quota + // ============================================================================ + 'Pro quota limit reached for {{model}}.': '{{model}} 的 Pro 配额已达到上限', + 'Change auth (executes the /auth command)': '更改认证(执行 /auth 命令)', + 'Continue with {{model}}': '使用 {{model}} 继续', + + // ============================================================================ + // Dialogs - Welcome Back + // ============================================================================ + 'Current Plan:': '当前计划:', + 'Progress: {{done}}/{{total}} tasks completed': + '进度:已完成 {{done}}/{{total}} 个任务', + ', {{inProgress}} in progress': ',{{inProgress}} 个进行中', + 'Pending Tasks:': '待处理任务:', + 'What would you like to do?': '您想要做什么?', + 'Choose how to proceed with your session:': '选择如何继续您的会话:', + 'Start new chat session': '开始新的聊天会话', + 'Continue previous conversation': '继续之前的对话', + '👋 Welcome back! (Last updated: {{timeAgo}})': + '👋 欢迎回来!(最后更新:{{timeAgo}})', + '🎯 Overall Goal:': '🎯 总体目标:', + + // ============================================================================ + // Dialogs - Auth + // ============================================================================ + 'Get started': '开始使用', + 'How would you like to authenticate for this project?': + '您希望如何为此项目进行身份验证?', + 'OpenAI API key is required to use OpenAI authentication.': + '使用 OpenAI 认证需要 OpenAI API 密钥', + 'You must select an auth method to proceed. Press Ctrl+C again to exit.': + '您必须选择认证方法才能继续。再次按 Ctrl+C 退出', + '(Use Enter to Set Auth)': '(使用 Enter 设置认证)', + 'Terms of Services and Privacy Notice for Qwen Code': + 'Qwen Code 的服务条款和隐私声明', + 'Qwen OAuth': 'Qwen OAuth (免费)', + OpenAI: 'OpenAI', + 'Failed to login. Message: {{message}}': '登录失败。消息:{{message}}', + 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.': + '认证方式被强制设置为 {{enforcedType}},但您当前使用的是 {{currentType}}', + 'Qwen OAuth authentication timed out. Please try again.': + 'Qwen OAuth 认证超时。请重试', + 'Qwen OAuth authentication cancelled.': 'Qwen OAuth 认证已取消', + 'Qwen OAuth Authentication': 'Qwen OAuth 认证', + 'Please visit this URL to authorize:': '请访问此 URL 进行授权:', + 'Or scan the QR code below:': '或扫描下方的二维码:', + 'Waiting for authorization': '等待授权中', + 'Time remaining:': '剩余时间:', + '(Press ESC or CTRL+C to cancel)': '(按 ESC 或 CTRL+C 取消)', + 'Qwen OAuth Authentication Timeout': 'Qwen OAuth 认证超时', + 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.': + 'OAuth 令牌已过期(超过 {{seconds}} 秒)。请重新选择认证方法', + 'Press any key to return to authentication type selection.': + '按任意键返回认证类型选择', + 'Waiting for Qwen OAuth authentication...': '正在等待 Qwen OAuth 认证...', + 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.': + '注意:使用 Qwen OAuth 时,settings.json 中现有的 API 密钥不会被清除。如果需要,您可以稍后切换回 OpenAI 认证。', + 'Authentication timed out. Please try again.': '认证超时。请重试。', + 'Waiting for auth... (Press ESC or CTRL+C to cancel)': + '正在等待认证...(按 ESC 或 CTRL+C 取消)', + 'Failed to authenticate. Message: {{message}}': '认证失败。消息:{{message}}', + 'Authenticated successfully with {{authType}} credentials.': + '使用 {{authType}} 凭据成功认证。', + 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}': + '无效的 QWEN_DEFAULT_AUTH_TYPE 值:"{{value}}"。有效值为:{{validValues}}', + 'OpenAI Configuration Required': '需要配置 OpenAI', + 'Please enter your OpenAI configuration. You can get an API key from': + '请输入您的 OpenAI 配置。您可以从以下地址获取 API 密钥:', + 'API Key:': 'API 密钥:', + 'Invalid credentials: {{errorMessage}}': '凭据无效:{{errorMessage}}', + 'Failed to validate credentials': '验证凭据失败', + 'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel': + '按 Enter 继续,Tab/↑↓ 导航,Esc 取消', + + // ============================================================================ + // 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 + // ============================================================================ + 'Manage folder trust settings': '管理文件夹信任设置', + + // ============================================================================ + // Status Bar + // ============================================================================ + 'Using:': '已加载: ', + '{{count}} open file': '{{count}} 个打开的文件', + '{{count}} open files': '{{count}} 个打开的文件', + '(ctrl+g to view)': '(按 ctrl+g 查看)', + '{{count}} {{name}} file': '{{count}} 个 {{name}} 文件', + '{{count}} {{name}} files': '{{count}} 个 {{name}} 文件', + '{{count}} MCP server': '{{count}} 个 MCP 服务器', + '{{count}} MCP servers': '{{count}} 个 MCP 服务器', + '{{count}} Blocked': '{{count}} 个已阻止', + '(ctrl+t to view)': '(按 ctrl+t 查看)', + '(ctrl+t to toggle)': '(按 ctrl+t 切换)', + 'Press Ctrl+C again to exit.': '再次按 Ctrl+C 退出', + 'Press Ctrl+D again to exit.': '再次按 Ctrl+D 退出', + 'Press Esc again to clear.': '再次按 Esc 清除', + + // ============================================================================ + // MCP Status + // ============================================================================ + 'No MCP servers configured.': '未配置 MCP 服务器', + 'Please view MCP documentation in your browser:': + '请在浏览器中查看 MCP 文档:', + 'or use the cli /docs command': '或使用 cli /docs 命令', + '⏳ MCP servers are starting up ({{count}} initializing)...': + '⏳ MCP 服务器正在启动({{count}} 个正在初始化)...', + 'Note: First startup may take longer. Tool availability will update automatically.': + '注意:首次启动可能需要更长时间。工具可用性将自动更新', + 'Configured MCP servers:': '已配置的 MCP 服务器:', + Ready: '就绪', + 'Starting... (first startup may take longer)': + '正在启动...(首次启动可能需要更长时间)', + Disconnected: '已断开连接', + '{{count}} tool': '{{count}} 个工具', + '{{count}} tools': '{{count}} 个工具', + '{{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 + // ============================================================================ + 'Tips for getting started:': '入门提示:', + '1. Ask questions, edit files, or run commands.': + '1. 提问、编辑文件或运行命令', + '2. Be specific for the best results.': '2. 具体描述以获得最佳结果', + 'files to customize your interactions with Qwen Code.': + '文件以自定义您与 Qwen Code 的交互', + 'for more information.': '获取更多信息', + + // ============================================================================ + // Exit Screen / Stats + // ============================================================================ + 'Agent powering down. Goodbye!': 'Qwen Code 正在关闭,再见!', + 'Interaction Summary': '交互摘要', + 'Session ID:': '会话 ID:', + 'Tool Calls:': '工具调用:', + 'Success Rate:': '成功率:', + 'User Agreement:': '用户同意率:', + reviewed: '已审核', + 'Code Changes:': '代码变更:', + Performance: '性能', + 'Wall Time:': '总耗时:', + 'Agent Active:': '代理活跃时间:', + 'API Time:': 'API 时间:', + 'Tool Time:': '工具时间:', + 'Session Stats': '会话统计', + 'Model Usage': '模型使用情况', + Reqs: '请求数', + 'Input Tokens': '输入令牌', + 'Output Tokens': '输出令牌', + 'Savings Highlight:': '节省亮点:', + 'of input tokens were served from the cache, reducing costs.': + '的输入令牌来自缓存,降低了成本', + 'Tip: For a full token breakdown, run `/stats model`.': + '提示:要查看完整的令牌明细,请运行 `/stats model`', + 'Model Stats For Nerds': '模型统计(技术细节)', + 'Tool Stats For Nerds': '工具统计(技术细节)', + Metric: '指标', + API: 'API', + Requests: '请求数', + Errors: '错误数', + 'Avg Latency': '平均延迟', + Tokens: '令牌', + Total: '总计', + Prompt: '提示', + Cached: '缓存', + Thoughts: '思考', + Tool: '工具', + Output: '输出', + 'No API calls have been made in this session.': + '本次会话中未进行任何 API 调用', + 'Tool Name': '工具名称', + Calls: '调用次数', + 'Success Rate': '成功率', + 'Avg Duration': '平均耗时', + 'User Decision Summary': '用户决策摘要', + 'Total Reviewed Suggestions:': '已审核建议总数:', + ' » Accepted:': ' » 已接受:', + ' » Rejected:': ' » 已拒绝:', + ' » Modified:': ' » 已修改:', + ' Overall Agreement Rate:': ' 总体同意率:', + 'No tool calls have been made in this session.': + '本次会话中未进行任何工具调用', + 'Session start time is unavailable, cannot calculate stats.': + '会话开始时间不可用,无法计算统计信息', + + // ============================================================================ + // Loading Phrases + // ============================================================================ + 'Waiting for user confirmation...': '等待用户确认...', + '(esc to cancel, {{time}})': '(按 esc 取消,{{time}})', + "I'm Feeling Lucky": '我感觉很幸运', + 'Shipping awesomeness... ': '正在运送精彩内容... ', + 'Painting the serifs back on...': '正在重新绘制衬线...', + 'Navigating the slime mold...': '正在导航粘液霉菌...', + 'Consulting the digital spirits...': '正在咨询数字精灵...', + 'Reticulating splines...': '正在网格化样条曲线...', + 'Warming up the AI hamsters...': '正在预热 AI 仓鼠...', + 'Asking the magic conch shell...': '正在询问魔法海螺壳...', + 'Generating witty retort...': '正在生成机智的反驳...', + 'Polishing the algorithms...': '正在打磨算法...', + "Don't rush perfection (or my code)...": '不要急于追求完美(或我的代码)...', + 'Brewing fresh bytes...': '正在酿造新鲜字节...', + 'Counting electrons...': '正在计算电子...', + 'Engaging cognitive processors...': '正在启动认知处理器...', + 'Checking for syntax errors in the universe...': + '正在检查宇宙中的语法错误...', + 'One moment, optimizing humor...': '稍等片刻,正在优化幽默感...', + 'Shuffling punchlines...': '正在洗牌笑点...', + 'Untangling neural nets...': '正在解开神经网络...', + 'Compiling brilliance...': '正在编译智慧...', + 'Loading wit.exe...': '正在加载 wit.exe...', + 'Summoning the cloud of wisdom...': '正在召唤智慧云...', + 'Preparing a witty response...': '正在准备机智的回复...', + "Just a sec, I'm debugging reality...": '稍等片刻,我正在调试现实...', + 'Confuzzling the options...': '正在混淆选项...', + 'Tuning the cosmic frequencies...': '正在调谐宇宙频率...', + 'Crafting a response worthy of your patience...': + '正在制作值得您耐心等待的回复...', + 'Compiling the 1s and 0s...': '正在编译 1 和 0...', + 'Resolving dependencies... and existential crises...': + '正在解决依赖关系...和存在主义危机...', + 'Defragmenting memories... both RAM and personal...': + '正在整理记忆碎片...包括 RAM 和个人记忆...', + 'Rebooting the humor module...': '正在重启幽默模块...', + 'Caching the essentials (mostly cat memes)...': + '正在缓存必需品(主要是猫咪表情包)...', + 'Optimizing for ludicrous speed': '正在优化到荒谬的速度', + "Swapping bits... don't tell the bytes...": '正在交换位...不要告诉字节...', + 'Garbage collecting... be right back...': '正在垃圾回收...马上回来...', + 'Assembling the interwebs...': '正在组装互联网...', + 'Converting coffee into code...': '正在将咖啡转换为代码...', + 'Updating the syntax for reality...': '正在更新现实的语法...', + 'Rewiring the synapses...': '正在重新连接突触...', + 'Looking for a misplaced semicolon...': '正在寻找放错位置的分号...', + "Greasin' the cogs of the machine...": '正在给机器的齿轮上油...', + 'Pre-heating the servers...': '正在预热服务器...', + 'Calibrating the flux capacitor...': '正在校准通量电容器...', + 'Engaging the improbability drive...': '正在启动不可能性驱动器...', + 'Channeling the Force...': '正在引导原力...', + 'Aligning the stars for optimal response...': '正在对齐星星以获得最佳回复...', + 'So say we all...': '我们都说...', + 'Loading the next great idea...': '正在加载下一个伟大的想法...', + "Just a moment, I'm in the zone...": '稍等片刻,我正进入状态...', + 'Preparing to dazzle you with brilliance...': '正在准备用智慧让您眼花缭乱...', + "Just a tick, I'm polishing my wit...": '稍等片刻,我正在打磨我的智慧...', + "Hold tight, I'm crafting a masterpiece...": '请稍等,我正在制作杰作...', + "Just a jiffy, I'm debugging the universe...": '稍等片刻,我正在调试宇宙...', + "Just a moment, I'm aligning the pixels...": '稍等片刻,我正在对齐像素...', + "Just a sec, I'm optimizing the humor...": '稍等片刻,我正在优化幽默感...', + "Just a moment, I'm tuning the algorithms...": '稍等片刻,我正在调整算法...', + 'Warp speed engaged...': '曲速已启动...', + 'Mining for more Dilithium crystals...': '正在挖掘更多二锂晶体...', + "Don't panic...": '不要惊慌...', + 'Following the white rabbit...': '正在跟随白兔...', + 'The truth is in here... somewhere...': '真相在这里...某个地方...', + 'Blowing on the cartridge...': '正在吹卡带...', + 'Loading... Do a barrel roll!': '正在加载...做个桶滚!', + 'Waiting for the respawn...': '等待重生...', + 'Finishing the Kessel Run in less than 12 parsecs...': + '正在以不到 12 秒差距完成凯塞尔航线...', + "The cake is not a lie, it's just still loading...": + '蛋糕不是谎言,只是还在加载...', + 'Fiddling with the character creation screen...': '正在摆弄角色创建界面...', + "Just a moment, I'm finding the right meme...": + '稍等片刻,我正在寻找合适的表情包...', + "Pressing 'A' to continue...": "按 'A' 继续...", + 'Herding digital cats...': '正在放牧数字猫...', + 'Polishing the pixels...': '正在打磨像素...', + 'Finding a suitable loading screen pun...': '正在寻找合适的加载屏幕双关语...', + 'Distracting you with this witty phrase...': + '正在用这个机智的短语分散您的注意力...', + 'Almost there... probably...': '快到了...可能...', + 'Our hamsters are working as fast as they can...': + '我们的仓鼠正在尽可能快地工作...', + 'Giving Cloudy a pat on the head...': '正在拍拍 Cloudy 的头...', + 'Petting the cat...': '正在抚摸猫咪...', + 'Rickrolling my boss...': '正在 Rickroll 我的老板...', + 'Never gonna give you up, never gonna let you down...': + '永远不会放弃你,永远不会让你失望...', + 'Slapping the bass...': '正在拍打低音...', + 'Tasting the snozberries...': '正在品尝 snozberries...', + "I'm going the distance, I'm going for speed...": + '我要走得更远,我要追求速度...', + 'Is this the real life? Is this just fantasy?...': + '这是真实的生活吗?还是只是幻想?...', + "I've got a good feeling about this...": '我对这个感觉很好...', + 'Poking the bear...': '正在戳熊...', + 'Doing research on the latest memes...': '正在研究最新的表情包...', + 'Figuring out how to make this more witty...': '正在想办法让这更有趣...', + 'Hmmm... let me think...': '嗯...让我想想...', + 'What do you call a fish with no eyes? A fsh...': + '没有眼睛的鱼叫什么?一条鱼...', + 'Why did the computer go to therapy? It had too many bytes...': + '为什么电脑去看心理医生?因为它有太多字节...', + "Why don't programmers like nature? It has too many bugs...": + '为什么程序员不喜欢大自然?因为虫子太多了...', + 'Why do programmers prefer dark mode? Because light attracts bugs...': + '为什么程序员喜欢暗色模式?因为光会吸引虫子...', + 'Why did the developer go broke? Because they used up all their cache...': + '为什么开发者破产了?因为他们用完了所有缓存...', + "What can you do with a broken pencil? Nothing, it's pointless...": + '你能用断了的铅笔做什么?什么都不能,因为它没有笔尖...', + 'Applying percussive maintenance...': '正在应用敲击维护...', + 'Searching for the correct USB orientation...': '正在寻找正确的 USB 方向...', + 'Ensuring the magic smoke stays inside the wires...': + '确保魔法烟雾留在电线内...', + 'Rewriting in Rust for no particular reason...': + '正在用 Rust 重写,没有特别的原因...', + 'Trying to exit Vim...': '正在尝试退出 Vim...', + 'Spinning up the hamster wheel...': '正在启动仓鼠轮...', + "That's not a bug, it's an undocumented feature...": + '这不是一个错误,这是一个未记录的功能...', + 'Engage.': '启动。', + "I'll be back... with an answer.": '我会回来的...带着答案。', + 'My other process is a TARDIS...': '我的另一个进程是 TARDIS...', + 'Communing with the machine spirit...': '正在与机器精神交流...', + 'Letting the thoughts marinate...': '让想法慢慢酝酿...', + 'Just remembered where I put my keys...': '刚刚想起我把钥匙放在哪里了...', + 'Pondering the orb...': '正在思考球体...', + "I've seen things you people wouldn't believe... like a user who reads loading messages.": + '我见过你们不会相信的事情...比如一个阅读加载消息的用户。', + 'Initiating thoughtful gaze...': '正在启动深思凝视...', + "What's a computer's favorite snack? Microchips.": + '电脑最喜欢的零食是什么?微芯片。', + "Why do Java developers wear glasses? Because they don't C#.": + '为什么 Java 开发者戴眼镜?因为他们不会 C#。', + 'Charging the laser... pew pew!': '正在给激光充电...砰砰!', + 'Dividing by zero... just kidding!': '除以零...只是开玩笑!', + 'Looking for an adult superviso... I mean, processing.': + '正在寻找成人监督...我是说,处理中。', + 'Making it go beep boop.': '让它发出哔哔声。', + 'Buffering... because even AIs need a moment.': + '正在缓冲...因为即使是 AI 也需要片刻。', + 'Entangling quantum particles for a faster response...': + '正在纠缠量子粒子以获得更快的回复...', + 'Polishing the chrome... on the algorithms.': '正在打磨铬...在算法上。', + 'Are you not entertained? (Working on it!)': '你不觉得有趣吗?(正在努力!)', + 'Summoning the code gremlins... to help, of course.': + '正在召唤代码小精灵...当然是来帮忙的。', + 'Just waiting for the dial-up tone to finish...': '只是等待拨号音结束...', + 'Recalibrating the humor-o-meter.': '正在重新校准幽默计。', + 'My other loading screen is even funnier.': '我的另一个加载屏幕更有趣。', + "Pretty sure there's a cat walking on the keyboard somewhere...": + '很确定有只猫在某个地方键盘上走...', + 'Enhancing... Enhancing... Still loading.': + '正在增强...正在增强...仍在加载。', + "It's not a bug, it's a feature... of this loading screen.": + '这不是一个错误,这是一个功能...这个加载屏幕的功能。', + 'Have you tried turning it off and on again? (The loading screen, not me.)': + '你试过把它关掉再打开吗?(加载屏幕,不是我。)', + 'Constructing additional pylons...': '正在建造额外的能量塔...', +}; diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 5a5a8f2d..514fb15b 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -24,6 +24,7 @@ import { extensionsCommand } from '../ui/commands/extensionsCommand.js'; import { helpCommand } from '../ui/commands/helpCommand.js'; import { ideCommand } from '../ui/commands/ideCommand.js'; import { initCommand } from '../ui/commands/initCommand.js'; +import { languageCommand } from '../ui/commands/languageCommand.js'; import { mcpCommand } from '../ui/commands/mcpCommand.js'; import { memoryCommand } from '../ui/commands/memoryCommand.js'; import { modelCommand } from '../ui/commands/modelCommand.js'; @@ -72,6 +73,7 @@ export class BuiltinCommandLoader implements ICommandLoader { helpCommand, await ideCommand(), initCommand, + languageCommand, mcpCommand, memoryCommand, modelCommand, diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 345bebd2..235bb3bc 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -89,6 +89,7 @@ import { useGitBranchName } from './hooks/useGitBranchName.js'; import { useExtensionUpdates } from './hooks/useExtensionUpdates.js'; import { ShellFocusContext } from './contexts/ShellFocusContext.js'; import { useQuitConfirmation } from './hooks/useQuitConfirmation.js'; +import { t } from '../i18n/index.js'; import { useWelcomeBack } from './hooks/useWelcomeBack.js'; import { useDialogClose } from './hooks/useDialogClose.js'; import { useInitializationAuthError } from './hooks/useInitializationAuthError.js'; @@ -384,7 +385,13 @@ export const AppContainer = (props: AppContainerProps) => { settings.merged.security?.auth.selectedType ) { onAuthError( - `Authentication is enforced to be ${settings.merged.security?.auth.enforcedType}, but you are currently using ${settings.merged.security?.auth.selectedType}.`, + t( + 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.', + { + enforcedType: settings.merged.security?.auth.enforcedType, + currentType: settings.merged.security?.auth.selectedType, + }, + ), ); } else if ( settings.merged.security?.auth?.selectedType && diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index ec0b2577..80c13b0b 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -15,6 +15,7 @@ import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; import { useUIState } from '../contexts/UIStateContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js'; import { useSettings } from '../contexts/SettingsContext.js'; +import { t } from '../../i18n/index.js'; function parseDefaultAuthType( defaultAuthType: string | undefined, @@ -39,10 +40,14 @@ export function AuthDialog(): React.JSX.Element { const items = [ { key: AuthType.QWEN_OAUTH, - label: 'Qwen OAuth', + label: t('Qwen OAuth'), value: AuthType.QWEN_OAUTH, }, - { key: AuthType.USE_OPENAI, label: 'OpenAI', value: AuthType.USE_OPENAI }, + { + key: AuthType.USE_OPENAI, + label: t('OpenAI'), + value: AuthType.USE_OPENAI, + }, ]; const initialAuthIndex = Math.max( @@ -98,7 +103,9 @@ export function AuthDialog(): React.JSX.Element { if (settings.merged.security?.auth?.selectedType === undefined) { // Prevent exiting if no auth method is set setErrorMessage( - 'You must select an auth method to proceed. Press Ctrl+C again to exit.', + t( + 'You must select an auth method to proceed. Press Ctrl+C again to exit.', + ), ); return; } @@ -116,9 +123,9 @@ export function AuthDialog(): React.JSX.Element { padding={1} width="100%" > - Get started + {t('Get started')} - How would you like to authenticate for this project? + {t('How would you like to authenticate for this project?')} )} - (Use Enter to Set Auth) + {t('(Use Enter to Set Auth)')} {hasApiKey && currentSelectedAuthType === AuthType.QWEN_OAUTH && ( - Note: Your existing API key in settings.json will not be cleared - when using Qwen OAuth. You can switch back to OpenAI authentication - later if needed. + {t( + 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.', + )} )} - Terms of Services and Privacy Notice for Qwen Code + {t('Terms of Services and Privacy Notice for Qwen Code')} diff --git a/packages/cli/src/ui/auth/AuthInProgress.tsx b/packages/cli/src/ui/auth/AuthInProgress.tsx index 6270ecf1..3269946d 100644 --- a/packages/cli/src/ui/auth/AuthInProgress.tsx +++ b/packages/cli/src/ui/auth/AuthInProgress.tsx @@ -10,6 +10,7 @@ import { Box, Text } from 'ink'; import Spinner from 'ink-spinner'; import { theme } from '../semantic-colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; +import { t } from '../../i18n/index.js'; interface AuthInProgressProps { onTimeout: () => void; @@ -48,13 +49,13 @@ export function AuthInProgress({ > {timedOut ? ( - Authentication timed out. Please try again. + {t('Authentication timed out. Please try again.')} ) : ( - Waiting for auth... (Press ESC or CTRL+C to - cancel) + {' '} + {t('Waiting for auth... (Press ESC or CTRL+C to cancel)')} )} diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts index 04da911c..d2369690 100644 --- a/packages/cli/src/ui/auth/useAuth.ts +++ b/packages/cli/src/ui/auth/useAuth.ts @@ -18,6 +18,7 @@ import type { OpenAICredentials } from '../components/OpenAIKeyPrompt.js'; import { useQwenAuth } from '../hooks/useQwenAuth.js'; import { AuthState, MessageType } from '../types.js'; import type { HistoryItem } from '../types.js'; +import { t } from '../../i18n/index.js'; export type { QwenAuthState } from '../hooks/useQwenAuth.js'; @@ -60,7 +61,9 @@ export const useAuthCommand = ( const handleAuthFailure = useCallback( (error: unknown) => { setIsAuthenticating(false); - const errorMessage = `Failed to authenticate. Message: ${getErrorMessage(error)}`; + const errorMessage = t('Failed to authenticate. Message: {{message}}', { + message: getErrorMessage(error), + }); onAuthError(errorMessage); // Log authentication failure @@ -127,7 +130,9 @@ export const useAuthCommand = ( addItem( { type: MessageType.INFO, - text: `Authenticated successfully with ${authType} credentials.`, + text: t('Authenticated successfully with {{authType}} credentials.', { + authType, + }), }, Date.now(), ); @@ -225,7 +230,13 @@ export const useAuthCommand = ( ) ) { onAuthError( - `Invalid QWEN_DEFAULT_AUTH_TYPE value: "${defaultAuthType}". Valid values are: ${[AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].join(', ')}`, + t( + 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}', + { + value: defaultAuthType, + validValues: [AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].join(', '), + }, + ), ); } }, [onAuthError]); diff --git a/packages/cli/src/ui/commands/aboutCommand.ts b/packages/cli/src/ui/commands/aboutCommand.ts index 0f35db92..800b2b00 100644 --- a/packages/cli/src/ui/commands/aboutCommand.ts +++ b/packages/cli/src/ui/commands/aboutCommand.ts @@ -8,10 +8,13 @@ import type { SlashCommand } from './types.js'; import { CommandKind } from './types.js'; import { MessageType, type HistoryItemAbout } from '../types.js'; import { getExtendedSystemInfo } from '../../utils/systemInfo.js'; +import { t } from '../../i18n/index.js'; export const aboutCommand: SlashCommand = { name: 'about', - description: 'show version info', + get description() { + return t('show version info'); + }, kind: CommandKind.BUILT_IN, action: async (context) => { const systemInfo = await getExtendedSystemInfo(context); diff --git a/packages/cli/src/ui/commands/agentsCommand.ts b/packages/cli/src/ui/commands/agentsCommand.ts index ccb5997a..02fed007 100644 --- a/packages/cli/src/ui/commands/agentsCommand.ts +++ b/packages/cli/src/ui/commands/agentsCommand.ts @@ -9,15 +9,20 @@ import { type SlashCommand, type OpenDialogActionReturn, } from './types.js'; +import { t } from '../../i18n/index.js'; export const agentsCommand: SlashCommand = { name: 'agents', - description: 'Manage subagents for specialized task delegation.', + get description() { + return t('Manage subagents for specialized task delegation.'); + }, kind: CommandKind.BUILT_IN, subCommands: [ { name: 'manage', - description: 'Manage existing subagents (view, edit, delete).', + get description() { + return t('Manage existing subagents (view, edit, delete).'); + }, kind: CommandKind.BUILT_IN, action: (): OpenDialogActionReturn => ({ type: 'dialog', @@ -26,7 +31,9 @@ export const agentsCommand: SlashCommand = { }, { name: 'create', - description: 'Create a new subagent with guided setup.', + get description() { + return t('Create a new subagent with guided setup.'); + }, kind: CommandKind.BUILT_IN, action: (): OpenDialogActionReturn => ({ type: 'dialog', diff --git a/packages/cli/src/ui/commands/approvalModeCommand.ts b/packages/cli/src/ui/commands/approvalModeCommand.ts index 5528d86f..90ae774b 100644 --- a/packages/cli/src/ui/commands/approvalModeCommand.ts +++ b/packages/cli/src/ui/commands/approvalModeCommand.ts @@ -10,10 +10,13 @@ import type { OpenDialogActionReturn, } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const approvalModeCommand: SlashCommand = { name: 'approval-mode', - description: 'View or change the approval mode for tool usage', + get description() { + return t('View or change the approval mode for tool usage'); + }, kind: CommandKind.BUILT_IN, action: async ( _context: CommandContext, diff --git a/packages/cli/src/ui/commands/authCommand.ts b/packages/cli/src/ui/commands/authCommand.ts index 5ba3088c..9caee464 100644 --- a/packages/cli/src/ui/commands/authCommand.ts +++ b/packages/cli/src/ui/commands/authCommand.ts @@ -6,10 +6,13 @@ import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const authCommand: SlashCommand = { name: 'auth', - description: 'change the auth method', + get description() { + return t('change the auth method'); + }, kind: CommandKind.BUILT_IN, action: (_context, _args): OpenDialogActionReturn => ({ type: 'dialog', diff --git a/packages/cli/src/ui/commands/bugCommand.ts b/packages/cli/src/ui/commands/bugCommand.ts index 869024b5..14cf3759 100644 --- a/packages/cli/src/ui/commands/bugCommand.ts +++ b/packages/cli/src/ui/commands/bugCommand.ts @@ -16,10 +16,13 @@ import { getSystemInfoFields, getFieldValue, } from '../../utils/systemInfoFields.js'; +import { t } from '../../i18n/index.js'; export const bugCommand: SlashCommand = { name: 'bug', - description: 'submit a bug report', + get description() { + return t('submit a bug report'); + }, kind: CommandKind.BUILT_IN, action: async (context: CommandContext, args?: string): Promise => { const bugDescription = (args || '').trim(); 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/clearCommand.ts b/packages/cli/src/ui/commands/clearCommand.ts index 4c6405c0..8beed859 100644 --- a/packages/cli/src/ui/commands/clearCommand.ts +++ b/packages/cli/src/ui/commands/clearCommand.ts @@ -7,21 +7,24 @@ import { uiTelemetryService } from '@qwen-code/qwen-code-core'; import type { SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const clearCommand: SlashCommand = { name: 'clear', - description: 'clear the screen and conversation history', + get description() { + return t('clear the screen and conversation history'); + }, kind: CommandKind.BUILT_IN, action: async (context, _args) => { const geminiClient = context.services.config?.getGeminiClient(); if (geminiClient) { - context.ui.setDebugMessage('Clearing terminal and resetting chat.'); + context.ui.setDebugMessage(t('Clearing terminal and resetting chat.')); // If resetChat fails, the exception will propagate and halt the command, // which is the correct behavior to signal a failure to the user. await geminiClient.resetChat(); } else { - context.ui.setDebugMessage('Clearing terminal.'); + context.ui.setDebugMessage(t('Clearing terminal.')); } uiTelemetryService.setLastPromptTokenCount(0); diff --git a/packages/cli/src/ui/commands/compressCommand.ts b/packages/cli/src/ui/commands/compressCommand.ts index 45dc6a46..399bfa61 100644 --- a/packages/cli/src/ui/commands/compressCommand.ts +++ b/packages/cli/src/ui/commands/compressCommand.ts @@ -8,11 +8,14 @@ import type { HistoryItemCompression } from '../types.js'; import { MessageType } from '../types.js'; import type { SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const compressCommand: SlashCommand = { name: 'compress', altNames: ['summarize'], - description: 'Compresses the context by replacing it with a summary.', + get description() { + return t('Compresses the context by replacing it with a summary.'); + }, kind: CommandKind.BUILT_IN, action: async (context) => { const { ui } = context; @@ -20,7 +23,7 @@ export const compressCommand: SlashCommand = { ui.addItem( { type: MessageType.ERROR, - text: 'Already compressing, wait for previous request to complete', + text: t('Already compressing, wait for previous request to complete'), }, Date.now(), ); @@ -60,7 +63,7 @@ export const compressCommand: SlashCommand = { ui.addItem( { type: MessageType.ERROR, - text: 'Failed to compress chat history.', + text: t('Failed to compress chat history.'), }, Date.now(), ); @@ -69,9 +72,9 @@ export const compressCommand: SlashCommand = { ui.addItem( { type: MessageType.ERROR, - text: `Failed to compress chat history: ${ - e instanceof Error ? e.message : String(e) - }`, + text: t('Failed to compress chat history: {{error}}', { + error: e instanceof Error ? e.message : String(e), + }), }, Date.now(), ); diff --git a/packages/cli/src/ui/commands/copyCommand.ts b/packages/cli/src/ui/commands/copyCommand.ts index 99115491..3b79dd48 100644 --- a/packages/cli/src/ui/commands/copyCommand.ts +++ b/packages/cli/src/ui/commands/copyCommand.ts @@ -7,10 +7,13 @@ import { copyToClipboard } from '../utils/commandUtils.js'; import type { SlashCommand, SlashCommandActionReturn } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const copyCommand: SlashCommand = { name: 'copy', - description: 'Copy the last result or code snippet to clipboard', + get description() { + return t('Copy the last result or code snippet to clipboard'); + }, kind: CommandKind.BUILT_IN, action: async (context, _args): Promise => { const chat = await context.services.config?.getGeminiClient()?.getChat(); diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index cc8970d0..e44530b7 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -10,6 +10,7 @@ import { MessageType } from '../types.js'; import * as os from 'node:os'; import * as path from 'node:path'; import { loadServerHierarchicalMemory } from '@qwen-code/qwen-code-core'; +import { t } from '../../i18n/index.js'; export function expandHomeDir(p: string): string { if (!p) { @@ -27,13 +28,18 @@ export function expandHomeDir(p: string): string { export const directoryCommand: SlashCommand = { name: 'directory', altNames: ['dir'], - description: 'Manage workspace directories', + get description() { + return t('Manage workspace directories'); + }, kind: CommandKind.BUILT_IN, subCommands: [ { name: 'add', - description: - 'Add directories to the workspace. Use comma to separate multiple paths', + get description() { + return t( + 'Add directories to the workspace. Use comma to separate multiple paths', + ); + }, kind: CommandKind.BUILT_IN, action: async (context: CommandContext, args: string) => { const { @@ -46,7 +52,7 @@ export const directoryCommand: SlashCommand = { addItem( { type: MessageType.ERROR, - text: 'Configuration is not available.', + text: t('Configuration is not available.'), }, Date.now(), ); @@ -63,7 +69,7 @@ export const directoryCommand: SlashCommand = { addItem( { type: MessageType.ERROR, - text: 'Please provide at least one path to add.', + text: t('Please provide at least one path to add.'), }, Date.now(), ); @@ -74,8 +80,9 @@ export const directoryCommand: SlashCommand = { return { type: 'message' as const, messageType: 'error' as const, - content: + content: t( 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.', + ), }; } @@ -88,7 +95,12 @@ export const directoryCommand: SlashCommand = { added.push(pathToAdd.trim()); } catch (e) { const error = e as Error; - errors.push(`Error adding '${pathToAdd.trim()}': ${error.message}`); + errors.push( + t("Error adding '{{path}}': {{error}}", { + path: pathToAdd.trim(), + error: error.message, + }), + ); } } @@ -117,12 +129,21 @@ export const directoryCommand: SlashCommand = { addItem( { type: MessageType.INFO, - text: `Successfully added GEMINI.md files from the following directories if there are:\n- ${added.join('\n- ')}`, + text: t( + 'Successfully added GEMINI.md files from the following directories if there are:\n- {{directories}}', + { + directories: added.join('\n- '), + }, + ), }, Date.now(), ); } catch (error) { - errors.push(`Error refreshing memory: ${(error as Error).message}`); + errors.push( + t('Error refreshing memory: {{error}}', { + error: (error as Error).message, + }), + ); } if (added.length > 0) { @@ -133,7 +154,9 @@ export const directoryCommand: SlashCommand = { addItem( { type: MessageType.INFO, - text: `Successfully added directories:\n- ${added.join('\n- ')}`, + text: t('Successfully added directories:\n- {{directories}}', { + directories: added.join('\n- '), + }), }, Date.now(), ); @@ -150,7 +173,9 @@ export const directoryCommand: SlashCommand = { }, { name: 'show', - description: 'Show all directories in the workspace', + get description() { + return t('Show all directories in the workspace'); + }, kind: CommandKind.BUILT_IN, action: async (context: CommandContext) => { const { @@ -161,7 +186,7 @@ export const directoryCommand: SlashCommand = { addItem( { type: MessageType.ERROR, - text: 'Configuration is not available.', + text: t('Configuration is not available.'), }, Date.now(), ); @@ -173,7 +198,9 @@ export const directoryCommand: SlashCommand = { addItem( { type: MessageType.INFO, - text: `Current workspace directories:\n${directoryList}`, + text: t('Current workspace directories:\n{{directories}}', { + directories: directoryList, + }), }, Date.now(), ); diff --git a/packages/cli/src/ui/commands/docsCommand.ts b/packages/cli/src/ui/commands/docsCommand.ts index 109aaab7..8fc01836 100644 --- a/packages/cli/src/ui/commands/docsCommand.ts +++ b/packages/cli/src/ui/commands/docsCommand.ts @@ -12,19 +12,28 @@ import { CommandKind, } from './types.js'; import { MessageType } from '../types.js'; +import { t, getCurrentLanguage } from '../../i18n/index.js'; export const docsCommand: SlashCommand = { name: 'docs', - description: 'open full Qwen Code documentation in your browser', + get description() { + return t('open full Qwen Code documentation in your browser'); + }, kind: CommandKind.BUILT_IN, action: async (context: CommandContext): Promise => { - const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en'; + const langPath = getCurrentLanguage()?.startsWith('zh') ? 'zh' : 'en'; + const docsUrl = `https://qwenlm.github.io/qwen-code-docs/${langPath}`; if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') { context.ui.addItem( { type: MessageType.INFO, - text: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`, + text: t( + 'Please open the following URL in your browser to view the documentation:\n{{url}}', + { + url: docsUrl, + }, + ), }, Date.now(), ); @@ -32,7 +41,9 @@ export const docsCommand: SlashCommand = { context.ui.addItem( { type: MessageType.INFO, - text: `Opening documentation in your browser: ${docsUrl}`, + text: t('Opening documentation in your browser: {{url}}', { + url: docsUrl, + }), }, Date.now(), ); diff --git a/packages/cli/src/ui/commands/editorCommand.ts b/packages/cli/src/ui/commands/editorCommand.ts index 5b5c4c5d..f39cbdbc 100644 --- a/packages/cli/src/ui/commands/editorCommand.ts +++ b/packages/cli/src/ui/commands/editorCommand.ts @@ -9,10 +9,13 @@ import { type OpenDialogActionReturn, type SlashCommand, } from './types.js'; +import { t } from '../../i18n/index.js'; export const editorCommand: SlashCommand = { name: 'editor', - description: 'set external editor preference', + get description() { + return t('set external editor preference'); + }, kind: CommandKind.BUILT_IN, action: (): OpenDialogActionReturn => ({ type: 'dialog', 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/helpCommand.ts b/packages/cli/src/ui/commands/helpCommand.ts index 4731efc5..c4772ea0 100644 --- a/packages/cli/src/ui/commands/helpCommand.ts +++ b/packages/cli/src/ui/commands/helpCommand.ts @@ -7,12 +7,15 @@ import type { SlashCommand } from './types.js'; import { CommandKind } from './types.js'; import { MessageType, type HistoryItemHelp } from '../types.js'; +import { t } from '../../i18n/index.js'; export const helpCommand: SlashCommand = { name: 'help', altNames: ['?'], kind: CommandKind.BUILT_IN, - description: 'for help on Qwen Code', + get description() { + return t('for help on Qwen Code'); + }, action: async (context) => { const helpItem: Omit = { type: MessageType.HELP, 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/initCommand.ts b/packages/cli/src/ui/commands/initCommand.ts index 0777be8e..16c98dff 100644 --- a/packages/cli/src/ui/commands/initCommand.ts +++ b/packages/cli/src/ui/commands/initCommand.ts @@ -15,10 +15,13 @@ import { getCurrentGeminiMdFilename } from '@qwen-code/qwen-code-core'; import { CommandKind } from './types.js'; import { Text } from 'ink'; import React from 'react'; +import { t } from '../../i18n/index.js'; export const initCommand: SlashCommand = { name: 'init', - description: 'Analyzes the project and creates a tailored QWEN.md file.', + get description() { + return t('Analyzes the project and creates a tailored QWEN.md file.'); + }, kind: CommandKind.BUILT_IN, action: async ( context: CommandContext, @@ -28,7 +31,7 @@ export const initCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Configuration not available.', + content: t('Configuration not available.'), }; } const targetDir = context.services.config.getTargetDir(); diff --git a/packages/cli/src/ui/commands/languageCommand.ts b/packages/cli/src/ui/commands/languageCommand.ts new file mode 100644 index 00000000..ba04920b --- /dev/null +++ b/packages/cli/src/ui/commands/languageCommand.ts @@ -0,0 +1,458 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + SlashCommand, + CommandContext, + SlashCommandActionReturn, + MessageActionReturn, +} from './types.js'; +import { CommandKind } from './types.js'; +import { SettingScope } from '../../config/settings.js'; +import { + setLanguageAsync, + getCurrentLanguage, + type SupportedLanguage, + t, +} from '../../i18n/index.js'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { Storage } from '@qwen-code/qwen-code-core'; + +const LLM_OUTPUT_LANGUAGE_RULE_FILENAME = 'output-language.md'; + +/** + * Generates the LLM output language rule template based on the language name. + */ +function generateLlmOutputLanguageRule(language: string): string { + return `# ⚠️ CRITICAL: ${language} Output Language Rule - HIGHEST PRIORITY ⚠️ + +## 🚨 MANDATORY RULE - NO EXCEPTIONS 🚨 + +**YOU MUST RESPOND IN ${language.toUpperCase()} FOR EVERY SINGLE OUTPUT, REGARDLESS OF THE USER'S INPUT LANGUAGE.** + +This is a **NON-NEGOTIABLE** requirement. Even if the user writes in English, says "hi", asks a simple question, or explicitly requests another language, **YOU MUST ALWAYS RESPOND IN ${language.toUpperCase()}.** + +## What Must Be in ${language} + +**EVERYTHING** you output: conversation replies, tool call descriptions, success/error messages, generated file content (comments, documentation), and all explanatory text. + +**Tool outputs**: All descriptive text from \`read_file\`, \`write_file\`, \`codebase_search\`, \`run_terminal_cmd\`, \`todo_write\`, \`web_search\`, etc. MUST be in ${language}. + +## Examples + +### ✅ CORRECT: +- User says "hi" → Respond in ${language} (e.g., "Bonjour" if ${language} is French) +- Tool result → "已成功读取文件 config.json" (if ${language} is Chinese) +- Error → "无法找到指定的文件" (if ${language} is Chinese) + +### ❌ WRONG: +- User says "hi" → "Hello" in English +- Tool result → "Successfully read file" in English +- Error → "File not found" in English + +## Notes + +- Code elements (variable/function names, syntax) can remain in English +- Comments, documentation, and all other text MUST be in ${language} + +**THIS RULE IS ACTIVE NOW. ALL OUTPUTS MUST BE IN ${language.toUpperCase()}. NO EXCEPTIONS.** +`; +} + +/** + * Gets the path to the LLM output language rule file. + */ +function getLlmOutputLanguageRulePath(): string { + return path.join( + Storage.getGlobalQwenDir(), + LLM_OUTPUT_LANGUAGE_RULE_FILENAME, + ); +} + +/** + * Gets the current LLM output language from the rule file if it exists. + */ +function getCurrentLlmOutputLanguage(): string | null { + const filePath = getLlmOutputLanguageRulePath(); + if (fs.existsSync(filePath)) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + // Extract language name from the first line (e.g., "# Chinese Response Rules" -> "Chinese") + const match = content.match(/^#\s+(.+?)\s+Response Rules/i); + if (match) { + return match[1]; + } + } catch { + // Ignore errors + } + } + return null; +} + +/** + * Sets the UI language and persists it to settings. + */ +async function setUiLanguage( + context: CommandContext, + lang: SupportedLanguage, +): Promise { + const { services } = context; + const { settings } = services; + + if (!services.config) { + return { + type: 'message', + messageType: 'error', + content: t('Configuration not available.'), + }; + } + + // Set language in i18n system (async to support JS translation files) + await setLanguageAsync(lang); + + // Persist to settings (user scope) + if (settings && typeof settings.setValue === 'function') { + try { + settings.setValue(SettingScope.User, 'general.language', lang); + } catch (error) { + console.warn('Failed to save language setting:', error); + } + } + + // Reload commands to update their descriptions with the new language + context.ui.reloadCommands(); + + // Map language codes to friendly display names + const langDisplayNames: Record = { + zh: '中文(zh-CN)', + en: 'English(en-US)', + }; + + return { + type: 'message', + messageType: 'info', + content: t('UI language changed to {{lang}}', { + lang: langDisplayNames[lang], + }), + }; +} + +/** + * Generates the LLM output language rule file. + */ +function generateLlmOutputLanguageRuleFile( + language: string, +): Promise { + try { + const filePath = getLlmOutputLanguageRulePath(); + const content = generateLlmOutputLanguageRule(language); + + // Ensure directory exists + const dir = path.dirname(filePath); + fs.mkdirSync(dir, { recursive: true }); + + // Write file (overwrite if exists) + fs.writeFileSync(filePath, content, 'utf-8'); + + return Promise.resolve({ + type: 'message', + messageType: 'info', + content: [ + t('LLM output language rule file generated at {{path}}', { + path: filePath, + }), + '', + t('Please restart the application for the changes to take effect.'), + ].join('\n'), + }); + } catch (error) { + return Promise.resolve({ + type: 'message', + messageType: 'error', + content: t( + 'Failed to generate LLM output language rule file: {{error}}', + { + error: error instanceof Error ? error.message : String(error), + }, + ), + }); + } +} + +export const languageCommand: SlashCommand = { + name: 'language', + get description() { + return t('View or change the language setting'); + }, + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + args: string, + ): Promise => { + const { services } = context; + + if (!services.config) { + return { + type: 'message', + messageType: 'error', + content: t('Configuration not available.'), + }; + } + + const trimmedArgs = args.trim(); + + // If no arguments, show current language settings and usage + if (!trimmedArgs) { + const currentUiLang = getCurrentLanguage(); + const currentLlmLang = getCurrentLlmOutputLanguage(); + const message = [ + t('Current UI language: {{lang}}', { lang: currentUiLang }), + currentLlmLang + ? t('Current LLM output language: {{lang}}', { lang: currentLlmLang }) + : t('LLM output language not set'), + '', + t('Available subcommands:'), + ` /language ui [zh-CN|en-US] - ${t('Set UI language')}`, + ` /language output - ${t('Set LLM output language')}`, + ].join('\n'); + + return { + type: 'message', + messageType: 'info', + content: message, + }; + } + + // Parse subcommand + const parts = trimmedArgs.split(/\s+/); + const subcommand = parts[0].toLowerCase(); + + if (subcommand === 'ui') { + // Handle /language ui [zh-CN|en-US] + if (parts.length === 1) { + // Show UI language subcommand help + return { + type: 'message', + messageType: 'info', + content: [ + t('Set UI language'), + '', + t('Usage: /language ui [zh-CN|en-US]'), + '', + t('Available options:'), + t(' - zh-CN: Simplified Chinese'), + t(' - en-US: English'), + '', + t( + 'To request additional UI language packs, please open an issue on GitHub.', + ), + ].join('\n'), + }; + } + + const langArg = parts[1].toLowerCase(); + let targetLang: SupportedLanguage | null = null; + + if (langArg === 'en' || langArg === 'english' || langArg === 'en-us') { + targetLang = 'en'; + } else if ( + langArg === 'zh' || + langArg === 'chinese' || + langArg === '中文' || + langArg === 'zh-cn' + ) { + targetLang = 'zh'; + } else { + return { + type: 'message', + messageType: 'error', + content: t('Invalid language. Available: en-US, zh-CN'), + }; + } + + return setUiLanguage(context, targetLang); + } else if (subcommand === 'output') { + // Handle /language output + if (parts.length === 1) { + return { + type: 'message', + messageType: 'info', + content: [ + t('Set LLM output language'), + '', + t('Usage: /language output '), + ` ${t('Example: /language output 中文')}`, + ].join('\n'), + }; + } + + // Join all parts after "output" as the language name + const language = parts.slice(1).join(' '); + return generateLlmOutputLanguageRuleFile(language); + } else { + // Backward compatibility: treat as UI language + const langArg = trimmedArgs.toLowerCase(); + let targetLang: SupportedLanguage | null = null; + + if (langArg === 'en' || langArg === 'english' || langArg === 'en-us') { + targetLang = 'en'; + } else if ( + langArg === 'zh' || + langArg === 'chinese' || + langArg === '中文' || + langArg === 'zh-cn' + ) { + targetLang = 'zh'; + } else { + return { + type: 'message', + messageType: 'error', + content: [ + t('Invalid command. Available subcommands:'), + ' - /language ui [zh-CN|en-US] - ' + t('Set UI language'), + ' - /language output - ' + t('Set LLM output language'), + ].join('\n'), + }; + } + + return setUiLanguage(context, targetLang); + } + }, + subCommands: [ + { + name: 'ui', + get description() { + return t('Set UI language'); + }, + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + args: string, + ): Promise => { + const trimmedArgs = args.trim(); + if (!trimmedArgs) { + return { + type: 'message', + messageType: 'info', + content: [ + t('Set UI language'), + '', + t('Usage: /language ui [zh-CN|en-US]'), + '', + t('Available options:'), + t(' - zh-CN: Simplified Chinese'), + t(' - en-US: English'), + '', + t( + 'To request additional UI language packs, please open an issue on GitHub.', + ), + ].join('\n'), + }; + } + + const langArg = trimmedArgs.toLowerCase(); + let targetLang: SupportedLanguage | null = null; + + if (langArg === 'en' || langArg === 'english' || langArg === 'en-us') { + targetLang = 'en'; + } else if ( + langArg === 'zh' || + langArg === 'chinese' || + langArg === '中文' || + langArg === 'zh-cn' + ) { + targetLang = 'zh'; + } else { + return { + type: 'message', + messageType: 'error', + content: t('Invalid language. Available: en-US, zh-CN'), + }; + } + + return setUiLanguage(context, targetLang); + }, + subCommands: [ + { + name: 'zh-CN', + altNames: ['zh', 'chinese', '中文'], + get description() { + return t('Set UI language to Simplified Chinese (zh-CN)'); + }, + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + args: string, + ): Promise => { + if (args.trim().length > 0) { + return { + type: 'message', + messageType: 'error', + content: t( + 'Language subcommands do not accept additional arguments.', + ), + }; + } + return setUiLanguage(context, 'zh'); + }, + }, + { + name: 'en-US', + altNames: ['en', 'english'], + get description() { + return t('Set UI language to English (en-US)'); + }, + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + args: string, + ): Promise => { + if (args.trim().length > 0) { + return { + type: 'message', + messageType: 'error', + content: t( + 'Language subcommands do not accept additional arguments.', + ), + }; + } + return setUiLanguage(context, 'en'); + }, + }, + ], + }, + { + name: 'output', + get description() { + return t('Set LLM output language'); + }, + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + args: string, + ): Promise => { + const trimmedArgs = args.trim(); + if (!trimmedArgs) { + return { + type: 'message', + messageType: 'info', + content: [ + t('Set LLM output language'), + '', + t('Usage: /language output '), + ` ${t('Example: /language output 中文')}`, + ` ${t('Example: /language output English')}`, + ` ${t('Example: /language output 日本語')}`, + ].join('\n'), + }; + } + + return generateLlmOutputLanguageRuleFile(trimmedArgs); + }, + }, + ], +}; diff --git a/packages/cli/src/ui/commands/mcpCommand.ts b/packages/cli/src/ui/commands/mcpCommand.ts index 2521e10c..d8fec717 100644 --- a/packages/cli/src/ui/commands/mcpCommand.ts +++ b/packages/cli/src/ui/commands/mcpCommand.ts @@ -24,10 +24,13 @@ import { } from '@qwen-code/qwen-code-core'; import { appEvents, AppEvent } from '../../utils/events.js'; import { MessageType, type HistoryItemMcpStatus } from '../types.js'; +import { t } from '../../i18n/index.js'; const authCommand: SlashCommand = { name: 'auth', - description: 'Authenticate with an OAuth-enabled MCP server', + get description() { + return t('Authenticate with an OAuth-enabled MCP server'); + }, kind: CommandKind.BUILT_IN, action: async ( context: CommandContext, @@ -40,7 +43,7 @@ const authCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Config not loaded.', + content: t('Config not loaded.'), }; } @@ -56,14 +59,14 @@ const authCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: 'No MCP servers configured with OAuth authentication.', + content: t('No MCP servers configured with OAuth authentication.'), }; } return { type: 'message', messageType: 'info', - content: `MCP servers with OAuth authentication:\n${oauthServers.map((s) => ` - ${s}`).join('\n')}\n\nUse /mcp auth to authenticate.`, + content: `${t('MCP servers with OAuth authentication:')}\n${oauthServers.map((s) => ` - ${s}`).join('\n')}\n\n${t('Use /mcp auth to authenticate.')}`, }; } @@ -72,7 +75,7 @@ const authCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: `MCP server '${serverName}' not found.`, + content: t("MCP server '{{name}}' not found.", { name: serverName }), }; } @@ -89,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(), ); @@ -111,7 +119,12 @@ const authCommand: SlashCommand = { context.ui.addItem( { type: 'info', - text: `✅ Successfully authenticated with MCP server '${serverName}'!`, + text: t( + "Successfully authenticated and refreshed tools for '{{name}}'.", + { + name: serverName, + }, + ), }, Date.now(), ); @@ -122,7 +135,9 @@ const authCommand: SlashCommand = { context.ui.addItem( { type: 'info', - text: `Re-discovering tools from '${serverName}'...`, + text: t("Re-discovering tools from '{{name}}'...", { + name: serverName, + }), }, Date.now(), ); @@ -140,13 +155,24 @@ const authCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: `Successfully authenticated and refreshed tools for '${serverName}'.`, + content: t( + "Successfully authenticated and refreshed tools for '{{name}}'.", + { + name: serverName, + }, + ), }; } catch (error) { return { type: 'message', messageType: 'error', - content: `Failed to authenticate with MCP server '${serverName}': ${getErrorMessage(error)}`, + content: t( + "Failed to authenticate with MCP server '{{name}}': {{error}}", + { + name: serverName, + error: getErrorMessage(error), + }, + ), }; } finally { appEvents.removeListener(AppEvent.OauthDisplayMessage, displayListener); @@ -165,7 +191,9 @@ const authCommand: SlashCommand = { const listCommand: SlashCommand = { name: 'list', - description: 'List configured MCP servers and tools', + get description() { + return t('List configured MCP servers and tools'); + }, kind: CommandKind.BUILT_IN, action: async ( context: CommandContext, @@ -176,7 +204,7 @@ const listCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Config not loaded.', + content: t('Config not loaded.'), }; } @@ -185,7 +213,7 @@ const listCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Could not retrieve tool registry.', + content: t('Could not retrieve tool registry.'), }; } @@ -276,7 +304,9 @@ const listCommand: SlashCommand = { const refreshCommand: SlashCommand = { name: 'refresh', - description: 'Restarts MCP servers.', + get description() { + return t('Restarts MCP servers.'); + }, kind: CommandKind.BUILT_IN, action: async ( context: CommandContext, @@ -286,7 +316,7 @@ const refreshCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Config not loaded.', + content: t('Config not loaded.'), }; } @@ -295,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(), ); @@ -324,8 +354,11 @@ const refreshCommand: SlashCommand = { export const mcpCommand: SlashCommand = { name: 'mcp', - description: - 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers', + get description() { + return t( + 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers', + ); + }, kind: CommandKind.BUILT_IN, subCommands: [listCommand, authCommand, refreshCommand], // Default action when no subcommand is provided 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/commands/permissionsCommand.ts b/packages/cli/src/ui/commands/permissionsCommand.ts index 60ef3884..2b6a7c34 100644 --- a/packages/cli/src/ui/commands/permissionsCommand.ts +++ b/packages/cli/src/ui/commands/permissionsCommand.ts @@ -6,10 +6,13 @@ import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const permissionsCommand: SlashCommand = { name: 'permissions', - description: 'Manage folder trust settings', + get description() { + return t('Manage folder trust settings'); + }, kind: CommandKind.BUILT_IN, action: (): OpenDialogActionReturn => ({ type: 'dialog', diff --git a/packages/cli/src/ui/commands/quitCommand.ts b/packages/cli/src/ui/commands/quitCommand.ts index 3e175d9c..fc9683c9 100644 --- a/packages/cli/src/ui/commands/quitCommand.ts +++ b/packages/cli/src/ui/commands/quitCommand.ts @@ -6,10 +6,13 @@ import { formatDuration } from '../utils/formatters.js'; import { CommandKind, type SlashCommand } from './types.js'; +import { t } from '../../i18n/index.js'; export const quitConfirmCommand: SlashCommand = { name: 'quit-confirm', - description: 'Show quit confirmation dialog', + get description() { + return t('Show quit confirmation dialog'); + }, kind: CommandKind.BUILT_IN, action: (context) => { const now = Date.now(); @@ -37,7 +40,9 @@ export const quitConfirmCommand: SlashCommand = { export const quitCommand: SlashCommand = { name: 'quit', altNames: ['exit'], - description: 'exit the cli', + get description() { + return t('exit the cli'); + }, kind: CommandKind.BUILT_IN, action: (context) => { const now = Date.now(); diff --git a/packages/cli/src/ui/commands/settingsCommand.ts b/packages/cli/src/ui/commands/settingsCommand.ts index 4a3665f1..f7052f19 100644 --- a/packages/cli/src/ui/commands/settingsCommand.ts +++ b/packages/cli/src/ui/commands/settingsCommand.ts @@ -6,10 +6,13 @@ import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const settingsCommand: SlashCommand = { name: 'settings', - description: 'View and edit Qwen Code settings', + get description() { + return t('View and edit Qwen Code settings'); + }, kind: CommandKind.BUILT_IN, action: (_context, _args): OpenDialogActionReturn => ({ type: 'dialog', diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index 46b46cba..378f1101 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -20,6 +20,7 @@ import { import type { SlashCommand, SlashCommandActionReturn } from './types.js'; import { CommandKind } from './types.js'; import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js'; +import { t } from '../../i18n/index.js'; export const GITHUB_WORKFLOW_PATHS = [ 'gemini-dispatch/gemini-dispatch.yml', @@ -91,7 +92,9 @@ export async function updateGitignore(gitRepoRoot: string): Promise { export const setupGithubCommand: SlashCommand = { name: 'setup-github', - description: 'Set up GitHub Actions', + get description() { + return t('Set up GitHub Actions'); + }, kind: CommandKind.BUILT_IN, action: async ( context: CommandContext, diff --git a/packages/cli/src/ui/commands/statsCommand.ts b/packages/cli/src/ui/commands/statsCommand.ts index 1fe628ab..cb4a3f51 100644 --- a/packages/cli/src/ui/commands/statsCommand.ts +++ b/packages/cli/src/ui/commands/statsCommand.ts @@ -12,11 +12,14 @@ import { type SlashCommand, CommandKind, } from './types.js'; +import { t } from '../../i18n/index.js'; export const statsCommand: SlashCommand = { name: 'stats', altNames: ['usage'], - description: 'check session stats. Usage: /stats [model|tools]', + get description() { + return t('check session stats. Usage: /stats [model|tools]'); + }, kind: CommandKind.BUILT_IN, action: (context: CommandContext) => { const now = new Date(); @@ -25,7 +28,7 @@ export const statsCommand: SlashCommand = { context.ui.addItem( { type: MessageType.ERROR, - text: 'Session start time is unavailable, cannot calculate stats.', + text: t('Session start time is unavailable, cannot calculate stats.'), }, Date.now(), ); @@ -43,7 +46,9 @@ export const statsCommand: SlashCommand = { subCommands: [ { name: 'model', - description: 'Show model-specific usage statistics.', + get description() { + return t('Show model-specific usage statistics.'); + }, kind: CommandKind.BUILT_IN, action: (context: CommandContext) => { context.ui.addItem( @@ -56,7 +61,9 @@ export const statsCommand: SlashCommand = { }, { name: 'tools', - description: 'Show tool-specific usage statistics.', + get description() { + return t('Show tool-specific usage statistics.'); + }, kind: CommandKind.BUILT_IN, action: (context: CommandContext) => { context.ui.addItem( diff --git a/packages/cli/src/ui/commands/summaryCommand.ts b/packages/cli/src/ui/commands/summaryCommand.ts index 7c666a04..5d943e8e 100644 --- a/packages/cli/src/ui/commands/summaryCommand.ts +++ b/packages/cli/src/ui/commands/summaryCommand.ts @@ -13,11 +13,15 @@ import { } from './types.js'; import { getProjectSummaryPrompt } from '@qwen-code/qwen-code-core'; import type { HistoryItemSummary } from '../types.js'; +import { t } from '../../i18n/index.js'; export const summaryCommand: SlashCommand = { name: 'summary', - description: - 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md', + get description() { + return t( + 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md', + ); + }, kind: CommandKind.BUILT_IN, action: async (context): Promise => { const { config } = context.services; @@ -26,7 +30,7 @@ export const summaryCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'Config not loaded.', + content: t('Config not loaded.'), }; } @@ -35,7 +39,7 @@ export const summaryCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: 'No chat client available to generate summary.', + content: t('No chat client available to generate summary.'), }; } @@ -44,15 +48,18 @@ export const summaryCommand: SlashCommand = { ui.addItem( { type: 'error' as const, - text: 'Already generating summary, wait for previous request to complete', + text: t( + 'Already generating summary, wait for previous request to complete', + ), }, Date.now(), ); return { type: 'message', messageType: 'error', - content: + content: t( 'Already generating summary, wait for previous request to complete', + ), }; } @@ -65,7 +72,7 @@ export const summaryCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: 'No conversation found to summarize.', + content: t('No conversation found to summarize.'), }; } @@ -171,9 +178,12 @@ export const summaryCommand: SlashCommand = { ui.addItem( { type: 'error' as const, - text: `❌ Failed to generate project context summary: ${ - error instanceof Error ? error.message : String(error) - }`, + text: `❌ ${t( + 'Failed to generate project context summary: {{error}}', + { + error: error instanceof Error ? error.message : String(error), + }, + )}`, }, Date.now(), ); @@ -181,9 +191,9 @@ export const summaryCommand: SlashCommand = { return { type: 'message', messageType: 'error', - content: `Failed to generate project context summary: ${ - error instanceof Error ? error.message : String(error) - }`, + content: t('Failed to generate project context summary: {{error}}', { + error: error instanceof Error ? error.message : String(error), + }), }; } }, diff --git a/packages/cli/src/ui/commands/terminalSetupCommand.ts b/packages/cli/src/ui/commands/terminalSetupCommand.ts index 31b473c7..3fb85446 100644 --- a/packages/cli/src/ui/commands/terminalSetupCommand.ts +++ b/packages/cli/src/ui/commands/terminalSetupCommand.ts @@ -7,6 +7,7 @@ import type { MessageActionReturn, SlashCommand } from './types.js'; import { CommandKind } from './types.js'; import { terminalSetup } from '../utils/terminalSetup.js'; +import { t } from '../../i18n/index.js'; /** * Command to configure terminal keybindings for multiline input support. @@ -16,8 +17,11 @@ import { terminalSetup } from '../utils/terminalSetup.js'; */ export const terminalSetupCommand: SlashCommand = { name: 'terminal-setup', - description: - 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)', + get description() { + return t( + 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)', + ); + }, kind: CommandKind.BUILT_IN, action: async (): Promise => { @@ -27,7 +31,8 @@ export const terminalSetupCommand: SlashCommand = { let content = result.message; if (result.requiresRestart) { content += - '\n\nPlease restart your terminal for the changes to take effect.'; + '\n\n' + + t('Please restart your terminal for the changes to take effect.'); } return { @@ -38,7 +43,9 @@ export const terminalSetupCommand: SlashCommand = { } catch (error) { return { type: 'message', - content: `Failed to configure terminal: ${error}`, + content: t('Failed to configure terminal: {{error}}', { + error: String(error), + }), messageType: 'error', }; } diff --git a/packages/cli/src/ui/commands/themeCommand.ts b/packages/cli/src/ui/commands/themeCommand.ts index 585c84f9..fd366366 100644 --- a/packages/cli/src/ui/commands/themeCommand.ts +++ b/packages/cli/src/ui/commands/themeCommand.ts @@ -6,10 +6,13 @@ import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const themeCommand: SlashCommand = { name: 'theme', - description: 'change the theme', + get description() { + return t('change the theme'); + }, kind: CommandKind.BUILT_IN, action: (_context, _args): OpenDialogActionReturn => ({ type: 'dialog', diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index 4378c450..4bd97e3e 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -10,10 +10,13 @@ import { CommandKind, } from './types.js'; import { MessageType, type HistoryItemToolsList } from '../types.js'; +import { t } from '../../i18n/index.js'; export const toolsCommand: SlashCommand = { name: 'tools', - description: 'list available Qwen Code tools. Usage: /tools [desc]', + get description() { + return t('list available Qwen Code tools. Usage: /tools [desc]'); + }, kind: CommandKind.BUILT_IN, action: async (context: CommandContext, args?: string): Promise => { const subCommand = args?.trim(); @@ -29,7 +32,7 @@ export const toolsCommand: SlashCommand = { context.ui.addItem( { type: MessageType.ERROR, - text: 'Could not retrieve tool registry.', + text: t('Could not retrieve tool registry.'), }, Date.now(), ); diff --git a/packages/cli/src/ui/commands/vimCommand.ts b/packages/cli/src/ui/commands/vimCommand.ts index b398cc48..8f3dc6bd 100644 --- a/packages/cli/src/ui/commands/vimCommand.ts +++ b/packages/cli/src/ui/commands/vimCommand.ts @@ -6,10 +6,13 @@ import type { SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { t } from '../../i18n/index.js'; export const vimCommand: SlashCommand = { name: 'vim', - description: 'toggle vim mode on/off', + get description() { + return t('toggle vim mode on/off'); + }, kind: CommandKind.BUILT_IN, action: async (context, _args) => { const newVimState = await context.ui.toggleVimEnabled(); diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx index fba5fb13..e04fd42c 100644 --- a/packages/cli/src/ui/components/AboutBox.tsx +++ b/packages/cli/src/ui/components/AboutBox.tsx @@ -13,6 +13,7 @@ import { getFieldValue, type SystemInfoField, } from '../../utils/systemInfoFields.js'; +import { t } from '../../i18n/index.js'; type AboutBoxProps = ExtendedSystemInfo; @@ -30,7 +31,7 @@ export const AboutBox: React.FC = (props) => { > - About Qwen Code + {t('About Qwen Code')} {fields.map((field: SystemInfoField) => ( diff --git a/packages/cli/src/ui/components/ApprovalModeDialog.tsx b/packages/cli/src/ui/components/ApprovalModeDialog.tsx index eb6441ec..163a45fd 100644 --- a/packages/cli/src/ui/components/ApprovalModeDialog.tsx +++ b/packages/cli/src/ui/components/ApprovalModeDialog.tsx @@ -15,6 +15,7 @@ import { SettingScope } from '../../config/settings.js'; import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { ScopeSelector } from './shared/ScopeSelector.js'; +import { t } from '../../i18n/index.js'; interface ApprovalModeDialogProps { /** Callback function when an approval mode is selected */ @@ -33,15 +34,15 @@ interface ApprovalModeDialogProps { const formatModeDescription = (mode: ApprovalMode): string => { switch (mode) { case ApprovalMode.PLAN: - return 'Analyze only, do not modify files or execute commands'; + return t('Analyze only, do not modify files or execute commands'); case ApprovalMode.DEFAULT: - return 'Require approval for file edits or shell commands'; + return t('Require approval for file edits or shell commands'); case ApprovalMode.AUTO_EDIT: - return 'Automatically approve file edits'; + return t('Automatically approve file edits'); case ApprovalMode.YOLO: - return 'Automatically approve all tools'; + return t('Automatically approve all tools'); default: - return `${mode} mode`; + return t('{{mode}} mode', { mode }); } }; @@ -134,7 +135,8 @@ export function ApprovalModeDialog({ {/* Approval Mode Selection */} - {focusSection === 'mode' ? '> ' : ' '}Approval Mode{' '} + {focusSection === 'mode' ? '> ' : ' '} + {t('Approval Mode')}{' '} {otherScopeModifiedMessage} @@ -167,15 +169,17 @@ export function ApprovalModeDialog({ {showWorkspacePriorityWarning && ( <> - ⚠ Workspace approval mode exists and takes priority. User-level - change will have no effect. + ⚠{' '} + {t( + 'Workspace approval mode exists and takes priority. User-level change will have no effect.', + )} )} - (Use Enter to select, Tab to change focus) + {t('(Use Enter to select, Tab to change focus)')} diff --git a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx index ec7c1604..550c77dc 100644 --- a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx +++ b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx @@ -8,6 +8,7 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { ApprovalMode } from '@qwen-code/qwen-code-core'; +import { t } from '../../i18n/index.js'; interface AutoAcceptIndicatorProps { approvalMode: ApprovalMode; @@ -23,18 +24,18 @@ export const AutoAcceptIndicator: React.FC = ({ switch (approvalMode) { case ApprovalMode.PLAN: textColor = theme.status.success; - textContent = 'plan mode'; - subText = ' (shift + tab to cycle)'; + textContent = t('plan mode'); + subText = ` ${t('(shift + tab to cycle)')}`; break; case ApprovalMode.AUTO_EDIT: textColor = theme.status.warning; - textContent = 'auto-accept edits'; - subText = ' (shift + tab to cycle)'; + textContent = t('auto-accept edits'); + subText = ` ${t('(shift + tab to cycle)')}`; break; case ApprovalMode.YOLO: textColor = theme.status.error; - textContent = 'YOLO mode'; - subText = ' (shift + tab to cycle)'; + textContent = t('YOLO mode'); + subText = ` ${t('(shift + tab to cycle)')}`; break; case ApprovalMode.DEFAULT: default: diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 4e255983..1b51227a 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -26,6 +26,7 @@ import { useSettings } from '../contexts/SettingsContext.js'; import { ApprovalMode } from '@qwen-code/qwen-code-core'; import { StreamingState } from '../types.js'; import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; +import { t } from '../../i18n/index.js'; export const Composer = () => { const config = useConfig(); @@ -86,14 +87,16 @@ export const Composer = () => { )} {uiState.ctrlCPressedOnce ? ( - Press Ctrl+C again to exit. + {t('Press Ctrl+C again to exit.')} ) : uiState.ctrlDPressedOnce ? ( - Press Ctrl+D again to exit. + {t('Press Ctrl+D again to exit.')} ) : uiState.showEscapePrompt ? ( - Press Esc again to clear. + + {t('Press Esc again to clear.')} + ) : ( !settings.merged.ui?.hideContextSummary && ( { isEmbeddedShellFocused={uiState.embeddedShellFocused} placeholder={ vimEnabled - ? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode." - : ' Type your message or @path/to/file' + ? ' ' + t("Press 'i' for INSERT mode and 'Esc' for NORMAL mode.") + : ' ' + t('Type your message or @path/to/file') } /> )} 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/EditorSettingsDialog.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.tsx index d81ffad6..6926bf41 100644 --- a/packages/cli/src/ui/components/EditorSettingsDialog.tsx +++ b/packages/cli/src/ui/components/EditorSettingsDialog.tsx @@ -19,6 +19,7 @@ import { SettingScope } from '../../config/settings.js'; import type { EditorType } from '@qwen-code/qwen-code-core'; import { isEditorAvailable } from '@qwen-code/qwen-code-core'; import { useKeypress } from '../hooks/useKeypress.js'; +import { t } from '../../i18n/index.js'; interface EditorDialogProps { onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void; @@ -66,12 +67,16 @@ export function EditorSettingsDialog({ const scopeItems = [ { - label: 'User Settings', + get label() { + return t('User Settings'); + }, value: SettingScope.User, key: SettingScope.User, }, { - label: 'Workspace Settings', + get label() { + return t('Workspace Settings'); + }, value: SettingScope.Workspace, key: SettingScope.Workspace, }, @@ -145,7 +150,8 @@ export function EditorSettingsDialog({ - {focusedSection === 'scope' ? '> ' : ' '}Apply To + {focusedSection === 'scope' ? '> ' : ' '} + {t('Apply To')} = ({ 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/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 25274a12..6091c9e2 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -210,6 +210,7 @@ describe('InputPrompt', () => { inputWidth: 80, suggestionsWidth: 80, focus: true, + placeholder: ' Type your message or @path/to/file', }; }); diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 2bd9b275..8af77059 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -27,6 +27,7 @@ import { parseInputForHighlighting, buildSegmentsForVisualSlice, } from '../utils/highlight.js'; +import { t } from '../../i18n/index.js'; import { clipboardHasImage, saveClipboardImage, @@ -88,7 +89,7 @@ export const InputPrompt: React.FC = ({ config, slashCommands, commandContext, - placeholder = ' Type your message or @path/to/file', + placeholder, focus = true, suggestionsWidth, shellModeActive, @@ -697,13 +698,13 @@ export const InputPrompt: React.FC = ({ let statusText = ''; if (shellModeActive) { statusColor = theme.ui.symbol; - statusText = 'Shell mode'; + statusText = t('Shell mode'); } else if (showYoloStyling) { statusColor = theme.status.error; - statusText = 'YOLO mode'; + statusText = t('YOLO mode'); } else if (showAutoAcceptStyling) { statusColor = theme.status.warning; - statusText = 'Accepting edits'; + statusText = t('Accepting edits'); } return ( diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx index a1a1694a..5fc2c20b 100644 --- a/packages/cli/src/ui/components/LoadingIndicator.tsx +++ b/packages/cli/src/ui/components/LoadingIndicator.tsx @@ -14,6 +14,7 @@ import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js'; import { formatDuration } from '../utils/formatters.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; +import { t } from '../../i18n/index.js'; interface LoadingIndicatorProps { currentLoadingPhrase?: string; @@ -40,7 +41,12 @@ export const LoadingIndicator: React.FC = ({ const cancelAndTimerContent = streamingState !== StreamingState.WaitingForConfirmation - ? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})` + ? t('(esc to cancel, {{time}})', { + time: + elapsedTime < 60 + ? `${elapsedTime}s` + : formatDuration(elapsedTime * 1000), + }) : null; return ( 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/ModelStatsDisplay.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.tsx index 95a8fe46..ce0a481e 100644 --- a/packages/cli/src/ui/components/ModelStatsDisplay.tsx +++ b/packages/cli/src/ui/components/ModelStatsDisplay.tsx @@ -15,6 +15,7 @@ import { } from '../utils/computeStats.js'; import type { ModelMetrics } from '../contexts/SessionContext.js'; import { useSessionStats } from '../contexts/SessionContext.js'; +import { t } from '../../i18n/index.js'; const METRIC_COL_WIDTH = 28; const MODEL_COL_WIDTH = 22; @@ -65,7 +66,7 @@ export const ModelStatsDisplay: React.FC = () => { paddingX={2} > - No API calls have been made in this session. + {t('No API calls have been made in this session.')} ); @@ -94,7 +95,7 @@ export const ModelStatsDisplay: React.FC = () => { paddingX={2} > - Model Stats For Nerds + {t('Model Stats For Nerds')} @@ -102,7 +103,7 @@ export const ModelStatsDisplay: React.FC = () => { - Metric + {t('Metric')} {modelNames.map((name) => ( @@ -125,13 +126,13 @@ export const ModelStatsDisplay: React.FC = () => { /> {/* API Section */} - + m.api.totalRequests.toLocaleString())} /> { const errorRate = calculateErrorRate(m); return ( @@ -146,7 +147,7 @@ export const ModelStatsDisplay: React.FC = () => { })} /> { const avgLatency = calculateAverageLatency(m); return formatDuration(avgLatency); @@ -156,9 +157,9 @@ export const ModelStatsDisplay: React.FC = () => { {/* Tokens Section */} - + ( {m.tokens.total.toLocaleString()} @@ -166,13 +167,13 @@ export const ModelStatsDisplay: React.FC = () => { ))} /> m.tokens.prompt.toLocaleString())} /> {hasCached && ( { const cacheHitRate = calculateCacheHitRate(m); @@ -186,20 +187,20 @@ export const ModelStatsDisplay: React.FC = () => { )} {hasThoughts && ( m.tokens.thoughts.toLocaleString())} /> )} {hasTool && ( m.tokens.tool.toLocaleString())} /> )} m.tokens.candidates.toLocaleString())} /> diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx index 0dc89bc7..ae65d358 100644 --- a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx +++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx @@ -10,6 +10,7 @@ import { z } from 'zod'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; +import { t } from '../../i18n/index.js'; interface OpenAIKeyPromptProps { onSubmit: (apiKey: string, baseUrl: string, model: string) => void; @@ -64,9 +65,11 @@ export function OpenAIKeyPrompt({ const errorMessage = error.errors .map((e) => `${e.path.join('.')}: ${e.message}`) .join(', '); - setValidationError(`Invalid credentials: ${errorMessage}`); + setValidationError( + t('Invalid credentials: {{errorMessage}}', { errorMessage }), + ); } else { - setValidationError('Failed to validate credentials'); + setValidationError(t('Failed to validate credentials')); } } }; @@ -205,7 +208,7 @@ export function OpenAIKeyPrompt({ width="100%" > - OpenAI Configuration Required + {t('OpenAI Configuration Required')} {validationError && ( @@ -214,7 +217,9 @@ export function OpenAIKeyPrompt({ )} - Please enter your OpenAI configuration. You can get an API key from{' '} + {t( + 'Please enter your OpenAI configuration. You can get an API key from', + )}{' '} https://bailian.console.aliyun.com/?tab=model#/api-key @@ -225,7 +230,7 @@ export function OpenAIKeyPrompt({ - API Key: + {t('API Key:')} @@ -240,7 +245,7 @@ export function OpenAIKeyPrompt({ - Base URL: + {t('Base URL:')} @@ -255,7 +260,7 @@ export function OpenAIKeyPrompt({ - Model: + {t('Model:')} @@ -267,7 +272,7 @@ export function OpenAIKeyPrompt({ - Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel + {t('Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel')} diff --git a/packages/cli/src/ui/components/ProQuotaDialog.tsx b/packages/cli/src/ui/components/ProQuotaDialog.tsx index 0f3c4a55..cc9bd5f8 100644 --- a/packages/cli/src/ui/components/ProQuotaDialog.tsx +++ b/packages/cli/src/ui/components/ProQuotaDialog.tsx @@ -8,6 +8,7 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { theme } from '../semantic-colors.js'; +import { t } from '../../i18n/index.js'; interface ProQuotaDialogProps { failedModel: string; @@ -22,12 +23,12 @@ export function ProQuotaDialog({ }: ProQuotaDialogProps): React.JSX.Element { const items = [ { - label: 'Change auth (executes the /auth command)', + label: t('Change auth (executes the /auth command)'), value: 'auth' as const, key: 'auth', }, { - label: `Continue with ${fallbackModel}`, + label: t('Continue with {{model}}', { model: fallbackModel }), value: 'continue' as const, key: 'continue', }, @@ -40,7 +41,7 @@ export function ProQuotaDialog({ return ( - Pro quota limit reached for {failedModel}. + {t('Pro quota limit reached for {{model}}.', { model: failedModel })} = ({ const options: Array> = [ { key: 'quit', - label: 'Quit immediately (/quit)', + label: t('Quit immediately (/quit)'), value: QuitChoice.QUIT, }, { key: 'summary-and-quit', - label: 'Generate summary and quit (/summary)', + label: t('Generate summary and quit (/summary)'), value: QuitChoice.SUMMARY_AND_QUIT, }, { key: 'save-and-quit', - label: 'Save conversation and quit (/chat save)', + label: t('Save conversation and quit (/chat save)'), value: QuitChoice.SAVE_AND_QUIT, }, { key: 'cancel', - label: 'Cancel (stay in application)', + label: t('Cancel (stay in application)'), value: QuitChoice.CANCEL, }, ]; @@ -69,7 +70,7 @@ export const QuitConfirmationDialog: React.FC = ({ marginLeft={1} > - What would you like to do before exiting? + {t('What would you like to do before exiting?')} diff --git a/packages/cli/src/ui/components/QwenOAuthProgress.tsx b/packages/cli/src/ui/components/QwenOAuthProgress.tsx index 3e630fb3..d83bfb04 100644 --- a/packages/cli/src/ui/components/QwenOAuthProgress.tsx +++ b/packages/cli/src/ui/components/QwenOAuthProgress.tsx @@ -13,6 +13,7 @@ import qrcode from 'qrcode-terminal'; import { Colors } from '../colors.js'; import type { DeviceAuthorizationData } from '@qwen-code/qwen-code-core'; import { useKeypress } from '../hooks/useKeypress.js'; +import { t } from '../../i18n/index.js'; interface QwenOAuthProgressProps { onTimeout: () => void; @@ -52,11 +53,11 @@ function QrCodeDisplay({ width="100%" > - Qwen OAuth Authentication + {t('Qwen OAuth Authentication')} - Please visit this URL to authorize: + {t('Please visit this URL to authorize:')} @@ -66,7 +67,7 @@ function QrCodeDisplay({ - Or scan the QR code below: + {t('Or scan the QR code below:')} @@ -103,15 +104,18 @@ function StatusDisplay({ > - Waiting for authorization{dots} + {t('Waiting for authorization')} + {dots} - Time remaining: {formatTime(timeRemaining)} + {t('Time remaining:')} {formatTime(timeRemaining)} + + + {t('(Press ESC or CTRL+C to cancel)')} - (Press ESC or CTRL+C to cancel) ); @@ -215,19 +219,24 @@ export function QwenOAuthProgress({ width="100%" > - Qwen OAuth Authentication Timeout + {t('Qwen OAuth Authentication Timeout')} {authMessage || - `OAuth token expired (over ${defaultTimeout} seconds). Please select authentication method again.`} + t( + 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.', + { + seconds: defaultTimeout.toString(), + }, + )} - Press any key to return to authentication type selection. + {t('Press any key to return to authentication type selection.')} @@ -275,16 +284,17 @@ export function QwenOAuthProgress({ > - Waiting for Qwen OAuth authentication... + + {t('Waiting for Qwen OAuth authentication...')} - Time remaining: {Math.floor(timeRemaining / 60)}: + {t('Time remaining:')} {Math.floor(timeRemaining / 60)}: {(timeRemaining % 60).toString().padStart(2, '0')} - (Press ESC or CTRL+C to cancel) + {t('(Press ESC or CTRL+C to cancel)')} diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx index d4a0a11d..c8d79e0e 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx @@ -6,6 +6,7 @@ import type React from 'react'; import { StatsDisplay } from './StatsDisplay.js'; +import { t } from '../../i18n/index.js'; interface SessionSummaryDisplayProps { duration: string; @@ -14,5 +15,8 @@ interface SessionSummaryDisplayProps { export const SessionSummaryDisplay: React.FC = ({ duration, }) => ( - + ); diff --git a/packages/cli/src/ui/components/SettingsDialog.tsx b/packages/cli/src/ui/components/SettingsDialog.tsx index 210672bb..45b0f554 100644 --- a/packages/cli/src/ui/components/SettingsDialog.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.tsx @@ -11,6 +11,7 @@ import type { LoadedSettings, Settings } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js'; import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { ScopeSelector } from './shared/ScopeSelector.js'; +import { t } from '../../i18n/index.js'; import { getDialogSettingKeys, setPendingSettingValue, @@ -124,7 +125,9 @@ export function SettingsDialog({ const definition = getSettingDefinition(key); return { - label: definition?.label || key, + label: definition?.label + ? t(definition.label) || definition.label + : key, value: key, type: definition?.type, toggle: () => { @@ -779,7 +782,8 @@ export function SettingsDialog({ > - {focusSection === 'settings' ? '> ' : ' '}Settings + {focusSection === 'settings' ? '> ' : ' '} + {t('Settings')} {showScrollUp && } @@ -916,13 +920,15 @@ export function SettingsDialog({ - (Use Enter to select - {showScopeSelection ? ', Tab to change focus' : ''}) + {t('(Use Enter to select{{tabText}})', { + tabText: showScopeSelection ? t(', Tab to change focus') : '', + })} {showRestartPrompt && ( - To see changes, Qwen Code must be restarted. Press r to exit and - apply changes now. + {t( + 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.', + )} )} diff --git a/packages/cli/src/ui/components/ShellConfirmationDialog.tsx b/packages/cli/src/ui/components/ShellConfirmationDialog.tsx index f2ab61b0..d83bf9bc 100644 --- a/packages/cli/src/ui/components/ShellConfirmationDialog.tsx +++ b/packages/cli/src/ui/components/ShellConfirmationDialog.tsx @@ -12,6 +12,7 @@ import { RenderInline } from '../utils/InlineMarkdownRenderer.js'; import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; +import { t } from '../../i18n/index.js'; export interface ShellConfirmationRequest { commands: string[]; @@ -51,17 +52,17 @@ export const ShellConfirmationDialog: React.FC< const options: Array> = [ { - label: 'Yes, allow once', + label: t('Yes, allow once'), value: ToolConfirmationOutcome.ProceedOnce, key: 'Yes, allow once', }, { - label: 'Yes, allow always for this session', + label: t('Yes, allow always for this session'), value: ToolConfirmationOutcome.ProceedAlways, key: 'Yes, allow always for this session', }, { - label: 'No (esc)', + label: t('No (esc)'), value: ToolConfirmationOutcome.Cancel, key: 'No (esc)', }, @@ -78,10 +79,10 @@ export const ShellConfirmationDialog: React.FC< > - Shell Command Execution + {t('Shell Command Execution')} - A custom command wants to run the following shell commands: + {t('A custom command wants to run the following shell commands:')} - Do you want to proceed? + {t('Do you want to proceed?')} diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 8c7bacd7..a6511942 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -19,6 +19,7 @@ import { USER_AGREEMENT_RATE_MEDIUM, } from '../utils/displayUtils.js'; import { computeSessionStats } from '../utils/computeStats.js'; +import { t } from '../../i18n/index.js'; // A more flexible and powerful StatRow component interface StatRowProps { @@ -85,22 +86,22 @@ const ModelUsageTable: React.FC<{ - Model Usage + {t('Model Usage')} - Reqs + {t('Reqs')} - Input Tokens + {t('Input Tokens')} - Output Tokens + {t('Output Tokens')} @@ -141,13 +142,14 @@ const ModelUsageTable: React.FC<{ {cacheEfficiency > 0 && ( - Savings Highlight:{' '} + {t('Savings Highlight:')}{' '} {totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)} - %) of input tokens were served from the cache, reducing costs. + %){' '} + {t('of input tokens were served from the cache, reducing costs.')} - » Tip: For a full token breakdown, run `/stats model`. + » {t('Tip: For a full token breakdown, run `/stats model`.')} )} @@ -199,7 +201,7 @@ export const StatsDisplay: React.FC = ({ } return ( - Session Stats + {t('Session Stats')} ); }; @@ -215,33 +217,33 @@ export const StatsDisplay: React.FC = ({ {renderTitle()} -
- +
+ {stats.sessionId} - + {tools.totalCalls} ({' '} ✓ {tools.totalSuccess}{' '} x {tools.totalFail} ) - + {computed.successRate.toFixed(1)}% {computed.totalDecisions > 0 && ( - + {computed.agreementRate.toFixed(1)}%{' '} - ({computed.totalDecisions} reviewed) + ({computed.totalDecisions} {t('reviewed')}) )} {files && (files.totalLinesAdded > 0 || files.totalLinesRemoved > 0) && ( - + +{files.totalLinesAdded} @@ -254,16 +256,16 @@ export const StatsDisplay: React.FC = ({ )}
-
- +
+ {duration} - + {formatDuration(computed.agentActiveTime)} - + {formatDuration(computed.totalApiTime)}{' '} @@ -271,7 +273,7 @@ export const StatsDisplay: React.FC = ({ - + {formatDuration(computed.totalToolTime)}{' '} diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index 468ec888..a12fed79 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -17,6 +17,7 @@ import { SettingScope } from '../../config/settings.js'; import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { ScopeSelector } from './shared/ScopeSelector.js'; +import { t } from '../../i18n/index.js'; interface ThemeDialogProps { /** Callback function when a theme is selected */ @@ -198,7 +199,8 @@ export function ThemeDialog({ {/* Left Column: Selection */} - {mode === 'theme' ? '> ' : ' '}Select Theme{' '} + {mode === 'theme' ? '> ' : ' '} + {t('Select Theme')}{' '} {otherScopeModifiedMessage} @@ -218,7 +220,7 @@ export function ThemeDialog({ {/* Right Column: Preview */} - Preview + {t('Preview')} {/* Get the Theme object for the highlighted theme, fall back to default if not found */} {(() => { @@ -274,8 +276,9 @@ def fibonacci(n): )} - (Use Enter to {mode === 'theme' ? 'select' : 'apply scope'}, Tab to{' '} - {mode === 'theme' ? 'configure scope' : 'select theme'}) + {mode === 'theme' + ? t('(Use Enter to select, Tab to configure scope)') + : t('(Use Enter to apply scope, Tab to select theme)')} diff --git a/packages/cli/src/ui/components/Tips.tsx b/packages/cli/src/ui/components/Tips.tsx index 810d57ef..c8537b55 100644 --- a/packages/cli/src/ui/components/Tips.tsx +++ b/packages/cli/src/ui/components/Tips.tsx @@ -8,6 +8,7 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { type Config } from '@qwen-code/qwen-code-core'; +import { t } from '../../i18n/index.js'; interface TipsProps { config: Config; @@ -17,12 +18,12 @@ export const Tips: React.FC = ({ config }) => { const geminiMdFileCount = config.getGeminiMdFileCount(); return ( - Tips for getting started: + {t('Tips for getting started:')} - 1. Ask questions, edit files, or run commands. + {t('1. Ask questions, edit files, or run commands.')} - 2. Be specific for the best results. + {t('2. Be specific for the best results.')} {geminiMdFileCount === 0 && ( @@ -30,7 +31,7 @@ export const Tips: React.FC = ({ config }) => { QWEN.md {' '} - files to customize your interactions with Qwen Code. + {t('files to customize your interactions with Qwen Code.')} )} @@ -38,7 +39,7 @@ export const Tips: React.FC = ({ config }) => { /help {' '} - for more information. + {t('for more information.')} ); diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.tsx index e1dcb959..f45dd9e8 100644 --- a/packages/cli/src/ui/components/ToolStatsDisplay.tsx +++ b/packages/cli/src/ui/components/ToolStatsDisplay.tsx @@ -17,6 +17,7 @@ import { } from '../utils/displayUtils.js'; import { useSessionStats } from '../contexts/SessionContext.js'; import type { ToolCallStats } from '@qwen-code/qwen-code-core'; +import { t } from '../../i18n/index.js'; const TOOL_NAME_COL_WIDTH = 25; const CALLS_COL_WIDTH = 8; @@ -68,7 +69,7 @@ export const ToolStatsDisplay: React.FC = () => { paddingX={2} > - No tool calls have been made in this session. + {t('No tool calls have been made in this session.')} ); @@ -103,7 +104,7 @@ export const ToolStatsDisplay: React.FC = () => { width={70} > - Tool Stats For Nerds + {t('Tool Stats For Nerds')} @@ -111,22 +112,22 @@ export const ToolStatsDisplay: React.FC = () => { - Tool Name + {t('Tool Name')} - Calls + {t('Calls')} - Success Rate + {t('Success Rate')} - Avg Duration + {t('Avg Duration')} @@ -151,13 +152,15 @@ export const ToolStatsDisplay: React.FC = () => { {/* User Decision Summary */} - User Decision Summary + {t('User Decision Summary')} - Total Reviewed Suggestions: + + {t('Total Reviewed Suggestions:')} + {totalReviewed} @@ -167,7 +170,7 @@ export const ToolStatsDisplay: React.FC = () => { - » Accepted: + {t(' » Accepted:')} {totalDecisions.accept} @@ -177,7 +180,7 @@ export const ToolStatsDisplay: React.FC = () => { - » Rejected: + {t(' » Rejected:')} {totalDecisions.reject} @@ -187,7 +190,7 @@ export const ToolStatsDisplay: React.FC = () => { - » Modified: + {t(' » Modified:')} {totalDecisions.modify} @@ -209,7 +212,9 @@ export const ToolStatsDisplay: React.FC = () => { - Overall Agreement Rate: + + {t(' Overall Agreement Rate:')} + 0 ? agreementColor : undefined}> diff --git a/packages/cli/src/ui/components/WelcomeBackDialog.tsx b/packages/cli/src/ui/components/WelcomeBackDialog.tsx index d16a2d8c..5ce5de31 100644 --- a/packages/cli/src/ui/components/WelcomeBackDialog.tsx +++ b/packages/cli/src/ui/components/WelcomeBackDialog.tsx @@ -12,6 +12,7 @@ import { type RadioSelectItem, } from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; +import { t } from '../../i18n/index.js'; interface WelcomeBackDialogProps { welcomeBackInfo: ProjectSummaryInfo; @@ -36,12 +37,12 @@ export function WelcomeBackDialog({ const options: Array> = [ { key: 'restart', - label: 'Start new chat session', + label: t('Start new chat session'), value: 'restart', }, { key: 'continue', - label: 'Continue previous conversation', + label: t('Continue previous conversation'), value: 'continue', }, ]; @@ -67,7 +68,9 @@ export function WelcomeBackDialog({ > - 👋 Welcome back! (Last updated: {timeAgo}) + {t('👋 Welcome back! (Last updated: {{timeAgo}})', { + timeAgo: timeAgo || '', + })} @@ -75,7 +78,7 @@ export function WelcomeBackDialog({ {goalContent && ( - 🎯 Overall Goal: + {t('🎯 Overall Goal:')} {goalContent} @@ -87,19 +90,25 @@ export function WelcomeBackDialog({ {totalTasks > 0 && ( - 📋 Current Plan: + 📋 {t('Current Plan:')} - Progress: {doneCount}/{totalTasks} tasks completed - {inProgressCount > 0 && `, ${inProgressCount} in progress`} + {t('Progress: {{done}}/{{total}} tasks completed', { + done: String(doneCount), + total: String(totalTasks), + })} + {inProgressCount > 0 && + t(', {{inProgress}} in progress', { + inProgress: String(inProgressCount), + })} {pendingTasks.length > 0 && ( - Pending Tasks: + {t('Pending Tasks:')} {pendingTasks.map((task: string, index: number) => ( @@ -113,8 +122,8 @@ export function WelcomeBackDialog({ {/* Action Selection */} - What would you like to do? - Choose how to proceed with your session: + {t('What would you like to do?')} + {t('Choose how to proceed with your session:')} diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index cf8d4444..7c2c04f9 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -12,6 +12,8 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly │ │ │ Debug Keystroke Logging false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ @@ -20,8 +22,6 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -46,6 +46,8 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select │ │ │ Debug Keystroke Logging false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ @@ -54,8 +56,6 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -80,6 +80,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett │ │ │ Debug Keystroke Logging false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ @@ -88,8 +90,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -114,6 +114,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin │ │ │ Debug Keystroke Logging false* │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false* │ @@ -122,8 +124,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin │ │ │ Hide Tips false* │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -148,6 +148,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Debug Keystroke Logging false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ @@ -156,8 +158,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -182,6 +182,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Debug Keystroke Logging (Modified in Workspace) false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ @@ -190,8 +192,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -216,6 +216,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set │ │ │ Debug Keystroke Logging false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ @@ -224,8 +226,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -250,6 +250,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and │ │ │ Debug Keystroke Logging false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false* │ @@ -258,8 +260,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -284,6 +284,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security │ │ │ Debug Keystroke Logging false │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ @@ -292,8 +294,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security │ │ │ Hide Tips false │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ @@ -318,6 +318,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se │ │ │ Debug Keystroke Logging true* │ │ │ +│ Language Auto (detect from system) │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title true* │ @@ -326,8 +328,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se │ │ │ Hide Tips true* │ │ │ -│ Hide Banner false │ -│ │ │ ▼ │ │ │ │ │ diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.tsx index cd6224e3..362b9e05 100644 --- a/packages/cli/src/ui/components/messages/CompressionMessage.tsx +++ b/packages/cli/src/ui/components/messages/CompressionMessage.tsx @@ -10,6 +10,7 @@ import Spinner from 'ink-spinner'; import { theme } from '../../semantic-colors.js'; import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js'; import { CompressionStatus } from '@qwen-code/qwen-code-core'; +import { t } from '../../../i18n/index.js'; export interface CompressionDisplayProps { compression: CompressionProps; @@ -30,22 +31,32 @@ export function CompressionMessage({ const getCompressionText = () => { if (isPending) { - return 'Compressing chat history'; + return t('Compressing chat history'); } switch (compressionStatus) { case CompressionStatus.COMPRESSED: - return `Chat history compressed from ${originalTokens} to ${newTokens} tokens.`; + return t( + 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.', + { + originalTokens: String(originalTokens), + newTokens: String(newTokens), + }, + ); case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT: // For smaller histories (< 50k tokens), compression overhead likely exceeds benefits if (originalTokens < 50000) { - return 'Compression was not beneficial for this history size.'; + return t('Compression was not beneficial for this history size.'); } // For larger histories where compression should work but didn't, // this suggests an issue with the compression process itself - return 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.'; + return t( + 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.', + ); case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR: - return 'Could not compress chat history due to a token counting error.'; + return t( + 'Could not compress chat history due to a token counting error.', + ); case CompressionStatus.NOOP: return 'Nothing to compress.'; default: 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/shared/ScopeSelector.tsx b/packages/cli/src/ui/components/shared/ScopeSelector.tsx index 30aa1e40..04ff8080 100644 --- a/packages/cli/src/ui/components/shared/ScopeSelector.tsx +++ b/packages/cli/src/ui/components/shared/ScopeSelector.tsx @@ -9,6 +9,7 @@ import { Box, Text } from 'ink'; import type { SettingScope } from '../../../config/settings.js'; import { getScopeItems } from '../../../utils/dialogScopeUtils.js'; import { RadioButtonSelect } from './RadioButtonSelect.js'; +import { t } from '../../../i18n/index.js'; interface ScopeSelectorProps { /** Callback function when a scope is selected */ @@ -29,6 +30,7 @@ export function ScopeSelector({ }: ScopeSelectorProps): React.JSX.Element { const scopeItems = getScopeItems().map((item) => ({ ...item, + label: t(item.label), key: item.value, })); @@ -40,7 +42,8 @@ export function ScopeSelector({ return ( - {isFocused ? '> ' : ' '}Apply To + {isFocused ? '> ' : ' '} + {t('Apply To')} void; @@ -90,25 +91,25 @@ export function AgentCreationWizard({ const n = state.currentStep; switch (kind) { case 'LOCATION': - return `Step ${n}: Choose Location`; + return t('Step {{n}}: Choose Location', { n: n.toString() }); case 'GEN_METHOD': - return `Step ${n}: Choose Generation Method`; + return t('Step {{n}}: Choose Generation Method', { n: n.toString() }); case 'LLM_DESC': - return `Step ${n}: Describe Your Subagent`; + return t('Step {{n}}: Describe Your Subagent', { n: n.toString() }); case 'MANUAL_NAME': - return `Step ${n}: Enter Subagent Name`; + return t('Step {{n}}: Enter Subagent Name', { n: n.toString() }); case 'MANUAL_PROMPT': - return `Step ${n}: Enter System Prompt`; + return t('Step {{n}}: Enter System Prompt', { n: n.toString() }); case 'MANUAL_DESC': - return `Step ${n}: Enter Description`; + return t('Step {{n}}: Enter Description', { n: n.toString() }); case 'TOOLS': - return `Step ${n}: Select Tools`; + return t('Step {{n}}: Select Tools', { n: n.toString() }); case 'COLOR': - return `Step ${n}: Choose Background Color`; + return t('Step {{n}}: Choose Background Color', { n: n.toString() }); case 'FINAL': - return `Step ${n}: Confirm and Save`; + return t('Step {{n}}: Confirm and Save', { n: n.toString() }); default: - return 'Unknown Step'; + return t('Unknown Step'); } }; @@ -163,11 +164,11 @@ export function AgentCreationWizard({ // Special case: During generation in description input step, only show cancel option const kind = getStepKind(state.generationMethod, state.currentStep); if (kind === 'LLM_DESC' && state.isGenerating) { - return 'Esc to cancel'; + return t('Esc to cancel'); } if (getStepKind(state.generationMethod, state.currentStep) === 'FINAL') { - return 'Press Enter to save, e to save and edit, Esc to go back'; + return t('Press Enter to save, e to save and edit, Esc to go back'); } // Steps that have ↑↓ navigation (RadioButtonSelect components) @@ -177,14 +178,17 @@ export function AgentCreationWizard({ kindForNav === 'GEN_METHOD' || kindForNav === 'TOOLS' || kindForNav === 'COLOR'; - const navigationPart = hasNavigation ? '↑↓ to navigate, ' : ''; + const navigationPart = hasNavigation ? t('↑↓ to navigate, ') : ''; const escAction = state.currentStep === WIZARD_STEPS.LOCATION_SELECTION - ? 'cancel' - : 'go back'; + ? t('cancel') + : t('go back'); - return `Press Enter to continue, ${navigationPart}Esc to ${escAction}`; + return t('Press Enter to continue, {{navigation}}Esc to {{action}}', { + navigation: navigationPart, + action: escAction, + }); }; return ( @@ -210,16 +214,16 @@ export function AgentCreationWizard({ state={state} dispatch={dispatch} onNext={handleNext} - description="Enter a clear, unique name for this subagent." - placeholder="e.g., Code Reviewer" + description={t('Enter a clear, unique name for this subagent.')} + placeholder={t('e.g., Code Reviewer')} height={1} initialText={state.generatedName} - onChange={(t) => { - const value = t; // keep raw, trim later when validating + onChange={(text) => { + const value = text; // keep raw, trim later when validating dispatch({ type: 'SET_GENERATED_NAME', name: value }); }} - validate={(t) => - t.trim().length === 0 ? 'Name cannot be empty.' : null + validate={(text) => + text.trim().length === 0 ? t('Name cannot be empty.') : null } /> ); @@ -230,18 +234,22 @@ export function AgentCreationWizard({ state={state} dispatch={dispatch} onNext={handleNext} - description="Write the system prompt that defines this subagent's behavior. Be comprehensive for best results." - placeholder="e.g., You are an expert code reviewer..." + description={t( + "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.", + )} + placeholder={t('e.g., You are an expert code reviewer...')} height={10} initialText={state.generatedSystemPrompt} - onChange={(t) => { + onChange={(text) => { dispatch({ type: 'SET_GENERATED_SYSTEM_PROMPT', - systemPrompt: t, + systemPrompt: text, }); }} - validate={(t) => - t.trim().length === 0 ? 'System prompt cannot be empty.' : null + validate={(text) => + text.trim().length === 0 + ? t('System prompt cannot be empty.') + : null } /> ); @@ -252,15 +260,24 @@ export function AgentCreationWizard({ state={state} dispatch={dispatch} onNext={handleNext} - description="Describe when and how this subagent should be used." - placeholder="e.g., Reviews code for best practices and potential bugs." + description={t( + 'Describe when and how this subagent should be used.', + )} + placeholder={t( + 'e.g., Reviews code for best practices and potential bugs.', + )} height={6} initialText={state.generatedDescription} - onChange={(t) => { - dispatch({ type: 'SET_GENERATED_DESCRIPTION', description: t }); + onChange={(text) => { + dispatch({ + type: 'SET_GENERATED_DESCRIPTION', + description: text, + }); }} - validate={(t) => - t.trim().length === 0 ? 'Description cannot be empty.' : null + validate={(text) => + text.trim().length === 0 + ? t('Description cannot be empty.') + : null } /> ); @@ -292,7 +309,9 @@ export function AgentCreationWizard({ return ( - Invalid step: {state.currentStep} + {t('Invalid step: {{step}}', { + step: state.currentStep.toString(), + })} ); diff --git a/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx b/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx index 9a8cd81a..f9174b66 100644 --- a/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx +++ b/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx @@ -15,6 +15,7 @@ import { theme } from '../../../semantic-colors.js'; import { shouldShowColor, getColorForDisplay } from '../utils.js'; import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js'; import { useKeypress } from '../../../hooks/useKeypress.js'; +import { t } from '../../../../i18n/index.js'; /** * Step 6: Final confirmation and actions. @@ -62,15 +63,24 @@ export function CreationSummary({ if (conflictLevel === targetLevel) { allWarnings.push( - `Name "${state.generatedName}" already exists at ${conflictLevel} level - will overwrite existing subagent`, + t( + 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent', + { name: state.generatedName, level: conflictLevel }, + ), ); } else if (targetLevel === 'project') { allWarnings.push( - `Name "${state.generatedName}" exists at user level - project level will take precedence`, + t( + 'Name "{{name}}" exists at user level - project level will take precedence', + { name: state.generatedName }, + ), ); } else { allWarnings.push( - `Name "${state.generatedName}" exists at project level - existing subagent will take precedence`, + t( + 'Name "{{name}}" exists at project level - existing subagent will take precedence', + { name: state.generatedName }, + ), ); } } @@ -83,12 +93,16 @@ export function CreationSummary({ // Check length warnings if (state.generatedDescription.length > 300) { allWarnings.push( - `Description is over ${state.generatedDescription.length} characters`, + t('Description is over {{length}} characters', { + length: state.generatedDescription.length.toString(), + }), ); } if (state.generatedSystemPrompt.length > 10000) { allWarnings.push( - `System prompt is over ${state.generatedSystemPrompt.length} characters`, + t('System prompt is over {{length}} characters', { + length: state.generatedSystemPrompt.length.toString(), + }), ); } @@ -181,7 +195,9 @@ export function CreationSummary({ showSuccessAndClose(); } catch (error) { setSaveError( - `Failed to save and edit subagent: ${error instanceof Error ? error.message : 'Unknown error'}`, + t('Failed to save and edit subagent: {{error}}', { + error: error instanceof Error ? error.message : 'Unknown error', + }), ); } }, [ @@ -215,13 +231,15 @@ export function CreationSummary({ - ✅ Subagent Created Successfully! + {t('✅ Subagent Created Successfully!')} - Subagent "{state.generatedName}" has been saved to{' '} - {state.location} level. + {t('Subagent "{{name}}" has been saved to {{level}} level.', { + name: state.generatedName, + level: state.location, + })} @@ -232,35 +250,35 @@ export function CreationSummary({ - Name: + {t('Name: ')} {state.generatedName} - Location: + {t('Location: ')} {state.location === 'project' - ? 'Project Level (.qwen/agents/)' - : 'User Level (~/.qwen/agents/)'} + ? t('Project Level (.qwen/agents/)') + : t('User Level (~/.qwen/agents/)')} - Tools: + {t('Tools: ')} {toolsDisplay} {shouldShowColor(state.color) && ( - Color: + {t('Color: ')} {state.color} )} - Description: + {t('Description:')} @@ -269,7 +287,7 @@ export function CreationSummary({ - System Prompt: + {t('System Prompt:')} @@ -281,7 +299,7 @@ export function CreationSummary({ {saveError && ( - ❌ Error saving subagent: + {t('❌ Error saving subagent:')} @@ -294,7 +312,7 @@ export function CreationSummary({ {warnings.length > 0 && ( - Warnings: + {t('Warnings:')} {warnings.map((warning, index) => ( diff --git a/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx b/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx index d81cafb2..4e7f4491 100644 --- a/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx +++ b/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx @@ -14,6 +14,7 @@ import { useKeypress, type Key } from '../../../hooks/useKeypress.js'; import { keyMatchers, Command } from '../../../keyMatchers.js'; import { theme } from '../../../semantic-colors.js'; import { TextInput } from '../../shared/TextInput.js'; +import { t } from '../../../../i18n/index.js'; /** * Step 3: Description input with LLM generation. @@ -103,7 +104,9 @@ export function DescriptionInput({ dispatch({ type: 'SET_VALIDATION_ERRORS', errors: [ - `Failed to generate subagent: ${error instanceof Error ? error.message : 'Unknown error'}`, + t('Failed to generate subagent: {{error}}', { + error: error instanceof Error ? error.message : 'Unknown error', + }), ], }); } @@ -135,15 +138,17 @@ export function DescriptionInput({ isActive: state.isGenerating, }); - const placeholder = - 'e.g., Expert code reviewer that reviews code based on best practices...'; + const placeholder = t( + 'e.g., Expert code reviewer that reviews code based on best practices...', + ); return ( - Describe what this subagent should do and when it should be used. (Be - comprehensive for best results) + {t( + 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)', + )} @@ -153,7 +158,7 @@ export function DescriptionInput({ - Generating subagent configuration... + {t('Generating subagent configuration...')} ) : ( diff --git a/packages/cli/src/ui/components/subagents/create/GenerationMethodSelector.tsx b/packages/cli/src/ui/components/subagents/create/GenerationMethodSelector.tsx index 0018e8bd..b7f111e6 100644 --- a/packages/cli/src/ui/components/subagents/create/GenerationMethodSelector.tsx +++ b/packages/cli/src/ui/components/subagents/create/GenerationMethodSelector.tsx @@ -7,6 +7,7 @@ import { Box } from 'ink'; import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; import type { WizardStepProps } from '../types.js'; +import { t } from '../../../../i18n/index.js'; interface GenerationOption { label: string; @@ -15,11 +16,15 @@ interface GenerationOption { const generationOptions: GenerationOption[] = [ { - label: 'Generate with Qwen Code (Recommended)', + get label() { + return t('Generate with Qwen Code (Recommended)'); + }, value: 'qwen', }, { - label: 'Manual Creation', + get label() { + return t('Manual Creation'); + }, value: 'manual', }, ]; diff --git a/packages/cli/src/ui/components/subagents/create/LocationSelector.tsx b/packages/cli/src/ui/components/subagents/create/LocationSelector.tsx index 51601730..aad81c3a 100644 --- a/packages/cli/src/ui/components/subagents/create/LocationSelector.tsx +++ b/packages/cli/src/ui/components/subagents/create/LocationSelector.tsx @@ -7,6 +7,7 @@ import { Box } from 'ink'; import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; import type { WizardStepProps } from '../types.js'; +import { t } from '../../../../i18n/index.js'; interface LocationOption { label: string; @@ -15,11 +16,15 @@ interface LocationOption { const locationOptions: LocationOption[] = [ { - label: 'Project Level (.qwen/agents/)', + get label() { + return t('Project Level (.qwen/agents/)'); + }, value: 'project', }, { - label: 'User Level (~/.qwen/agents/)', + get label() { + return t('User Level (~/.qwen/agents/)'); + }, value: 'user', }, ]; diff --git a/packages/cli/src/ui/components/subagents/create/ToolSelector.tsx b/packages/cli/src/ui/components/subagents/create/ToolSelector.tsx index ccea5b61..547e14ed 100644 --- a/packages/cli/src/ui/components/subagents/create/ToolSelector.tsx +++ b/packages/cli/src/ui/components/subagents/create/ToolSelector.tsx @@ -10,6 +10,7 @@ import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; import type { ToolCategory } from '../types.js'; import { Kind, type Config } from '@qwen-code/qwen-code-core'; import { theme } from '../../../semantic-colors.js'; +import { t } from '../../../../i18n/index.js'; interface ToolOption { label: string; @@ -45,7 +46,7 @@ export function ToolSelector({ toolCategories: [ { id: 'all', - name: 'All Tools (Default)', + name: t('All Tools (Default)'), tools: [], }, ], @@ -89,22 +90,22 @@ export function ToolSelector({ const toolCategories = [ { id: 'all', - name: 'All Tools', + name: t('All Tools'), tools: [], }, { id: 'read', - name: 'Read-only Tools', + name: t('Read-only Tools'), tools: readTools, }, { id: 'edit', - name: 'Read & Edit Tools', + name: t('Read & Edit Tools'), tools: [...readTools, ...editTools], }, { id: 'execute', - name: 'Read & Edit & Execution Tools', + name: t('Read & Edit & Execution Tools'), tools: [...readTools, ...editTools, ...executeTools], }, ].filter((category) => category.id === 'all' || category.tools.length > 0); @@ -202,11 +203,11 @@ export function ToolSelector({ {currentCategory.id === 'all' ? ( - All tools selected, including MCP tools + {t('All tools selected, including MCP tools')} ) : currentCategory.tools.length > 0 ? ( <> - Selected tools: + {t('Selected tools:')} {(() => { // Filter the already categorized tools to show only those in current category @@ -224,17 +225,19 @@ export function ToolSelector({ <> {categoryReadTools.length > 0 && ( - • Read-only tools: {categoryReadTools.join(', ')} + • {t('Read-only tools:')}{' '} + {categoryReadTools.join(', ')} )} {categoryEditTools.length > 0 && ( - • Edit tools: {categoryEditTools.join(', ')} + • {t('Edit tools:')} {categoryEditTools.join(', ')} )} {categoryExecuteTools.length > 0 && ( - • Execution tools: {categoryExecuteTools.join(', ')} + • {t('Execution tools:')}{' '} + {categoryExecuteTools.join(', ')} )} diff --git a/packages/cli/src/ui/components/subagents/manage/ActionSelectionStep.tsx b/packages/cli/src/ui/components/subagents/manage/ActionSelectionStep.tsx index c0a6b5a9..28393d08 100644 --- a/packages/cli/src/ui/components/subagents/manage/ActionSelectionStep.tsx +++ b/packages/cli/src/ui/components/subagents/manage/ActionSelectionStep.tsx @@ -9,6 +9,7 @@ import { Box } from 'ink'; import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; import { MANAGEMENT_STEPS } from '../types.js'; import { type SubagentConfig } from '@qwen-code/qwen-code-core'; +import { t } from '../../../../i18n/index.js'; interface ActionSelectionStepProps { selectedAgent: SubagentConfig | null; @@ -27,10 +28,34 @@ export const ActionSelectionStep = ({ // Filter actions based on whether the agent is built-in const allActions = [ - { key: 'view', label: 'View Agent', value: 'view' as const }, - { key: 'edit', label: 'Edit Agent', value: 'edit' as const }, - { key: 'delete', label: 'Delete Agent', value: 'delete' as const }, - { key: 'back', label: 'Back', value: 'back' as const }, + { + key: 'view', + get label() { + return t('View Agent'); + }, + value: 'view' as const, + }, + { + key: 'edit', + get label() { + return t('Edit Agent'); + }, + value: 'edit' as const, + }, + { + key: 'delete', + get label() { + return t('Delete Agent'); + }, + value: 'delete' as const, + }, + { + key: 'back', + get label() { + return t('Back'); + }, + value: 'back' as const, + }, ]; const actions = selectedAgent?.isBuiltin diff --git a/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx b/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx index 245d348e..77cfa47d 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx @@ -9,6 +9,7 @@ import { type SubagentConfig } from '@qwen-code/qwen-code-core'; import type { StepNavigationProps } from '../types.js'; import { theme } from '../../../semantic-colors.js'; import { useKeypress } from '../../../hooks/useKeypress.js'; +import { t } from '../../../../i18n/index.js'; interface AgentDeleteStepProps extends StepNavigationProps { selectedAgent: SubagentConfig | null; @@ -41,7 +42,7 @@ export function AgentDeleteStep({ if (!selectedAgent) { return ( - No agent selected + {t('No agent selected')} ); } @@ -49,8 +50,9 @@ export function AgentDeleteStep({ return ( - Are you sure you want to delete agent “{selectedAgent.name} - ”? + {t('Are you sure you want to delete agent "{{name}}"?', { + name: selectedAgent.name, + })} ); diff --git a/packages/cli/src/ui/components/subagents/manage/AgentEditStep.tsx b/packages/cli/src/ui/components/subagents/manage/AgentEditStep.tsx index 4037dff1..ab1cd2a9 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentEditStep.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentEditStep.tsx @@ -11,6 +11,7 @@ import { MANAGEMENT_STEPS } from '../types.js'; import { theme } from '../../../semantic-colors.js'; import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js'; import { type SubagentConfig } from '@qwen-code/qwen-code-core'; +import { t } from '../../../../i18n/index.js'; interface EditOption { id: string; @@ -20,15 +21,21 @@ interface EditOption { const editOptions: EditOption[] = [ { id: 'editor', - label: 'Open in editor', + get label() { + return t('Open in editor'); + }, }, { id: 'tools', - label: 'Edit tools', + get label() { + return t('Edit tools'); + }, }, { id: 'color', - label: 'Edit color', + get label() { + return t('Edit color'); + }, }, ]; @@ -65,7 +72,9 @@ export function EditOptionsStep({ await launchEditor(selectedAgent?.filePath); } catch (err) { setError( - `Failed to launch editor: ${err instanceof Error ? err.message : 'Unknown error'}`, + t('Failed to launch editor: {{error}}', { + error: err instanceof Error ? err.message : 'Unknown error', + }), ); } } else if (selectedValue === 'tools') { @@ -98,7 +107,7 @@ export function EditOptionsStep({ {error && ( - ❌ Error: + {t('❌ Error:')} diff --git a/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx b/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx index 73076163..613ac87e 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx @@ -9,6 +9,7 @@ import { Box, Text } from 'ink'; import { theme } from '../../../semantic-colors.js'; import { useKeypress } from '../../../hooks/useKeypress.js'; import { type SubagentConfig } from '@qwen-code/qwen-code-core'; +import { t } from '../../../../i18n/index.js'; interface NavigationState { currentBlock: 'project' | 'user' | 'builtin'; @@ -205,9 +206,9 @@ export const AgentSelectionStep = ({ if (availableAgents.length === 0) { return ( - No subagents found. + {t('No subagents found.')} - Use '/agents create' to create your first subagent. + {t("Use '/agents create' to create your first subagent.")} ); @@ -237,7 +238,7 @@ export const AgentSelectionStep = ({ {agent.isBuiltin && ( {' '} - (built-in) + {t('(built-in)')} )} {agent.level === 'user' && projectNames.has(agent.name) && ( @@ -245,7 +246,7 @@ export const AgentSelectionStep = ({ color={isSelected ? theme.status.warning : theme.text.secondary} > {' '} - (overridden by project level agent) + {t('(overridden by project level agent)')} )} @@ -265,7 +266,9 @@ export const AgentSelectionStep = ({ {projectAgents.length > 0 && ( - Project Level ({projectAgents[0].filePath.replace(/\/[^/]+$/, '')}) + {t('Project Level ({{path}})', { + path: projectAgents[0].filePath.replace(/\/[^/]+$/, ''), + })} {projectAgents.map((agent, index) => { @@ -285,7 +288,9 @@ export const AgentSelectionStep = ({ marginBottom={builtinAgents.length > 0 ? 1 : 0} > - User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')}) + {t('User Level ({{path}})', { + path: userAgents[0].filePath.replace(/\/[^/]+$/, ''), + })} {userAgents.map((agent, index) => { @@ -302,7 +307,7 @@ export const AgentSelectionStep = ({ {builtinAgents.length > 0 && ( - Built-in Agents + {t('Built-in Agents')} {builtinAgents.map((agent, index) => { @@ -321,7 +326,9 @@ export const AgentSelectionStep = ({ builtinAgents.length > 0) && ( - Using: {enabledAgentsCount} agents + {t('Using: {{count}} agents', { + count: enabledAgentsCount.toString(), + })} )} diff --git a/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx b/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx index 8f5fd2dd..ee2fd366 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx @@ -8,6 +8,7 @@ import { Box, Text } from 'ink'; import { theme } from '../../../semantic-colors.js'; import { shouldShowColor, getColorForDisplay } from '../utils.js'; import { type SubagentConfig } from '@qwen-code/qwen-code-core'; +import { t } from '../../../../i18n/index.js'; interface AgentViewerStepProps { selectedAgent: SubagentConfig | null; @@ -17,7 +18,7 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => { if (!selectedAgent) { return ( - No agent selected + {t('No agent selected')} ); } @@ -30,31 +31,31 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => { - File Path: + {t('File Path: ')} {agent.filePath} - Tools: + {t('Tools: ')} {toolsDisplay} {shouldShowColor(agent.color) && ( - Color: + {t('Color: ')} {agent.color} )} - Description: + {t('Description:')} {agent.description} - System Prompt: + {t('System Prompt:')} {agent.systemPrompt} diff --git a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx index 5a775001..f496d6bc 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx @@ -18,6 +18,7 @@ import { theme } from '../../../semantic-colors.js'; import { getColorForDisplay, shouldShowColor } from '../utils.js'; import type { SubagentConfig, Config } from '@qwen-code/qwen-code-core'; import { useKeypress } from '../../../hooks/useKeypress.js'; +import { t } from '../../../../i18n/index.js'; interface AgentsManagerDialogProps { onClose: () => void; @@ -143,21 +144,21 @@ export function AgentsManagerDialog({ const getStepHeaderText = () => { switch (currentStep) { case MANAGEMENT_STEPS.AGENT_SELECTION: - return 'Agents'; + return t('Agents'); case MANAGEMENT_STEPS.ACTION_SELECTION: - return 'Choose Action'; + return t('Choose Action'); case MANAGEMENT_STEPS.AGENT_VIEWER: return selectedAgent?.name; case MANAGEMENT_STEPS.EDIT_OPTIONS: - return `Edit ${selectedAgent?.name}`; + return t('Edit {{name}}', { name: selectedAgent?.name || '' }); case MANAGEMENT_STEPS.EDIT_TOOLS: - return `Edit Tools: ${selectedAgent?.name}`; + return t('Edit Tools: {{name}}', { name: selectedAgent?.name || '' }); case MANAGEMENT_STEPS.EDIT_COLOR: - return `Edit Color: ${selectedAgent?.name}`; + return t('Edit Color: {{name}}', { name: selectedAgent?.name || '' }); case MANAGEMENT_STEPS.DELETE_CONFIRMATION: - return `Delete ${selectedAgent?.name}`; + return t('Delete {{name}}', { name: selectedAgent?.name || '' }); default: - return 'Unknown Step'; + return t('Unknown Step'); } }; @@ -183,20 +184,20 @@ export function AgentsManagerDialog({ const getNavigationInstructions = () => { if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) { if (availableAgents.length === 0) { - return 'Esc to close'; + return t('Esc to close'); } - return 'Enter to select, ↑↓ to navigate, Esc to close'; + return t('Enter to select, ↑↓ to navigate, Esc to close'); } if (currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) { - return 'Esc to go back'; + return t('Esc to go back'); } if (currentStep === MANAGEMENT_STEPS.DELETE_CONFIRMATION) { - return 'Enter to confirm, Esc to cancel'; + return t('Enter to confirm, Esc to cancel'); } - return 'Enter to select, ↑↓ to navigate, Esc to go back'; + return t('Enter to select, ↑↓ to navigate, Esc to go back'); }; return ( @@ -295,7 +296,9 @@ export function AgentsManagerDialog({ default: return ( - Invalid step: {currentStep} + + {t('Invalid step: {{step}}', { step: currentStep })} + ); } 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/components/views/ToolsList.tsx b/packages/cli/src/ui/components/views/ToolsList.tsx index dd0f753d..061716e6 100644 --- a/packages/cli/src/ui/components/views/ToolsList.tsx +++ b/packages/cli/src/ui/components/views/ToolsList.tsx @@ -9,6 +9,7 @@ import { Box, Text } from 'ink'; import { theme } from '../../semantic-colors.js'; import { type ToolDefinition } from '../../types.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; +import { t } from '../../../i18n/index.js'; interface ToolsListProps { tools: readonly ToolDefinition[]; @@ -23,7 +24,7 @@ export const ToolsList: React.FC = ({ }) => ( - Available Qwen Code CLI tools: + {t('Available Qwen Code CLI tools:')} {tools.length > 0 ? ( @@ -46,7 +47,7 @@ export const ToolsList: React.FC = ({ )) ) : ( - No tools available + {t('No tools available')} )} ); diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts index 57b83c68..8fa878b3 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; +import { t } from '../../i18n/index.js'; export const WITTY_LOADING_PHRASES = [ "I'm Feeling Lucky", @@ -151,10 +152,14 @@ export const usePhraseCycler = ( isWaiting: boolean, customPhrases?: string[], ) => { - const loadingPhrases = - customPhrases && customPhrases.length > 0 - ? customPhrases - : WITTY_LOADING_PHRASES; + // Translate all phrases at once if using default phrases + const loadingPhrases = useMemo( + () => + customPhrases && customPhrases.length > 0 + ? customPhrases + : WITTY_LOADING_PHRASES.map((phrase) => t(phrase)), + [customPhrases], + ); const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState( loadingPhrases[0], @@ -163,7 +168,7 @@ export const usePhraseCycler = ( useEffect(() => { if (isWaiting) { - setCurrentLoadingPhrase('Waiting for user confirmation...'); + setCurrentLoadingPhrase(t('Waiting for user confirmation...')); if (phraseIntervalRef.current) { clearInterval(phraseIntervalRef.current); phraseIntervalRef.current = null; diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts index 9c534538..467ef313 100644 --- a/packages/cli/src/ui/hooks/useThemeCommand.ts +++ b/packages/cli/src/ui/hooks/useThemeCommand.ts @@ -9,6 +9,7 @@ import { themeManager } from '../themes/theme-manager.js'; import type { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting import { type HistoryItem, MessageType } from '../types.js'; import process from 'node:process'; +import { t } from '../../i18n/index.js'; interface UseThemeCommandReturn { isThemeDialogOpen: boolean; @@ -34,7 +35,9 @@ export const useThemeCommand = ( addItem( { type: MessageType.INFO, - text: 'Theme configuration unavailable due to NO_COLOR env variable.', + text: t( + 'Theme configuration unavailable due to NO_COLOR env variable.', + ), }, Date.now(), ); @@ -48,7 +51,11 @@ export const useThemeCommand = ( if (!themeManager.setActiveTheme(themeName)) { // If theme is not found, open the theme selection dialog and set error message setIsThemeDialogOpen(true); - setThemeError(`Theme "${themeName}" not found.`); + setThemeError( + t('Theme "{{themeName}}" not found.', { + themeName: themeName ?? '', + }), + ); } else { setThemeError(null); // Clear any previous theme error on success } @@ -75,7 +82,11 @@ export const useThemeCommand = ( const isBuiltIn = themeManager.findThemeByName(themeName); const isCustom = themeName && mergedCustomThemes[themeName]; if (!isBuiltIn && !isCustom) { - setThemeError(`Theme "${themeName}" not found in selected scope.`); + setThemeError( + t('Theme "{{themeName}}" not found in selected scope.', { + themeName: themeName ?? '', + }), + ); setIsThemeDialogOpen(true); return; } 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/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index af5367f7..d7a7f41e 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -30,6 +30,7 @@ import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { isKittyProtocolEnabled } from './kittyProtocolDetector.js'; import { VSCODE_SHIFT_ENTER_SEQUENCE } from './platformConstants.js'; +import { t } from '../../i18n/index.js'; const execAsync = promisify(exec); @@ -146,7 +147,10 @@ async function configureVSCodeStyle( if (!configDir) { return { success: false, - message: `Could not determine ${terminalName} config path on Windows: APPDATA environment variable is not set.`, + message: t( + 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.', + { terminalName }, + ), }; } @@ -166,9 +170,12 @@ async function configureVSCodeStyle( return { success: false, message: - `${terminalName} keybindings.json exists but is not a valid JSON array. ` + - `Please fix the file manually or delete it to allow automatic configuration.\n` + - `File: ${keybindingsFile}`, + t( + '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.', + { terminalName }, + ) + + '\n' + + t('File: {{file}}', { file: keybindingsFile }), }; } keybindings = parsedContent; @@ -176,10 +183,14 @@ async function configureVSCodeStyle( return { success: false, message: - `Failed to parse ${terminalName} keybindings.json. The file contains invalid JSON.\n` + - `Please fix the file manually or delete it to allow automatic configuration.\n` + - `File: ${keybindingsFile}\n` + - `Error: ${parseError}`, + t( + 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.', + { terminalName }, + ) + + '\n' + + t('File: {{file}}', { file: keybindingsFile }) + + '\n' + + t('Error: {{error}}', { error: String(parseError) }), }; } } catch { @@ -214,18 +225,23 @@ async function configureVSCodeStyle( if (existingShiftEnter || existingCtrlEnter) { const messages: string[] = []; if (existingShiftEnter) { - messages.push(`- Shift+Enter binding already exists`); + messages.push('- ' + t('Shift+Enter binding already exists')); } if (existingCtrlEnter) { - messages.push(`- Ctrl+Enter binding already exists`); + messages.push('- ' + t('Ctrl+Enter binding already exists')); } return { success: false, message: - `Existing keybindings detected. Will not modify to avoid conflicts.\n` + + t( + 'Existing keybindings detected. Will not modify to avoid conflicts.', + ) + + '\n' + messages.join('\n') + '\n' + - `Please check and modify manually if needed: ${keybindingsFile}`, + t('Please check and modify manually if needed: {{file}}', { + file: keybindingsFile, + }), }; } @@ -263,19 +279,34 @@ async function configureVSCodeStyle( await fs.writeFile(keybindingsFile, JSON.stringify(keybindings, null, 4)); return { success: true, - message: `Added Shift+Enter and Ctrl+Enter keybindings to ${terminalName}.\nModified: ${keybindingsFile}`, + message: + t( + 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.', + { + terminalName, + }, + ) + + '\n' + + t('Modified: {{file}}', { file: keybindingsFile }), requiresRestart: true, }; } else { return { success: true, - message: `${terminalName} keybindings already configured.`, + message: t('{{terminalName}} keybindings already configured.', { + terminalName, + }), }; } } catch (error) { return { success: false, - message: `Failed to configure ${terminalName}.\nFile: ${keybindingsFile}\nError: ${error}`, + message: + t('Failed to configure {{terminalName}}.', { terminalName }) + + '\n' + + t('File: {{file}}', { file: keybindingsFile }) + + '\n' + + t('Error: {{error}}', { error: String(error) }), }; } } @@ -322,8 +353,9 @@ export async function terminalSetup(): Promise { if (isKittyProtocolEnabled()) { return { success: true, - message: + message: t( 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).', + ), }; } @@ -332,8 +364,9 @@ export async function terminalSetup(): Promise { if (!terminal) { return { success: false, - message: - 'Could not detect terminal type. Supported terminals: VS Code, Cursor, and Windsurf.', + message: t( + 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.', + ), }; } @@ -349,7 +382,9 @@ export async function terminalSetup(): Promise { default: return { success: false, - message: `Terminal "${terminal}" is not supported yet.`, + message: t('Terminal "{{terminal}}" is not supported yet.', { + terminal, + }), }; } } diff --git a/packages/cli/src/utils/settingsUtils.ts b/packages/cli/src/utils/settingsUtils.ts index a9a42937..dcc3f2b6 100644 --- a/packages/cli/src/utils/settingsUtils.ts +++ b/packages/cli/src/utils/settingsUtils.ts @@ -16,6 +16,7 @@ import type { SettingsValue, } from '../config/settingsSchema.js'; import { getSettingsSchema } from '../config/settingsSchema.js'; +import { t } from '../i18n/index.js'; // The schema is now nested, but many parts of the UI and logic work better // with a flattened structure and dot-notation keys. This section flattens the @@ -446,7 +447,11 @@ export function getDisplayValue( if (definition?.type === 'enum' && definition.options) { const option = definition.options?.find((option) => option.value === value); - valueString = option?.label ?? `${value}`; + if (option?.label) { + valueString = t(option.label) || option.label; + } else { + valueString = `${value}`; + } } // Check if value is different from default OR if it's in modified settings OR if there are pending changes diff --git a/packages/cli/src/utils/systemInfoFields.ts b/packages/cli/src/utils/systemInfoFields.ts index d4b959fb..66308ac2 100644 --- a/packages/cli/src/utils/systemInfoFields.ts +++ b/packages/cli/src/utils/systemInfoFields.ts @@ -5,6 +5,7 @@ */ import type { ExtendedSystemInfo } from './systemInfo.js'; +import { t } from '../i18n/index.js'; /** * Field configuration for system information display @@ -23,59 +24,59 @@ export function getSystemInfoFields( ): SystemInfoField[] { const allFields: SystemInfoField[] = [ { - label: 'CLI Version', + label: t('CLI Version'), key: 'cliVersion', }, { - label: 'Git Commit', + label: t('Git Commit'), key: 'gitCommit', }, { - label: 'Model', + label: t('Model'), key: 'modelVersion', }, { - label: 'Sandbox', + label: t('Sandbox'), key: 'sandboxEnv', }, { - label: 'OS Platform', + label: t('OS Platform'), key: 'osPlatform', }, { - label: 'OS Arch', + label: t('OS Arch'), key: 'osArch', }, { - label: 'OS Release', + label: t('OS Release'), key: 'osRelease', }, { - label: 'Node.js Version', + label: t('Node.js Version'), key: 'nodeVersion', }, { - label: 'NPM Version', + label: t('NPM Version'), key: 'npmVersion', }, { - label: 'Session ID', + label: t('Session ID'), key: 'sessionId', }, { - label: 'Auth Method', + label: t('Auth Method'), key: 'selectedAuthType', }, { - label: 'Base URL', + label: t('Base URL'), key: 'baseUrl', }, { - label: 'Memory Usage', + label: t('Memory Usage'), key: 'memoryUsage', }, { - label: 'IDE Client', + label: t('IDE Client'), key: 'ideClient', }, ]; diff --git a/scripts/check-i18n.ts b/scripts/check-i18n.ts new file mode 100644 index 00000000..7c07619b --- /dev/null +++ b/scripts/check-i18n.ts @@ -0,0 +1,457 @@ +#!/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 { glob } from 'glob'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +// Get __dirname for ESM modules +// @ts-expect-error - import.meta is supported in NodeNext module system at runtime +const __dirname = dirname(fileURLToPath(import.meta.url)); + +interface CheckResult { + success: boolean; + errors: string[]; + warnings: string[]; + stats: { + totalKeys: number; + translatedKeys: number; + unusedKeys: string[]; + unusedKeysOnlyInLocales?: string[]; // 新增:只在 locales 中存在的未使用键 + }; +} + +/** + * 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); + try { + 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); + } + } + } + } catch { + // Skip files that can't be read + continue; + } + } + + 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[] { + return Array.from(allKeys) + .filter((key) => !usedKeys.has(key)) + .sort(); +} + +/** + * Save keys that exist only in locale files to a JSON file + * @param keysOnlyInLocales Array of keys that exist only in locale files + * @param outputPath Path to save the JSON file + */ +function saveKeysOnlyInLocalesToJson( + keysOnlyInLocales: string[], + outputPath: string, +): void { + try { + const data = { + generatedAt: new Date().toISOString(), + keys: keysOnlyInLocales, + count: keysOnlyInLocales.length, + }; + fs.writeFileSync(outputPath, JSON.stringify(data, null, 2)); + console.log(`Keys that exist only in locale files saved to: ${outputPath}`); + } catch (error) { + console.error(`Failed to save keys to JSON file: ${error}`); + } +} + +/** + * Check if unused keys exist only in locale files and nowhere else in the codebase + * Optimized to search all keys in a single pass instead of multiple grep calls + * @param unusedKeys The list of unused keys to check + * @param sourceDir The source directory to search in + * @param localesDir The locales directory to exclude from search + * @returns Array of keys that exist only in locale files + */ +async function findKeysOnlyInLocales( + unusedKeys: string[], + sourceDir: string, + localesDir: string, +): Promise { + if (unusedKeys.length === 0) { + return []; + } + + const keysOnlyInLocales: string[] = []; + const localesDirName = path.basename(localesDir); + + // Find all TypeScript/TSX files (excluding locales, node_modules, dist, and test files) + const files = await glob('**/*.{ts,tsx}', { + cwd: sourceDir, + ignore: [ + '**/node_modules/**', + '**/dist/**', + '**/*.test.ts', + '**/*.test.tsx', + `**/${localesDirName}/**`, + ], + }); + + // Read all files and check for key usage + const foundKeys = new Set(); + + for (const file of files) { + const filePath = path.join(sourceDir, file); + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Check each unused key in the file content + for (const key of unusedKeys) { + if (!foundKeys.has(key) && content.includes(key)) { + foundKeys.add(key); + } + } + } catch { + // Skip files that can't be read + continue; + } + } + + // Keys that were not found in any source files exist only in locales + for (const key of unusedKeys) { + if (!foundKeys.has(key)) { + keysOnlyInLocales.push(key); + } + } + + return keysOnlyInLocales; +} + +/** + * 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); + + // Find keys that exist only in locales (and nowhere else in the codebase) + const unusedKeysOnlyInLocales = + unusedKeys.length > 0 + ? await findKeysOnlyInLocales(unusedKeys, sourceDir, localesDir) + : []; + + 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, + unusedKeysOnlyInLocales, + }, + }; +} + +// Run checks +async function main() { + const result = await checkI18n(); + + console.log('\n=== i18n Check Results ===\n'); + + console.log(`Total keys: ${result.stats.totalKeys}`); + console.log(`Translated keys: ${result.stats.translatedKeys}`); + const coverage = + result.stats.totalKeys > 0 + ? ((result.stats.translatedKeys / result.stats.totalKeys) * 100).toFixed( + 1, + ) + : '0.0'; + console.log(`Translation coverage: ${coverage}%\n`); + + if (result.warnings.length > 0) { + console.log('⚠️ Warnings:'); + result.warnings.forEach((warning) => console.log(` - ${warning}`)); + + // Show unused keys + 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}"`)); + } + + // Show keys that exist only in locales files + if ( + result.stats.unusedKeysOnlyInLocales && + result.stats.unusedKeysOnlyInLocales.length > 0 + ) { + console.log( + '\n⚠️ The following keys exist ONLY in locale files and nowhere else in the codebase:', + ); + console.log( + ' Please review these keys - they might be safe to remove.', + ); + result.stats.unusedKeysOnlyInLocales.forEach((key) => + console.log(` - "${key}"`), + ); + + // Save these keys to a JSON file + const outputPath = path.join( + __dirname, + 'unused-keys-only-in-locales.json', + ); + saveKeysOnlyInLocalesToJson( + result.stats.unusedKeysOnlyInLocales, + outputPath, + ); + } + + 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); + } +} + +main().catch((error) => { + console.error('❌ Fatal error:', error); + process.exit(1); +}); diff --git a/scripts/copy_files.js b/scripts/copy_files.js index ddf25464..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,9 +40,18 @@ function copyFilesRecursive(source, target) { const targetPath = path.join(target, item.name); if (item.isDirectory()) { - copyFilesRecursive(sourcePath, targetPath); - } else if (extensionsToCopy.includes(path.extname(item.name))) { - fs.copyFileSync(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' && normalizedPath.startsWith('i18n/locales/'); + if (extensionsToCopy.includes(ext) || isLocaleJs) { + fs.copyFileSync(sourcePath, targetPath); + } } } } @@ -52,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()); diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js index 12268d61..534f104c 100644 --- a/scripts/prepare-package.js +++ b/scripts/prepare-package.js @@ -56,6 +56,43 @@ for (const file of filesToCopy) { } } +// Copy locales folder +console.log('Copying locales folder...'); +const localesSourceDir = path.join( + rootDir, + 'packages', + 'cli', + 'src', + 'i18n', + 'locales', +); +const localesDestDir = path.join(distDir, 'locales'); + +if (fs.existsSync(localesSourceDir)) { + // Recursive copy function + function copyRecursiveSync(src, dest) { + const stats = fs.statSync(src); + if (stats.isDirectory()) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + const entries = fs.readdirSync(src); + for (const entry of entries) { + const srcPath = path.join(src, entry); + const destPath = path.join(dest, entry); + copyRecursiveSync(srcPath, destPath); + } + } else { + fs.copyFileSync(src, dest); + } + } + + copyRecursiveSync(localesSourceDir, localesDestDir); + console.log('Copied locales folder'); +} else { + console.warn(`Warning: locales folder not found at ${localesSourceDir}`); +} + // Copy package.json from root and modify it for publishing console.log('Creating package.json for distribution...'); const rootPackageJson = JSON.parse( @@ -85,7 +122,7 @@ const distPackageJson = { bin: { qwen: 'cli.js', }, - files: ['cli.js', 'vendor', '*.sb', 'README.md', 'LICENSE'], + files: ['cli.js', 'vendor', '*.sb', 'README.md', 'LICENSE', 'locales'], config: rootPackageJson.config, dependencies: runtimeDependencies, optionalDependencies: {