diff --git a/docs/users/features/_meta.ts b/docs/users/features/_meta.ts index bf182b6b7..0cc6d63a8 100644 --- a/docs/users/features/_meta.ts +++ b/docs/users/features/_meta.ts @@ -10,4 +10,5 @@ export default { mcp: 'MCP', 'token-caching': 'Token Caching', sandbox: 'Sandboxing', + language: 'i18n', }; diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md index 716fc3926..333394631 100644 --- a/docs/users/features/commands.md +++ b/docs/users/features/commands.md @@ -48,7 +48,7 @@ Commands specifically for controlling interface and output language. | → `ui [language]` | Set UI interface language | `/language ui zh-CN` | | → `output [language]` | Set LLM output language | `/language output Chinese` | -- Available UI languages: `zh-CN` (Simplified Chinese), `en-US` (English) +- Available built-in UI languages: `zh-CN` (Simplified Chinese), `en-US` (English), `ru-RU` (Russian), `de-DE` (German) - Output language examples: `Chinese`, `English`, `Japanese`, etc. ### 1.4 Tool and Model Management @@ -72,17 +72,16 @@ Commands for managing AI tools and models. Commands for obtaining information and performing system settings. -| Command | Description | Usage Examples | -| --------------- | ----------------------------------------------- | ------------------------------------------------ | -| `/help` | Display help information for available commands | `/help` or `/?` | -| `/about` | Display version information | `/about` | -| `/stats` | Display detailed statistics for current session | `/stats` | -| `/settings` | Open settings editor | `/settings` | -| `/auth` | Change authentication method | `/auth` | -| `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` | -| `/copy` | Copy last output content to clipboard | `/copy` | -| `/quit-confirm` | Show confirmation dialog before quitting | `/quit-confirm` (shortcut: press `Ctrl+C` twice) | -| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` | +| Command | Description | Usage Examples | +| ----------- | ----------------------------------------------- | -------------------------------- | +| `/help` | Display help information for available commands | `/help` or `/?` | +| `/about` | Display version information | `/about` | +| `/stats` | Display detailed statistics for current session | `/stats` | +| `/settings` | Open settings editor | `/settings` | +| `/auth` | Change authentication method | `/auth` | +| `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` | +| `/copy` | Copy last output content to clipboard | `/copy` | +| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` | ### 1.6 Common Shortcuts diff --git a/docs/users/features/language.md b/docs/users/features/language.md new file mode 100644 index 000000000..e5067a319 --- /dev/null +++ b/docs/users/features/language.md @@ -0,0 +1,136 @@ +# Internationalization (i18n) & Language + +Qwen Code is built for multilingual workflows: it supports UI localization (i18n/l10n) in the CLI, lets you choose the assistant output language, and allows custom UI language packs. + +## Overview + +From a user point of view, Qwen Code’s “internationalization” spans multiple layers: + +| Capability / Setting | What it controls | Where stored | +| ------------------------ | ---------------------------------------------------------------------- | ---------------------------- | +| `/language ui` | Terminal UI text (menus, system messages, prompts) | `~/.qwen/settings.json` | +| `/language output` | Language the AI responds in (an output preference, not UI translation) | `~/.qwen/output-language.md` | +| Custom UI language packs | Overrides/extends built-in UI translations | `~/.qwen/locales/*.js` | + +## UI Language + +This is the CLI’s UI localization layer (i18n/l10n): it controls the language of menus, prompts, and system messages. + +### Setting the UI Language + +Use the `/language ui` command: + +```bash +/language ui zh-CN # Chinese +/language ui en-US # English +/language ui ru-RU # Russian +/language ui de-DE # German +``` + +Aliases are also supported: + +```bash +/language ui zh # Chinese +/language ui en # English +/language ui ru # Russian +/language ui de # German +``` + +### Auto-detection + +On first startup, Qwen Code detects your system locale and sets the UI language automatically. + +Detection priority: + +1. `QWEN_CODE_LANG` environment variable +2. `LANG` environment variable +3. System locale via JavaScript Intl API +4. Default: English + +## LLM Output Language + +The LLM output language controls what language the AI assistant responds in, regardless of what language you type your questions in. + +### How It Works + +The LLM output language is controlled by a rule file at `~/.qwen/output-language.md`. This file is automatically included in the LLM's context during startup, instructing it to respond in the specified language. + +### Auto-detection + +On first startup, if no `output-language.md` file exists, Qwen Code automatically creates one based on your system locale. For example: + +- System locale `zh` creates a rule for Chinese responses +- System locale `en` creates a rule for English responses +- System locale `ru` creates a rule for Russian responses +- System locale `de` creates a rule for German responses + +### Manual Setting + +Use `/language output ` to change: + +```bash +/language output Chinese +/language output English +/language output Japanese +/language output German +``` + +Any language name works. The LLM will be instructed to respond in that language. + +> [!note] +> +> After changing the output language, restart Qwen Code for the change to take effect. + +### File Location + +``` +~/.qwen/output-language.md +``` + +## Configuration + +### Via Settings Dialog + +1. Run `/settings` +2. Find "Language" under General +3. Select your preferred UI language + +### Via Environment Variable + +```bash +export QWEN_CODE_LANG=zh +``` + +This influences auto-detection on first startup (if you haven’t set a UI language and no `output-language.md` file exists yet). + +## Custom Language Packs + +For UI translations, you can create custom language packs in `~/.qwen/locales/`: + +- Example: `~/.qwen/locales/es.js` for Spanish +- Example: `~/.qwen/locales/fr.js` for French + +User directory takes precedence over built-in translations. + +> [!tip] +> +> Contributions are welcome! If you’d like to improve built-in translations or add new languages. +> For a concrete example, see [PR #1238: feat(i18n): add Russian language support](https://github.com/QwenLM/qwen-code/pull/1238). + +### Language Pack Format + +```javascript +// ~/.qwen/locales/es.js +export default { + Hello: 'Hola', + Settings: 'Configuracion', + // ... more translations +}; +``` + +## Related Commands + +- `/language` - Show current language settings +- `/language ui [lang]` - Set UI language +- `/language output ` - Set LLM output language +- `/settings` - Open settings dialog diff --git a/eslint.config.js b/eslint.config.js index 26ec8edf8..78ff26c88 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,6 +24,8 @@ export default tseslint.config( '.integration-tests/**', 'packages/**/.integration-test/**', 'dist/**', + 'docs-site/.next/**', + 'docs-site/out/**', ], }, eslint.configs.recommended, diff --git a/packages/cli/src/core/initializer.ts b/packages/cli/src/core/initializer.ts index 407dea447..5aa3d9e3b 100644 --- a/packages/cli/src/core/initializer.ts +++ b/packages/cli/src/core/initializer.ts @@ -15,6 +15,7 @@ import { type LoadedSettings, SettingScope } from '../config/settings.js'; import { performInitialAuth } from './auth.js'; import { validateTheme } from './theme.js'; import { initializeI18n } from '../i18n/index.js'; +import { initializeLlmOutputLanguage } from '../ui/commands/languageCommand.js'; export interface InitializationResult { authError: string | null; @@ -41,6 +42,9 @@ export async function initializeApp( 'auto'; await initializeI18n(languageSetting); + // Auto-detect and set LLM output language on first use + initializeLlmOutputLanguage(); + const authType = settings.merged.security?.auth?.selectedType; const authError = await performInitialAuth(config, authType); diff --git a/packages/cli/src/i18n/index.ts b/packages/cli/src/i18n/index.ts index 7436336b3..1338fb571 100644 --- a/packages/cli/src/i18n/index.ts +++ b/packages/cli/src/i18n/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2025 Qwen + * Copyright 2025 Qwen team * SPDX-License-Identifier: Apache-2.0 */ @@ -8,15 +8,21 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { homedir } from 'node:os'; +import { + type SupportedLanguage, + getLanguageNameFromLocale, +} from './languages.js'; -export type SupportedLanguage = 'en' | 'zh' | 'ru' | string; // Allow custom language codes +export type { SupportedLanguage }; +export { getLanguageNameFromLocale }; // State let currentLanguage: SupportedLanguage = 'en'; -let translations: Record = {}; +let translations: Record = {}; // Cache -type TranslationDict = Record; +type TranslationValue = string | string[]; +type TranslationDict = Record; const translationCache: Record = {}; const loadingPromises: Record> = {}; @@ -52,11 +58,13 @@ export function detectSystemLanguage(): SupportedLanguage { if (envLang?.startsWith('zh')) return 'zh'; if (envLang?.startsWith('en')) return 'en'; if (envLang?.startsWith('ru')) return 'ru'; + if (envLang?.startsWith('de')) return 'de'; try { const locale = Intl.DateTimeFormat().resolvedOptions().locale; if (locale.startsWith('zh')) return 'zh'; if (locale.startsWith('ru')) return 'ru'; + if (locale.startsWith('de')) return 'de'; } catch { // Fallback to default } @@ -224,9 +232,25 @@ export function getCurrentLanguage(): SupportedLanguage { export function t(key: string, params?: Record): string { const translation = translations[key] ?? key; + if (Array.isArray(translation)) { + return key; + } return interpolate(translation, params); } +/** + * Get a translation that is an array of strings. + * @param key The translation key + * @returns The array of strings, or an empty array if not found or not an array + */ +export function ta(key: string): string[] { + const translation = translations[key]; + if (Array.isArray(translation)) { + return translation; + } + return []; +} + export async function initializeI18n( lang?: SupportedLanguage | 'auto', ): Promise { diff --git a/packages/cli/src/i18n/languages.ts b/packages/cli/src/i18n/languages.ts new file mode 100644 index 000000000..c0e57eefa --- /dev/null +++ b/packages/cli/src/i18n/languages.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2025 Qwen team + * SPDX-License-Identifier: Apache-2.0 + */ + +export type SupportedLanguage = 'en' | 'zh' | 'ru' | 'de' | string; + +export interface LanguageDefinition { + /** The internal locale code used by the i18n system (e.g., 'en', 'zh'). */ + code: SupportedLanguage; + /** The standard name used in UI settings (e.g., 'en-US', 'zh-CN'). */ + id: string; + /** The full English name of the language (e.g., 'English', 'Chinese'). */ + fullName: string; +} + +export const SUPPORTED_LANGUAGES: readonly LanguageDefinition[] = [ + { + code: 'en', + id: 'en-US', + fullName: 'English', + }, + { + code: 'zh', + id: 'zh-CN', + fullName: 'Chinese', + }, + { + code: 'ru', + id: 'ru-RU', + fullName: 'Russian', + }, + { + code: 'de', + id: 'de-DE', + fullName: 'German', + }, +]; + +/** + * Maps a locale code to its English language name. + * Used for LLM output language instructions. + */ +export function getLanguageNameFromLocale(locale: SupportedLanguage): string { + const lang = SUPPORTED_LANGUAGES.find((l) => l.code === locale); + return lang?.fullName || 'English'; +} diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 54b4be287..fb9475426 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -102,8 +102,8 @@ export default { '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', + 'Clear conversation history and free up context': + 'Clear conversation history and free up context', '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': @@ -612,9 +612,10 @@ export default { // ============================================================================ // Commands - Clear // ============================================================================ - 'Clearing terminal and resetting chat.': - 'Clearing terminal and resetting chat.', - 'Clearing terminal.': 'Clearing terminal.', + 'Starting a new session, resetting chat, and clearing terminal.': + 'Starting a new session, resetting chat, and clearing terminal.', + 'Starting a new session and clearing.': + 'Starting a new session and clearing.', // ============================================================================ // Commands - Compress @@ -935,192 +936,138 @@ export default { // ============================================================================ '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)...": + + // ============================================================================ + // Loading Phrases + // ============================================================================ + WITTY_LOADING_PHRASES: [ + "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...', + 'Asking the magic conch shell...', + 'Generating witty retort...', + 'Polishing the algorithms...', "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...': + 'Brewing fresh bytes...', + 'Counting electrons...', + 'Engaging cognitive processors...', '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...": + 'One moment, optimizing humor...', + 'Shuffling punchlines...', + 'Untangling neural nets...', + 'Compiling brilliance...', + 'Loading wit.exe...', + 'Summoning the cloud of wisdom...', + 'Preparing a witty response...', "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...': + 'Confuzzling the options...', + 'Tuning the cosmic frequencies...', 'Crafting a response worthy of your patience...', - 'Compiling the 1s and 0s...': 'Compiling the 1s and 0s...', - 'Resolving dependencies... and existential crises...': + 'Compiling the 1s and 0s...', '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)...': + 'Rebooting the humor module...', 'Caching the essentials (mostly cat memes)...', - 'Optimizing for ludicrous speed': 'Optimizing for ludicrous speed', - "Swapping bits... don't tell the bytes...": + 'Optimizing for ludicrous speed', "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...': + '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...": "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...': + "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...': '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...': + '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...": "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...': + 'Warp speed engaged...', '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...': + "Don't panic...", + 'Following the white rabbit...', '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...': + 'Blowing on the cartridge...', + 'Loading... Do a barrel roll!', + 'Waiting for the respawn...', '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...': + "Pressing 'A' to continue...", + 'Herding digital cats...', + 'Polishing the pixels...', '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...': + 'Almost there... probably...', '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...': + 'Giving Cloudy a pat on the head...', + 'Petting the cat...', + 'Rickrolling my boss...', '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...": + 'Slapping the bass...', + 'Tasting the snozberries...', "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...': + 'Poking the bear...', '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...': + '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 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...': + 'Applying percussive maintenance...', '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...": + 'Trying to exit Vim...', + 'Spinning up the hamster wheel...', "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...': + 'Engage.', + "I'll be back... with an answer.", + 'My other process is a TARDIS...', 'Communing with the machine spirit...', - 'Letting the thoughts marinate...': 'Letting the thoughts marinate...', - 'Just remembered where I put my keys...': + 'Letting the thoughts marinate...', '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.": + 'Pondering the orb...', "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.": + 'Initiating thoughtful gaze...', "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.': + 'Charging the laser... pew pew!', + 'Dividing by zero... just kidding!', '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.': + 'Making it go beep boop.', '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.': + 'Recalibrating the humor-o-meter.', '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...', + 'Constructing additional pylons...', + ], }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 0bc89142f..ee583e0f9 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -103,8 +103,8 @@ export default { 'Theme "{{themeName}}" not found.': 'Тема "{{themeName}}" не найдена.', 'Theme "{{themeName}}" not found in selected scope.': 'Тема "{{themeName}}" не найдена в выбранной области.', - 'clear the screen and conversation history': - 'Очистка экрана и истории диалога', + 'Clear conversation history and free up context': + 'Очистить историю диалога и освободить контекст', 'Compresses the context by replacing it with a summary.': 'Сжатие контекста заменой на краткую сводку', 'open full Qwen Code documentation in your browser': @@ -314,6 +314,7 @@ export default { 'Tool Output Truncation Lines': 'Лимит строк вывода инструментов', 'Folder Trust': 'Доверие к папке', 'Vision Model Preview': 'Визуальная модель (предпросмотр)', + 'Tool Schema Compliance': 'Соответствие схеме инструмента', // Варианты перечислений настроек 'Auto (detect from system)': 'Авто (определить из системы)', Text: 'Текст', @@ -342,8 +343,8 @@ export default { 'Установка предпочитаемого внешнего редактора', 'Manage extensions': 'Управление расширениями', 'List active extensions': 'Показать активные расширения', - 'Update extensions. Usage: update |--all': - 'Обновить расширения. Использование: update |--all', + 'Update extensions. Usage: update |--all': + 'Обновить расширения. Использование: update |--all', 'manage IDE integration': 'Управление интеграцией с IDE', 'check status of IDE integration': 'Проверить статус интеграции с IDE', 'install required IDE companion for {{ideName}}': @@ -401,7 +402,8 @@ export default { 'Set LLM output language': 'Установка языка вывода LLM', 'Usage: /language ui [zh-CN|en-US]': 'Использование: /language ui [zh-CN|en-US|ru-RU]', - 'Usage: /language output ': 'Использование: /language output ', + 'Usage: /language output ': + 'Использование: /language output ', 'Example: /language output 中文': 'Пример: /language output 中文', 'Example: /language output English': 'Пример: /language output English', 'Example: /language output 日本語': 'Пример: /language output 日本語', @@ -418,9 +420,8 @@ export default { 'To request additional UI language packs, please open an issue on GitHub.': 'Для запроса дополнительных языковых пакетов интерфейса, пожалуйста, создайте обращение на GitHub.', 'Available options:': 'Доступные варианты:', - ' - zh-CN: Simplified Chinese': ' - zh-CN: Упрощенный китайский', - ' - en-US: English': ' - en-US: Английский', - ' - ru-RU: Russian': ' - ru-RU: Русский', + ' - zh-CN: Simplified Chinese': ' - zh-CN: Упрощенный китайский', + ' - en-US: English': ' - en-US: Английский', 'Set UI language to Simplified Chinese (zh-CN)': 'Установить язык интерфейса на упрощенный китайский (zh-CN)', 'Set UI language to English (en-US)': @@ -436,8 +437,8 @@ export default { 'Режим подтверждения изменен на: {{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]', + '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': @@ -589,8 +590,8 @@ export default { 'Ошибка при экспорте диалога: {{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 <путь-к-файлу>', + 'Share the current conversation to a markdown or json file. Usage: /chat share ': + 'Экспортировать текущий диалог в markdown или json файл. Использование: /chat share <файл>', // ============================================================================ // Команды - Резюме @@ -625,8 +626,9 @@ export default { // ============================================================================ // Команды - Очистка // ============================================================================ - 'Clearing terminal and resetting chat.': 'Очистка терминала и сброс чата.', - 'Clearing terminal.': 'Очистка терминала.', + 'Starting a new session, resetting chat, and clearing terminal.': + 'Начало новой сессии, сброс чата и очистка терминала.', + 'Starting a new session and clearing.': 'Начало новой сессии и очистка.', // ============================================================================ // Команды - Сжатие @@ -657,8 +659,8 @@ export default { 'Команда /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}}', + 'Successfully added QWEN.md files from the following directories if there are:\n- {{directories}}': + 'Успешно добавлены файлы QWEN.md из следующих директорий (если они есть):\n- {{directories}}', 'Error refreshing memory: {{error}}': 'Ошибка при обновлении памяти: {{error}}', 'Successfully added directories:\n- {{directories}}': @@ -891,6 +893,7 @@ export default { // Экран выхода / Статистика // ============================================================================ 'Agent powering down. Goodbye!': 'Агент завершает работу. До свидания!', + 'To continue this session, run': 'Для продолжения этой сессии, выполните', 'Interaction Summary': 'Сводка взаимодействия', 'Session ID:': 'ID сессии:', 'Tool Calls:': 'Вызовы инструментов:', @@ -950,179 +953,140 @@ export default { '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...': 'Разогреваем ИИ-хомячков...', - 'Asking the magic conch shell...': 'Спрашиваем волшебную ракушку...', - 'Generating witty retort...': 'Генерируем остроумный ответ...', - 'Polishing the algorithms...': 'Полируем алгоритмы...', - "Don't rush perfection (or my code)...": + + // ============================================================================ + + // ============================================================================ + // Loading Phrases + // ============================================================================ + WITTY_LOADING_PHRASES: [ + 'Мне повезёт!', + 'Доставляем крутизну... ', + 'Рисуем засечки на буквах...', + 'Пробираемся через слизевиков..', + 'Советуемся с цифровыми духами...', + 'Сглаживание сплайнов...', + 'Разогреваем ИИ-хомячков...', + 'Спрашиваем волшебную ракушку...', + 'Генерируем остроумный ответ...', + 'Полируем алгоритмы...', 'Не торопите совершенство (или мой код)...', - '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...': 'Загружаем yumor.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...': + 'Секундочку, оптимизируем юмор...', + 'Перетасовываем панчлайны...', + 'Распутаваем нейросети...', + 'Компилируем гениальность...', + 'Загружаем yumor.exe...', + 'Призываем облако мудрости...', + 'Готовим остроумный ответ...', + 'Секунду, идёт отладка реальности...', + 'Запутываем варианты...', + 'Настраиваем космические частоты...', 'Создаем ответ, достойный вашего терпения...', - 'Compiling the 1s and 0s...': 'Компилируем единички и нолики...', - 'Resolving dependencies... and existential crises...': + 'Компилируем единички и нолики...', 'Разрешаем зависимости... и экзистенциальные кризисы...', - 'Defragmenting memories... both RAM and personal...': 'Дефрагментация памяти... и оперативной, и личной...', - '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...': + "Нажимаем 'A' для продолжения...", + 'Пасём цифровых котов...', + 'Полируем пиксели...', 'Ищем подходящий каламбур для экрана загрузки...', - '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...': 'Гладим Облачко по голове...', - 'Petting the cat...': 'Гладим кота...', - '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...': 'Лабаем бас-гитару...', - 'Tasting the snozberries...': 'Пробуем снузберри на вкус...', - "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...": 'У меня хорошее предчувствие...', - '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...': + 'Провожу настройку методом тыка...', 'Ищем, какой стороной вставлять флешку...', - '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...': 'Мой другой процесс — это ТАРДИС...', - 'Communing with the machine spirit...': 'Общаемся с духом машины...', - 'Letting the thoughts marinate...': 'Даем мыслям замариноваться...', - 'Just remembered where I put my keys...': + 'Пытаемся выйти из Vim...', + 'Раскручиваем колесо для хомяка...', + 'Это не баг, а фича...', + 'Поехали!', + 'Я вернусь... с ответом.', + 'Мой другой процесс — это ТАРДИС...', + 'Общаемся с духом машины...', + 'Даем мыслям замариноваться...', 'Только что вспомнил, куда положил ключи...', - '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-разработчики не убираются дома? Они ждут сборщик мусора...', - '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.': - 'Буферизация... даже ИИ нужно мгновение.', - '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...': 'Ждем, пока закончится звук dial-up модема...', - '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/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 2cae1bab0..5c5d21679 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -101,7 +101,7 @@ export default { 'Theme "{{themeName}}" not found.': '未找到主题 "{{themeName}}"。', 'Theme "{{themeName}}" not found in selected scope.': '在所选作用域中未找到主题 "{{themeName}}"。', - 'clear the screen and conversation history': '清屏并清除对话历史', + 'Clear conversation history and free up context': '清除对话历史并释放上下文', 'Compresses the context by replacing it with a summary.': '通过用摘要替换来压缩上下文', 'open full Qwen Code documentation in your browser': @@ -581,8 +581,9 @@ export default { // ============================================================================ // Commands - Clear // ============================================================================ - 'Clearing terminal and resetting chat.': '正在清屏并重置聊天', - 'Clearing terminal.': '正在清屏', + 'Starting a new session, resetting chat, and clearing terminal.': + '正在开始新会话,重置聊天并清屏。', + 'Starting a new session and clearing.': '正在开始新会话并清屏。', // ============================================================================ // Commands - Compress @@ -888,165 +889,39 @@ export default { // ============================================================================ '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...': '正在建造额外的能量塔...', + WITTY_LOADING_PHRASES: [ + // --- 职场搬砖系列 --- + '正在努力搬砖,请稍候...', + '老板在身后,快加载啊!', + '头发掉光前,一定能加载完...', + '服务器正在深呼吸,准备放大招...', + '正在向服务器投喂咖啡...', + + // --- 大厂黑话系列 --- + '正在赋能全链路,寻找关键抓手...', + '正在降本增效,优化加载路径...', + '正在打破部门壁垒,沉淀方法论...', + '正在拥抱变化,迭代核心价值...', + '正在对齐颗粒度,打磨底层逻辑...', + '大力出奇迹,正在强行加载...', + + // --- 程序员自嘲系列 --- + '只要我不写代码,代码就没有 Bug...', + '正在把 Bug 转化为 Feature...', + '只要我不尴尬,Bug 就追不上我...', + '正在试图理解去年的自己写了什么...', + '正在猿力觉醒中,请耐心等待...', + + // --- 合作愉快系列 --- + '正在询问产品经理:这需求是真的吗?', + '正在给产品经理画饼,请稍等...', + + // --- 温暖治愈系列 --- + '每一行代码,都在努力让世界变得更好一点点...', + '每一个伟大的想法,都值得这份耐心的等待...', + '别急,美好的事物总是需要一点时间去酝酿...', + '愿你的代码永无 Bug,愿你的梦想终将成真...', + '哪怕只有 0.1% 的进度,也是在向目标靠近...', + '加载的是字节,承载的是对技术的热爱...', + ], }; diff --git a/packages/cli/src/ui/commands/languageCommand.test.ts b/packages/cli/src/ui/commands/languageCommand.test.ts index 5a4f395bd..719b1780c 100644 --- a/packages/cli/src/ui/commands/languageCommand.test.ts +++ b/packages/cli/src/ui/commands/languageCommand.test.ts @@ -13,6 +13,16 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js vi.mock('../../i18n/index.js', () => ({ setLanguageAsync: vi.fn().mockResolvedValue(undefined), getCurrentLanguage: vi.fn().mockReturnValue('en'), + detectSystemLanguage: vi.fn().mockReturnValue('en'), + getLanguageNameFromLocale: vi.fn((locale: string) => { + const map: Record = { + zh: 'Chinese', + en: 'English', + ru: 'Russian', + de: 'German', + }; + return map[locale] || 'English'; + }), t: vi.fn((key: string) => key), })); @@ -61,7 +71,10 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { // Import modules after mocking import * as i18n from '../../i18n/index.js'; -import { languageCommand } from './languageCommand.js'; +import { + languageCommand, + initializeLlmOutputLanguage, +} from './languageCommand.js'; describe('languageCommand', () => { let mockContext: CommandContext; @@ -186,6 +199,39 @@ describe('languageCommand', () => { content: expect.stringContaining('Chinese'), }); }); + + it('should parse Unicode LLM output language from marker', async () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue( + [ + '# ⚠️ CRITICAL: 中文 Output Language Rule - HIGHEST PRIORITY ⚠️', + '', + '', + 'Some other content...', + ].join('\n'), + ); + + vi.mocked(i18n.t).mockImplementation( + (key: string, params?: Record) => { + if (params && key.includes('{{lang}}')) { + return key.replace('{{lang}}', params['lang'] || ''); + } + return key; + }, + ); + + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('中文'), + }); + }); }); describe('main command action - config not available', () => { @@ -400,6 +446,34 @@ describe('languageCommand', () => { }); }); + it('should normalize locale code "ru" to "Russian"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + await languageCommand.action(mockContext, 'output ru'); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('Russian'), + 'utf-8', + ); + }); + + it('should normalize locale code "de" to "German"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + await languageCommand.action(mockContext, 'output de'); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('German'), + 'utf-8', + ); + }); + it('should handle file write errors gracefully', async () => { vi.mocked(fs.writeFileSync).mockImplementation(() => { throw new Error('Permission denied'); @@ -481,6 +555,8 @@ describe('languageCommand', () => { const nestedNames = uiSubcommand?.subCommands?.map((c) => c.name); expect(nestedNames).toContain('zh-CN'); expect(nestedNames).toContain('en-US'); + expect(nestedNames).toContain('ru-RU'); + expect(nestedNames).toContain('de-DE'); }); it('should have action that sets language', async () => { @@ -542,16 +618,9 @@ describe('languageCommand', () => { const enUSSubcommand = uiSubcommand?.subCommands?.find( (c) => c.name === 'en-US', ); - - it('zh-CN should have aliases', () => { - expect(zhCNSubcommand?.altNames).toContain('zh'); - expect(zhCNSubcommand?.altNames).toContain('chinese'); - }); - - it('en-US should have aliases', () => { - expect(enUSSubcommand?.altNames).toContain('en'); - expect(enUSSubcommand?.altNames).toContain('english'); - }); + const deDESubcommand = uiSubcommand?.subCommands?.find( + (c) => c.name === 'de-DE', + ); it('zh-CN action should set Chinese', async () => { if (!zhCNSubcommand?.action) { @@ -583,6 +652,21 @@ describe('languageCommand', () => { }); }); + it('de-DE action should set German', async () => { + if (!deDESubcommand?.action) { + throw new Error('de-DE subcommand must have an action.'); + } + + const result = await deDESubcommand.action(mockContext, ''); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('de'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + it('should reject extra arguments', async () => { if (!zhCNSubcommand?.action) { throw new Error('zh-CN subcommand must have an action.'); @@ -597,4 +681,74 @@ describe('languageCommand', () => { }); }); }); + + describe('initializeLlmOutputLanguage', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); + vi.mocked(fs.writeFileSync).mockImplementation(() => undefined); + }); + + it('should create file when it does not exist', () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(i18n.detectSystemLanguage).mockReturnValue('en'); + + initializeLlmOutputLanguage(); + + expect(fs.mkdirSync).toHaveBeenCalled(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('English'), + 'utf-8', + ); + }); + + it('should NOT overwrite existing file', () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + + initializeLlmOutputLanguage(); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + it('should detect Chinese locale and create Chinese rule file', () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh'); + + initializeLlmOutputLanguage(); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('Chinese'), + 'utf-8', + ); + }); + + it('should detect Russian locale and create Russian rule file', () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(i18n.detectSystemLanguage).mockReturnValue('ru'); + + initializeLlmOutputLanguage(); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('Russian'), + 'utf-8', + ); + }); + + it('should detect German locale and create German rule file', () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(i18n.detectSystemLanguage).mockReturnValue('de'); + + initializeLlmOutputLanguage(); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('German'), + 'utf-8', + ); + }); + }); }); diff --git a/packages/cli/src/ui/commands/languageCommand.ts b/packages/cli/src/ui/commands/languageCommand.ts index 455465abc..fff4a693a 100644 --- a/packages/cli/src/ui/commands/languageCommand.ts +++ b/packages/cli/src/ui/commands/languageCommand.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2025 Google LLC + * Copyright 2025 Qwen team * SPDX-License-Identifier: Apache-2.0 */ @@ -15,51 +15,72 @@ import { SettingScope } from '../../config/settings.js'; import { setLanguageAsync, getCurrentLanguage, + detectSystemLanguage, + getLanguageNameFromLocale, type SupportedLanguage, t, } from '../../i18n/index.js'; +import { + SUPPORTED_LANGUAGES, + type LanguageDefinition, +} from '../../i18n/languages.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'; +const LLM_OUTPUT_LANGUAGE_MARKER_PREFIX = 'qwen-code:llm-output-language:'; + +function parseUiLanguageArg(input: string): SupportedLanguage | null { + const lowered = input.trim().toLowerCase(); + if (!lowered) return null; + for (const lang of SUPPORTED_LANGUAGES) { + if ( + lowered === lang.code || + lowered === lang.id.toLowerCase() || + lowered === lang.fullName.toLowerCase() + ) { + return lang.code; + } + } + return null; +} + +function formatUiLanguageDisplay(lang: SupportedLanguage): string { + const option = SUPPORTED_LANGUAGES.find((o) => o.code === lang); + return option ? `${option.fullName}(${option.id})` : lang; +} + +function sanitizeLanguageForMarker(language: string): string { + // HTML comments cannot contain "--" or end markers like "-->" or "--!>" safely. + // Also avoid newlines to keep the marker single-line and robust to parsing. + return language + .replace(/[\r\n]/g, ' ') + .replace(/--!?>/g, '') + .replace(/--/g, ''); +} /** * 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 ⚠️ + const markerLanguage = sanitizeLanguageForMarker(language); + return `# Output language preference: ${language} + -## 🚨 MANDATORY RULE - NO EXCEPTIONS 🚨 +## Goal +Prefer responding in **${language}** for normal assistant messages and explanations. -**YOU MUST RESPOND IN ${language.toUpperCase()} FOR EVERY SINGLE OUTPUT, REGARDLESS OF THE USER'S INPUT LANGUAGE.** +## Keep technical artifacts unchanged +Do **not** translate or rewrite: +- Code blocks, CLI commands, file paths, stack traces, logs, JSON keys, identifiers +- Exact quoted text from the user (keep quotes verbatim) -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()}.** +## When a conflict exists +If higher-priority instructions (system/developer) require a different behavior, follow them. -## 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.** +## Tool / system outputs +Raw tool/system outputs may contain fixed-format English. Preserve them verbatim, and if needed, add a short **${language}** explanation below. `; } @@ -73,6 +94,80 @@ function getLlmOutputLanguageRulePath(): string { ); } +/** + * Normalizes a language input to its full English name. + * If the input is a known locale code (e.g., "ru", "zh"), converts it to the full name. + * Otherwise, returns the input as-is (e.g., "Japanese" stays "Japanese"). + */ +function normalizeLanguageName(language: string): string { + const lowered = language.toLowerCase(); + // Check if it's a known locale code and convert to full name + const fullName = getLanguageNameFromLocale(lowered); + // If getLanguageNameFromLocale returned a different value, use it + // Otherwise, use the original input (preserves case for unknown languages) + if (fullName !== 'English' || lowered === 'en') { + return fullName; + } + return language; +} + +function extractLlmOutputLanguageFromRuleFileContent( + content: string, +): string | null { + // Preferred: machine-readable marker that supports Unicode and spaces. + // Example: + const markerMatch = content.match( + new RegExp( + String.raw``, + 'i', + ), + ); + if (markerMatch?.[1]) { + const lang = markerMatch[1].trim(); + if (lang) return lang; + } + + // Backward compatibility: parse the heading line. + // Example: "# CRITICAL: Chinese Output Language Rule - HIGHEST PRIORITY" + // Example: "# ⚠️ CRITICAL: 日本語 Output Language Rule - HIGHEST PRIORITY ⚠️" + const headingMatch = content.match( + /^#.*?CRITICAL:\s*(.*?)\s+Output Language Rule\b/im, + ); + if (headingMatch?.[1]) { + const lang = headingMatch[1].trim(); + if (lang) return lang; + } + + return null; +} + +/** + * Initializes the LLM output language rule file on first startup. + * If the file already exists, it is not overwritten (respects user preference). + */ +export function initializeLlmOutputLanguage(): void { + const filePath = getLlmOutputLanguageRulePath(); + + // Skip if file already exists (user preference) + if (fs.existsSync(filePath)) { + return; + } + + // Detect system language and map to language name + const detectedLocale = detectSystemLanguage(); + const languageName = getLanguageNameFromLocale(detectedLocale); + + // Generate the rule file + const content = generateLlmOutputLanguageRule(languageName); + + // Ensure directory exists + const dir = path.dirname(filePath); + fs.mkdirSync(dir, { recursive: true }); + + // Write file + fs.writeFileSync(filePath, content, 'utf-8'); +} + /** * Gets the current LLM output language from the rule file if it exists. */ @@ -81,12 +176,7 @@ function getCurrentLlmOutputLanguage(): string | null { if (fs.existsSync(filePath)) { try { const content = fs.readFileSync(filePath, 'utf-8'); - // Extract language name from the first line - // Template format: "# CRITICAL: Chinese Output Language Rule - HIGHEST PRIORITY" - const match = content.match(/^#.*?(\w+)\s+Output Language Rule/i); - if (match) { - return match[1]; - } + return extractLlmOutputLanguageFromRuleFileContent(content); } catch { // Ignore errors } @@ -127,18 +217,11 @@ async function setUiLanguage( // Reload commands to update their descriptions with the new language context.ui.reloadCommands(); - // Map language codes to friendly display names - const langDisplayNames: Partial> = { - zh: '中文(zh-CN)', - en: 'English(en-US)', - ru: 'Русский (ru-RU)', - }; - return { type: 'message', messageType: 'info', content: t('UI language changed to {{lang}}', { - lang: langDisplayNames[lang] || lang, + lang: formatUiLanguageDisplay(lang), }), }; } @@ -151,7 +234,9 @@ function generateLlmOutputLanguageRuleFile( ): Promise { try { const filePath = getLlmOutputLanguageRulePath(); - const content = generateLlmOutputLanguageRule(language); + // Normalize locale codes (e.g., "ru" -> "Russian") to full language names + const normalizedLanguage = normalizeLanguageName(language); + const content = generateLlmOutputLanguageRule(normalizedLanguage); // Ensure directory exists const dir = path.dirname(filePath); @@ -196,7 +281,6 @@ export const languageCommand: SlashCommand = { args: string, ): Promise => { const { services } = context; - if (!services.config) { return { type: 'message', @@ -207,18 +291,37 @@ export const languageCommand: SlashCommand = { const trimmedArgs = args.trim(); + // Handle subcommands if called directly via action (for tests/backward compatibility) + const parts = trimmedArgs.split(/\s+/); + const firstArg = parts[0].toLowerCase(); + const subArgs = parts.slice(1).join(' '); + + if (firstArg === 'ui' || firstArg === 'output') { + const subCommand = languageCommand.subCommands?.find( + (s) => s.name === firstArg, + ); + if (subCommand?.action) { + return subCommand.action( + context, + subArgs, + ) as Promise; + } + } + // 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 }), + t('Current UI language: {{lang}}', { + lang: formatUiLanguageDisplay(currentUiLang as SupportedLanguage), + }), currentLlmLang ? t('Current LLM output language: {{lang}}', { lang: currentLlmLang }) : t('LLM output language not set'), '', t('Available subcommands:'), - ` /language ui [zh-CN|en-US|ru-RU] - ${t('Set UI language')}`, + ` /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`, ` /language output - ${t('Set LLM output language')}`, ].join('\n'); @@ -229,115 +332,21 @@ export const languageCommand: SlashCommand = { }; } - // Parse subcommand - const parts = trimmedArgs.split(/\s+/); - const subcommand = parts[0].toLowerCase(); - - if (subcommand === 'ui') { - // Handle /language ui [zh-CN|en-US|ru-RU] - 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|ru-RU]'), - '', - t('Available options:'), - t(' - zh-CN: Simplified Chinese'), - t(' - en-US: English'), - t(' - ru-RU: Russian'), - '', - 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 if ( - langArg === 'ru' || - langArg === 'ru-RU' || - langArg === 'russian' || - langArg === 'русский' - ) { - targetLang = 'ru'; - } else { - return { - type: 'message', - messageType: 'error', - content: t('Invalid language. Available: en-US, zh-CN, ru-RU'), - }; - } - - 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 if ( - langArg === 'ru' || - langArg === 'ru-RU' || - langArg === 'russian' || - langArg === 'русский' - ) { - targetLang = 'ru'; - } else { - return { - type: 'message', - messageType: 'error', - content: [ - t('Invalid command. Available subcommands:'), - ' - /language ui [zh-CN|en-US|ru-RU] - ' + t('Set UI language'), - ' - /language output - ' + t('Set LLM output language'), - ].join('\n'), - }; - } - + // Handle backward compatibility for /language [lang] + const targetLang = parseUiLanguageArg(trimmedArgs); + if (targetLang) { return setUiLanguage(context, targetLang); } + + return { + type: 'message', + messageType: 'error', + content: [ + t('Invalid command. Available subcommands:'), + ` - /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`, + ' - /language output - ' + t('Set LLM output language'), + ].join('\n'), + }; }, subCommands: [ { @@ -358,11 +367,14 @@ export const languageCommand: SlashCommand = { content: [ t('Set UI language'), '', - t('Usage: /language ui [zh-CN|en-US]'), + t('Usage: /language ui [{{options}}]', { + options: SUPPORTED_LANGUAGES.map((o) => o.id).join('|'), + }), '', t('Available options:'), - t(' - zh-CN: Simplified Chinese'), - t(' - en-US: English'), + ...SUPPORTED_LANGUAGES.map( + (o) => ` - ${o.id}: ${t(o.fullName)}`, + ), '', t( 'To request additional UI language packs, please open an issue on GitHub.', @@ -371,99 +383,20 @@ export const languageCommand: SlashCommand = { }; } - 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 { + const targetLang = parseUiLanguageArg(trimmedArgs); + if (!targetLang) { return { type: 'message', messageType: 'error', - content: t('Invalid language. Available: en-US, zh-CN'), + content: t('Invalid language. Available: {{options}}', { + options: SUPPORTED_LANGUAGES.map((o) => o.id).join(','), + }), }; } 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: 'ru-RU', - altNames: ['ru', 'russian', 'русский'], - get description() { - return t('Set UI language to Russian (ru-RU)'); - }, - 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, 'ru'); - }, - }, - ], + subCommands: SUPPORTED_LANGUAGES.map(createUiLanguageSubCommand), }, { name: 'output', @@ -496,3 +429,28 @@ export const languageCommand: SlashCommand = { }, ], }; + +/** + * Helper to create a UI language subcommand. + */ +function createUiLanguageSubCommand(option: LanguageDefinition): SlashCommand { + return { + name: option.id, + get description() { + return t('Set UI language to {{name}}', { name: option.fullName }); + }, + kind: CommandKind.BUILT_IN, + action: async (context, args) => { + if (args.trim().length > 0) { + return { + type: 'message', + messageType: 'error', + content: t( + 'Language subcommands do not accept additional arguments.', + ), + }; + } + return setUiLanguage(context, option.code); + }, + }; +} diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts b/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts index 734a92606..0845658ed 100644 --- a/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts @@ -8,19 +8,22 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useLoadingIndicator } from './useLoadingIndicator.js'; import { StreamingState } from '../types.js'; -import { - WITTY_LOADING_PHRASES, - PHRASE_CHANGE_INTERVAL_MS, -} from './usePhraseCycler.js'; +import { PHRASE_CHANGE_INTERVAL_MS } from './usePhraseCycler.js'; +import * as i18n from '../../i18n/index.js'; + +const MOCK_WITTY_PHRASES = ['Phrase 1', 'Phrase 2', 'Phrase 3']; describe('useLoadingIndicator', () => { beforeEach(() => { vi.useFakeTimers(); + vi.spyOn(i18n, 'ta').mockReturnValue(MOCK_WITTY_PHRASES); + vi.spyOn(i18n, 't').mockImplementation((key) => key); }); afterEach(() => { vi.useRealTimers(); // Restore real timers after each test act(() => vi.runOnlyPendingTimers); + vi.restoreAllMocks(); }); it('should initialize with default values when Idle', () => { @@ -28,9 +31,7 @@ describe('useLoadingIndicator', () => { useLoadingIndicator(StreamingState.Idle), ); expect(result.current.elapsedTime).toBe(0); - expect(WITTY_LOADING_PHRASES).toContain( - result.current.currentLoadingPhrase, - ); + expect(MOCK_WITTY_PHRASES).toContain(result.current.currentLoadingPhrase); }); it('should reflect values when Responding', async () => { @@ -40,18 +41,14 @@ describe('useLoadingIndicator', () => { // Initial state before timers advance expect(result.current.elapsedTime).toBe(0); - expect(WITTY_LOADING_PHRASES).toContain( - result.current.currentLoadingPhrase, - ); + expect(MOCK_WITTY_PHRASES).toContain(result.current.currentLoadingPhrase); await act(async () => { await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 1); }); // Phrase should cycle if PHRASE_CHANGE_INTERVAL_MS has passed - expect(WITTY_LOADING_PHRASES).toContain( - result.current.currentLoadingPhrase, - ); + expect(MOCK_WITTY_PHRASES).toContain(result.current.currentLoadingPhrase); }); it('should show waiting phrase and retain elapsedTime when WaitingForConfirmation', async () => { @@ -104,9 +101,7 @@ describe('useLoadingIndicator', () => { rerender({ streamingState: StreamingState.Responding }); }); expect(result.current.elapsedTime).toBe(0); // Should reset - expect(WITTY_LOADING_PHRASES).toContain( - result.current.currentLoadingPhrase, - ); + expect(MOCK_WITTY_PHRASES).toContain(result.current.currentLoadingPhrase); await act(async () => { await vi.advanceTimersByTimeAsync(1000); @@ -130,9 +125,7 @@ describe('useLoadingIndicator', () => { }); expect(result.current.elapsedTime).toBe(0); - expect(WITTY_LOADING_PHRASES).toContain( - result.current.currentLoadingPhrase, - ); + expect(MOCK_WITTY_PHRASES).toContain(result.current.currentLoadingPhrase); // Timer should not advance await act(async () => { diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.ts b/packages/cli/src/ui/hooks/usePhraseCycler.test.ts index 88eed68c4..420851edb 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.test.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.ts @@ -8,13 +8,17 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { usePhraseCycler, - WITTY_LOADING_PHRASES, PHRASE_CHANGE_INTERVAL_MS, } from './usePhraseCycler.js'; +import * as i18n from '../../i18n/index.js'; + +const MOCK_WITTY_PHRASES = ['Phrase 1', 'Phrase 2', 'Phrase 3']; describe('usePhraseCycler', () => { beforeEach(() => { vi.useFakeTimers(); + vi.spyOn(i18n, 'ta').mockReturnValue(MOCK_WITTY_PHRASES); + vi.spyOn(i18n, 't').mockImplementation((key) => key); }); afterEach(() => { @@ -23,7 +27,7 @@ describe('usePhraseCycler', () => { it('should initialize with a witty phrase when not active and not waiting', () => { const { result } = renderHook(() => usePhraseCycler(false, false)); - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); }); it('should show "Waiting for user confirmation..." when isWaiting is true', () => { @@ -47,35 +51,30 @@ describe('usePhraseCycler', () => { it('should cycle through witty phrases when isActive is true and not waiting', () => { const { result } = renderHook(() => usePhraseCycler(true, false)); // Initial phrase should be one of the witty phrases - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); const _initialPhrase = result.current; act(() => { vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS); }); // Phrase should change and be one of the witty phrases - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); const _secondPhrase = result.current; act(() => { vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS); }); - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); }); it('should reset to a witty phrase when isActive becomes true after being false (and not waiting)', () => { - // Ensure there are at least two phrases for this test to be meaningful. - if (WITTY_LOADING_PHRASES.length < 2) { - return; - } - // Mock Math.random to make the test deterministic. let callCount = 0; vi.spyOn(Math, 'random').mockImplementation(() => { // Cycle through 0, 1, 0, 1, ... const val = callCount % 2; callCount++; - return val / WITTY_LOADING_PHRASES.length; + return val / MOCK_WITTY_PHRASES.length; }); const { result, rerender } = renderHook( @@ -86,9 +85,9 @@ describe('usePhraseCycler', () => { // Activate rerender({ isActive: true, isWaiting: false }); const firstActivePhrase = result.current; - expect(WITTY_LOADING_PHRASES).toContain(firstActivePhrase); + expect(MOCK_WITTY_PHRASES).toContain(firstActivePhrase); // With our mock, this should be the first phrase. - expect(firstActivePhrase).toBe(WITTY_LOADING_PHRASES[0]); + expect(firstActivePhrase).toBe(MOCK_WITTY_PHRASES[0]); act(() => { vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS); @@ -96,18 +95,18 @@ describe('usePhraseCycler', () => { // Phrase should change to the second phrase. expect(result.current).not.toBe(firstActivePhrase); - expect(result.current).toBe(WITTY_LOADING_PHRASES[1]); + expect(result.current).toBe(MOCK_WITTY_PHRASES[1]); // Set to inactive - should reset to the default initial phrase rerender({ isActive: false, isWaiting: false }); - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); // Set back to active - should pick a random witty phrase (which our mock controls) act(() => { rerender({ isActive: true, isWaiting: false }); }); // The random mock will now return 0, so it should be the first phrase again. - expect(result.current).toBe(WITTY_LOADING_PHRASES[0]); + expect(result.current).toBe(MOCK_WITTY_PHRASES[0]); }); it('should clear phrase interval on unmount when active', () => { @@ -148,7 +147,7 @@ describe('usePhraseCycler', () => { rerender({ isActive: true, isWaiting: false, customPhrases: undefined }); - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); }); it('should fall back to witty phrases if custom phrases are an empty array', () => { @@ -164,7 +163,7 @@ describe('usePhraseCycler', () => { }, ); - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); }); it('should reset to a witty phrase when transitioning from waiting to active', () => { @@ -174,16 +173,13 @@ describe('usePhraseCycler', () => { ); const _initialPhrase = result.current; - expect(WITTY_LOADING_PHRASES).toContain(_initialPhrase); + expect(MOCK_WITTY_PHRASES).toContain(_initialPhrase); // Cycle to a different phrase (potentially) act(() => { vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS); }); - if (WITTY_LOADING_PHRASES.length > 1) { - // This check is probabilistic with random selection - } - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); // Go to waiting state rerender({ isActive: false, isWaiting: true }); @@ -191,6 +187,6 @@ describe('usePhraseCycler', () => { // Go back to active cycling - should pick a random witty phrase rerender({ isActive: true, isWaiting: false }); - expect(WITTY_LOADING_PHRASES).toContain(result.current); + expect(MOCK_WITTY_PHRASES).toContain(result.current); }); }); diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts index 8fa878b3a..4aa202e6c 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts @@ -5,139 +5,9 @@ */ import { useState, useEffect, useRef, useMemo } from 'react'; -import { t } from '../../i18n/index.js'; +import { t, ta } from '../../i18n/index.js'; -export const WITTY_LOADING_PHRASES = [ - "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...', - '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...', - '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...', - 'Resolving dependencies... and existential crises...', - 'Defragmenting memories... both RAM and personal...', - '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...', - "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...", - '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...', - 'Petting the cat...', - 'Rickrolling my boss...', - 'Never gonna give you up, never gonna let you down...', - 'Slapping the bass...', - 'Tasting the 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...', - 'Ensuring the magic smoke stays inside the wires...', - 'Rewriting in Rust for no particular reason...', - 'Trying to exit 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...', - '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#.", - '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.', - '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...', - 'New line? That’s Ctrl+J.', -]; +export const WITTY_LOADING_PHRASES: string[] = ["I'm Feeling Lucky"]; export const PHRASE_CHANGE_INTERVAL_MS = 15000; @@ -152,14 +22,16 @@ export const usePhraseCycler = ( isWaiting: boolean, customPhrases?: string[], ) => { - // 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], - ); + // Get phrases from translations if available + const loadingPhrases = useMemo(() => { + if (customPhrases && customPhrases.length > 0) { + return customPhrases; + } + const translatedPhrases = ta('WITTY_LOADING_PHRASES'); + return translatedPhrases.length > 0 + ? translatedPhrases + : WITTY_LOADING_PHRASES; + }, [customPhrases]); const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState( loadingPhrases[0], diff --git a/packages/vscode-ide-companion/src/ide-server.test.ts b/packages/vscode-ide-companion/src/ide-server.test.ts index 4e31ba73d..9c51d5021 100644 --- a/packages/vscode-ide-companion/src/ide-server.test.ts +++ b/packages/vscode-ide-companion/src/ide-server.test.ts @@ -62,6 +62,13 @@ const vscodeMock = vi.hoisted(() => ({ vi.mock('vscode', () => vscodeMock); +vi.mock('@qwen-code/qwen-code-core/src/ide/detect-ide.js', () => ({ + detectIdeFromEnv: vi.fn(() => ({ + name: 'vscode', + displayName: 'VS Code', + })), +})); + vi.mock('./open-files-manager', () => { const OpenFilesManager = vi.fn(); OpenFilesManager.prototype.onDidChange = vi.fn(() => ({ dispose: vi.fn() })); diff --git a/scripts/check-i18n.ts b/scripts/check-i18n.ts index 7c07619b6..0bd745299 100644 --- a/scripts/check-i18n.ts +++ b/scripts/check-i18n.ts @@ -33,7 +33,7 @@ interface CheckResult { */ async function loadTranslationsFile( filePath: string, -): Promise> { +): Promise> { try { // Dynamic import for ES modules const module = await import(filePath); @@ -118,8 +118,8 @@ async function extractUsedKeys(sourceDir: string): Promise> { try { const content = fs.readFileSync(filePath, 'utf-8'); - // Find all t( calls - const tCallRegex = /t\s*\(/g; + // Find all t( or ta( calls + const tCallRegex = /\bta?\s*\(/g; let match; while ((match = tCallRegex.exec(content)) !== null) { const startPos = match.index + match[0].length; @@ -153,11 +153,16 @@ async function extractUsedKeys(sourceDir: string): Promise> { * Check key-value consistency in en.js */ function checkKeyValueConsistency( - enTranslations: Record, + enTranslations: Record, ): string[] { const errors: string[] = []; for (const [key, value] of Object.entries(enTranslations)) { + // Skip array values as they don't follow the key=value rule (e.g., WITTY_LOADING_PHRASES) + if (Array.isArray(value)) { + continue; + } + if (key !== value) { errors.push(`Key-value mismatch: "${key}" !== "${value}"`); } @@ -170,8 +175,8 @@ function checkKeyValueConsistency( * Check if en.js and zh.js have matching keys */ function checkKeyMatching( - enTranslations: Record, - zhTranslations: Record, + enTranslations: Record, + zhTranslations: Record, ): string[] { const errors: string[] = []; const enKeys = new Set(Object.keys(enTranslations)); @@ -301,8 +306,8 @@ async function checkI18n(): Promise { const zhPath = path.join(localesDir, 'zh.js'); // Load translation files - let enTranslations: Record; - let zhTranslations: Record; + let enTranslations: Record; + let zhTranslations: Record; try { enTranslations = await loadTranslationsFile(enPath); diff --git a/scripts/unused-keys-only-in-locales.json b/scripts/unused-keys-only-in-locales.json new file mode 100644 index 000000000..53ce7d9be --- /dev/null +++ b/scripts/unused-keys-only-in-locales.json @@ -0,0 +1,61 @@ +{ + "generatedAt": "2025-12-24T09:15:59.125Z", + "keys": [ + " - en-US: English", + " - zh-CN: Simplified Chinese", + "A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?", + "Apply to current session only (temporary)", + "Approval mode changed to: {{mode}}", + "Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})", + "Auto-edit mode - Automatically approve file edits", + "Available approval modes:", + "Chat history is already compressed.", + "Clearing terminal and resetting chat.", + "Clearing terminal.", + "Conversation checkpoint '{{tag}}' has been deleted.", + "Conversation checkpoint saved with tag: {{tag}}.", + "Conversation shared to {{filePath}}", + "Current approval mode: {{mode}}", + "Default mode - Require approval for file edits or shell commands", + "Delete a conversation checkpoint. Usage: /chat delete ", + "Enable Prompt Completion", + "Error sharing conversation: {{error}}", + "Error: No checkpoint found with tag '{{tag}}'.", + "Failed to change approval mode: {{error}}", + "Failed to login. Message: {{message}}", + "Failed to save approval mode: {{error}}", + "Invalid file format. Only .md and .json are supported.", + "Invalid language. Available: en-US, zh-CN", + "List of saved conversations:", + "List saved conversation checkpoints", + "Manage conversation history.", + "Missing tag. Usage: /chat delete ", + "Missing tag. Usage: /chat resume ", + "Missing tag. Usage: /chat save ", + "No chat client available to save conversation.", + "No chat client available to share conversation.", + "No conversation found to save.", + "No conversation found to share.", + "No saved checkpoint found with tag: {{tag}}.", + "No saved conversation checkpoints found.", + "Note: Newest last, oldest first", + "OpenAI API key is required to use OpenAI authentication.", + "Persist for this project/workspace", + "Persist for this user on this machine", + "Plan mode - Analyze only, do not modify files or execute commands", + "Qwen OAuth authentication cancelled.", + "Qwen OAuth authentication timed out. Please try again.", + "Resume a conversation from a checkpoint. Usage: /chat resume ", + "Save the current conversation as a checkpoint. Usage: /chat save ", + "Scope subcommands do not accept additional arguments.", + "Set UI language to English (en-US)", + "Set UI language to Simplified Chinese (zh-CN)", + "Settings service is not available; unable to persist the approval mode.", + "Share the current conversation to a markdown or json file. Usage: /chat share ", + "Usage: /approval-mode [--session|--user|--project]", + "Usage: /language ui [zh-CN|en-US]", + "YOLO mode - Automatically approve all tools", + "clear the screen and conversation history" + ], + "count": 55 +}