feat(i18n): Add Internationalization Support for UI and LLM Output (#1058)

This commit is contained in:
pomelo
2025-11-21 15:44:37 +08:00
committed by GitHub
parent 640f30655d
commit 48b77541c3
98 changed files with 4740 additions and 636 deletions

View File

@@ -195,6 +195,16 @@ Slash commands provide meta-level control over the CLI itself.
- **`/init`** - **`/init`**
- **Description:** Analyzes the current directory and creates a `QWEN.md` context file by default (or the filename specified by `contextFileName`). If a non-empty file already exists, no changes are made. The command seeds an empty file and prompts the model to populate it with project-specific instructions. - **Description:** Analyzes the current directory and creates a `QWEN.md` context file by default (or the filename specified by `contextFileName`). If a non-empty file already exists, no changes are made. The command seeds an empty file and prompts the model to populate it with project-specific instructions.
- [**`/language`**](./language.md)
- **Description:** View or change the language setting for both UI and LLM output.
- **Sub-commands:**
- **`ui`**: Set the UI language (zh-CN or en-US)
- **`output`**: Set the LLM output language
- **Usage:** `/language [ui|output] [language]`
- **Examples:**
- `/language ui zh-CN` (set UI language to Simplified Chinese)
- `/language output English` (set LLM output language to English)
### Custom Commands ### Custom Commands
For a quick start, see the [example](#example-a-pure-function-refactoring-command) below. For a quick start, see the [example](#example-a-pure-function-refactoring-command) below.

71
docs/cli/language.md Normal file
View File

@@ -0,0 +1,71 @@
# Language Command
The `/language` command allows you to customize the language settings for both the Qwen Code user interface (UI) and the language model's output. This command supports two distinct functionalities:
1. Setting the UI language for the Qwen Code interface
2. Setting the output language for the language model (LLM)
## UI Language Settings
To change the UI language of Qwen Code, use the `ui` subcommand:
```
/language ui [zh-CN|en-US]
```
### Available UI Languages
- **zh-CN**: Simplified Chinese (简体中文)
- **en-US**: English
### Examples
```
/language ui zh-CN # Set UI language to Simplified Chinese
/language ui en-US # Set UI language to English
```
### UI Language Subcommands
You can also use direct subcommands for convenience:
- `/language ui zh-CN` or `/language ui zh` or `/language ui 中文`
- `/language ui en-US` or `/language ui en` or `/language ui english`
## LLM Output Language Settings
To set the language for the language model's responses, use the `output` subcommand:
```
/language output <language>
```
This command generates a language rule file that instructs the LLM to respond in the specified language. The rule file is saved to `~/.qwen/output-language.md`.
### Examples
```
/language output 中文 # Set LLM output language to Chinese
/language output English # Set LLM output language to English
/language output 日本語 # Set LLM output language to Japanese
```
## Viewing Current Settings
When used without arguments, the `/language` command displays the current language settings:
```
/language
```
This will show:
- Current UI language
- Current LLM output language (if set)
- Available subcommands
## Notes
- UI language changes take effect immediately and reload all command descriptions
- LLM output language settings are persisted in a rule file that is automatically included in the model's context
- To request additional UI language packs, please open an issue on GitHub

View File

@@ -46,6 +46,7 @@
"lint:all": "node scripts/lint.js", "lint:all": "node scripts/lint.js",
"format": "prettier --experimental-cli --write .", "format": "prettier --experimental-cli --write .",
"typecheck": "npm run typecheck --workspaces --if-present", "typecheck": "npm run typecheck --workspaces --if-present",
"check-i18n": "npm run check-i18n --workspace=packages/cli",
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci", "preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
"prepare": "husky && npm run bundle", "prepare": "husky && npm run bundle",
"prepare:package": "node scripts/prepare-package.js", "prepare:package": "node scripts/prepare-package.js",

View File

@@ -26,7 +26,8 @@
"format": "prettier --write .", "format": "prettier --write .",
"test": "vitest run", "test": "vitest run",
"test:ci": "vitest run", "test:ci": "vitest run",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"check-i18n": "tsx ../../scripts/check-i18n.ts"
}, },
"files": [ "files": [
"dist" "dist"

View File

@@ -23,6 +23,7 @@ import {
WriteFileTool, WriteFileTool,
resolveTelemetrySettings, resolveTelemetrySettings,
FatalConfigError, FatalConfigError,
Storage,
InputFormat, InputFormat,
OutputFormat, OutputFormat,
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
@@ -602,6 +603,20 @@ export async function loadCliConfig(
(e) => e.contextFiles, (e) => e.contextFiles,
); );
// Automatically load output-language.md if it exists
const outputLanguageFilePath = path.join(
Storage.getGlobalQwenDir(),
'output-language.md',
);
if (fs.existsSync(outputLanguageFilePath)) {
extensionContextFilePaths.push(outputLanguageFilePath);
if (debugMode) {
logger.debug(
`Found output-language.md, adding to context files: ${outputLanguageFilePath}`,
);
}
}
const fileService = new FileDiscoveryService(cwd); const fileService = new FileDiscoveryService(cwd);
const fileFiltering = { const fileFiltering = {

View File

@@ -176,6 +176,23 @@ const SETTINGS_SCHEMA = {
description: 'Enable debug logging of keystrokes to the console.', description: 'Enable debug logging of keystrokes to the console.',
showInDialog: true, showInDialog: true,
}, },
language: {
type: 'enum',
label: 'Language',
category: 'General',
requiresRestart: false,
default: 'auto',
description:
'The language for the user interface. Use "auto" to detect from system settings. ' +
'You can also use custom language codes (e.g., "es", "fr") by placing JS language files ' +
'in ~/.qwen/locales/ (e.g., ~/.qwen/locales/es.js).',
showInDialog: true,
options: [
{ value: 'auto', label: 'Auto (detect from system)' },
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文 (Chinese)' },
],
},
}, },
}, },
output: { output: {

View File

@@ -14,6 +14,7 @@ import {
import { type LoadedSettings, SettingScope } from '../config/settings.js'; import { type LoadedSettings, SettingScope } from '../config/settings.js';
import { performInitialAuth } from './auth.js'; import { performInitialAuth } from './auth.js';
import { validateTheme } from './theme.js'; import { validateTheme } from './theme.js';
import { initializeI18n } from '../i18n/index.js';
export interface InitializationResult { export interface InitializationResult {
authError: string | null; authError: string | null;
@@ -33,6 +34,13 @@ export async function initializeApp(
config: Config, config: Config,
settings: LoadedSettings, settings: LoadedSettings,
): Promise<InitializationResult> { ): Promise<InitializationResult> {
// Initialize i18n system
const languageSetting =
process.env['QWEN_CODE_LANG'] ||
settings.merged.general?.language ||
'auto';
await initializeI18n(languageSetting);
const authType = settings.merged.security?.auth?.selectedType; const authType = settings.merged.security?.auth?.selectedType;
const authError = await performInitialAuth(config, authType); const authError = await performInitialAuth(config, authType);
@@ -44,7 +52,6 @@ export async function initializeApp(
undefined, undefined,
); );
} }
const themeError = validateTheme(settings); const themeError = validateTheme(settings);
const shouldOpenAuthDialog = const shouldOpenAuthDialog =

View File

@@ -6,6 +6,7 @@
import { themeManager } from '../ui/themes/theme-manager.js'; import { themeManager } from '../ui/themes/theme-manager.js';
import { type LoadedSettings } from '../config/settings.js'; import { type LoadedSettings } from '../config/settings.js';
import { t } from '../i18n/index.js';
/** /**
* Validates the configured theme. * Validates the configured theme.
@@ -15,7 +16,9 @@ import { type LoadedSettings } from '../config/settings.js';
export function validateTheme(settings: LoadedSettings): string | null { export function validateTheme(settings: LoadedSettings): string | null {
const effectiveTheme = settings.merged.ui?.theme; const effectiveTheme = settings.merged.ui?.theme;
if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) { if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) {
return `Theme "${effectiveTheme}" not found.`; return t('Theme "{{themeName}}" not found.', {
themeName: effectiveTheme,
});
} }
return null; return null;
} }

View File

@@ -0,0 +1,232 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { homedir } from 'node:os';
export type SupportedLanguage = 'en' | 'zh' | string; // Allow custom language codes
// State
let currentLanguage: SupportedLanguage = 'en';
let translations: Record<string, string> = {};
// Cache
type TranslationDict = Record<string, string>;
const translationCache: Record<string, TranslationDict> = {};
const loadingPromises: Record<string, Promise<TranslationDict>> = {};
// Path helpers
const getBuiltinLocalesDir = (): string => {
const __filename = fileURLToPath(import.meta.url);
return path.join(path.dirname(__filename), 'locales');
};
const getUserLocalesDir = (): string =>
path.join(homedir(), '.qwen', 'locales');
/**
* Get the path to the user's custom locales directory.
* Users can place custom language packs (e.g., es.js, fr.js) in this directory.
* @returns The path to ~/.qwen/locales
*/
export function getUserLocalesDirectory(): string {
return getUserLocalesDir();
}
const getLocalePath = (
lang: SupportedLanguage,
useUserDir: boolean = false,
): string => {
const baseDir = useUserDir ? getUserLocalesDir() : getBuiltinLocalesDir();
return path.join(baseDir, `${lang}.js`);
};
// Language detection
export function detectSystemLanguage(): SupportedLanguage {
const envLang = process.env['QWEN_CODE_LANG'] || process.env['LANG'];
if (envLang?.startsWith('zh')) return 'zh';
if (envLang?.startsWith('en')) return 'en';
try {
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
if (locale.startsWith('zh')) return 'zh';
} catch {
// Fallback to default
}
return 'en';
}
// Translation loading
async function loadTranslationsAsync(
lang: SupportedLanguage,
): Promise<TranslationDict> {
if (translationCache[lang]) {
return translationCache[lang];
}
const existingPromise = loadingPromises[lang];
if (existingPromise) {
return existingPromise;
}
const loadPromise = (async () => {
// Try user directory first (for custom language packs), then builtin directory
const searchDirs = [
{ dir: getUserLocalesDir(), isUser: true },
{ dir: getBuiltinLocalesDir(), isUser: false },
];
for (const { dir, isUser } of searchDirs) {
// Ensure directory exists
if (!fs.existsSync(dir)) {
continue;
}
const jsPath = getLocalePath(lang, isUser);
if (!fs.existsSync(jsPath)) {
continue;
}
try {
// Convert file path to file:// URL for cross-platform compatibility
const fileUrl = pathToFileURL(jsPath).href;
try {
const module = await import(fileUrl);
const result = module.default || module;
if (
result &&
typeof result === 'object' &&
Object.keys(result).length > 0
) {
translationCache[lang] = result;
return result;
} else {
throw new Error('Module loaded but result is empty or invalid');
}
} catch {
// For builtin locales, try alternative import method (relative path)
if (!isUser) {
try {
const module = await import(`./locales/${lang}.js`);
const result = module.default || module;
if (
result &&
typeof result === 'object' &&
Object.keys(result).length > 0
) {
translationCache[lang] = result;
return result;
}
} catch {
// Continue to next directory
}
}
// If import failed, continue to next directory
continue;
}
} catch (error) {
// Log warning but continue to next directory
if (isUser) {
console.warn(
`Failed to load translations from user directory for ${lang}:`,
error,
);
} else {
console.warn(`Failed to load JS translations for ${lang}:`, error);
if (error instanceof Error) {
console.warn(`Error details: ${error.message}`);
console.warn(`Stack: ${error.stack}`);
}
}
// Continue to next directory
continue;
}
}
// Return empty object if both directories fail
// Cache it to avoid repeated failed attempts
translationCache[lang] = {};
return {};
})();
loadingPromises[lang] = loadPromise;
// Clean up promise after completion to allow retry on next call if needed
loadPromise.finally(() => {
delete loadingPromises[lang];
});
return loadPromise;
}
function loadTranslations(lang: SupportedLanguage): TranslationDict {
// Only return from cache (JS files require async loading)
return translationCache[lang] || {};
}
// String interpolation
function interpolate(
template: string,
params?: Record<string, string>,
): string {
if (!params) return template;
return template.replace(
/\{\{(\w+)\}\}/g,
(match, key) => params[key] ?? match,
);
}
// Language setting helpers
function resolveLanguage(lang: SupportedLanguage | 'auto'): SupportedLanguage {
return lang === 'auto' ? detectSystemLanguage() : lang;
}
// Public API
export function setLanguage(lang: SupportedLanguage | 'auto'): void {
const resolvedLang = resolveLanguage(lang);
currentLanguage = resolvedLang;
// Try to load translations synchronously (from cache only)
const loaded = loadTranslations(resolvedLang);
translations = loaded;
// Warn if translations are empty and JS file exists (requires async loading)
if (Object.keys(loaded).length === 0) {
const userJsPath = getLocalePath(resolvedLang, true);
const builtinJsPath = getLocalePath(resolvedLang, false);
if (fs.existsSync(userJsPath) || fs.existsSync(builtinJsPath)) {
console.warn(
`Language file for ${resolvedLang} requires async loading. ` +
`Use setLanguageAsync() instead, or call initializeI18n() first.`,
);
}
}
}
export async function setLanguageAsync(
lang: SupportedLanguage | 'auto',
): Promise<void> {
currentLanguage = resolveLanguage(lang);
translations = await loadTranslationsAsync(currentLanguage);
}
export function getCurrentLanguage(): SupportedLanguage {
return currentLanguage;
}
export function t(key: string, params?: Record<string, string>): string {
const translation = translations[key] ?? key;
return interpolate(translation, params);
}
export async function initializeI18n(
lang?: SupportedLanguage | 'auto',
): Promise<void> {
await setLanguageAsync(lang ?? 'auto');
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
import { helpCommand } from '../ui/commands/helpCommand.js'; import { helpCommand } from '../ui/commands/helpCommand.js';
import { ideCommand } from '../ui/commands/ideCommand.js'; import { ideCommand } from '../ui/commands/ideCommand.js';
import { initCommand } from '../ui/commands/initCommand.js'; import { initCommand } from '../ui/commands/initCommand.js';
import { languageCommand } from '../ui/commands/languageCommand.js';
import { mcpCommand } from '../ui/commands/mcpCommand.js'; import { mcpCommand } from '../ui/commands/mcpCommand.js';
import { memoryCommand } from '../ui/commands/memoryCommand.js'; import { memoryCommand } from '../ui/commands/memoryCommand.js';
import { modelCommand } from '../ui/commands/modelCommand.js'; import { modelCommand } from '../ui/commands/modelCommand.js';
@@ -72,6 +73,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
helpCommand, helpCommand,
await ideCommand(), await ideCommand(),
initCommand, initCommand,
languageCommand,
mcpCommand, mcpCommand,
memoryCommand, memoryCommand,
modelCommand, modelCommand,

View File

@@ -89,6 +89,7 @@ import { useGitBranchName } from './hooks/useGitBranchName.js';
import { useExtensionUpdates } from './hooks/useExtensionUpdates.js'; import { useExtensionUpdates } from './hooks/useExtensionUpdates.js';
import { ShellFocusContext } from './contexts/ShellFocusContext.js'; import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { useQuitConfirmation } from './hooks/useQuitConfirmation.js'; import { useQuitConfirmation } from './hooks/useQuitConfirmation.js';
import { t } from '../i18n/index.js';
import { useWelcomeBack } from './hooks/useWelcomeBack.js'; import { useWelcomeBack } from './hooks/useWelcomeBack.js';
import { useDialogClose } from './hooks/useDialogClose.js'; import { useDialogClose } from './hooks/useDialogClose.js';
import { useInitializationAuthError } from './hooks/useInitializationAuthError.js'; import { useInitializationAuthError } from './hooks/useInitializationAuthError.js';
@@ -384,7 +385,13 @@ export const AppContainer = (props: AppContainerProps) => {
settings.merged.security?.auth.selectedType settings.merged.security?.auth.selectedType
) { ) {
onAuthError( onAuthError(
`Authentication is enforced to be ${settings.merged.security?.auth.enforcedType}, but you are currently using ${settings.merged.security?.auth.selectedType}.`, t(
'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.',
{
enforcedType: settings.merged.security?.auth.enforcedType,
currentType: settings.merged.security?.auth.selectedType,
},
),
); );
} else if ( } else if (
settings.merged.security?.auth?.selectedType && settings.merged.security?.auth?.selectedType &&

View File

@@ -15,6 +15,7 @@ import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
import { useUIState } from '../contexts/UIStateContext.js'; import { useUIState } from '../contexts/UIStateContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js';
import { useSettings } from '../contexts/SettingsContext.js'; import { useSettings } from '../contexts/SettingsContext.js';
import { t } from '../../i18n/index.js';
function parseDefaultAuthType( function parseDefaultAuthType(
defaultAuthType: string | undefined, defaultAuthType: string | undefined,
@@ -39,10 +40,14 @@ export function AuthDialog(): React.JSX.Element {
const items = [ const items = [
{ {
key: AuthType.QWEN_OAUTH, key: AuthType.QWEN_OAUTH,
label: 'Qwen OAuth', label: t('Qwen OAuth'),
value: AuthType.QWEN_OAUTH, value: AuthType.QWEN_OAUTH,
}, },
{ key: AuthType.USE_OPENAI, label: 'OpenAI', value: AuthType.USE_OPENAI }, {
key: AuthType.USE_OPENAI,
label: t('OpenAI'),
value: AuthType.USE_OPENAI,
},
]; ];
const initialAuthIndex = Math.max( const initialAuthIndex = Math.max(
@@ -98,7 +103,9 @@ export function AuthDialog(): React.JSX.Element {
if (settings.merged.security?.auth?.selectedType === undefined) { if (settings.merged.security?.auth?.selectedType === undefined) {
// Prevent exiting if no auth method is set // Prevent exiting if no auth method is set
setErrorMessage( setErrorMessage(
'You must select an auth method to proceed. Press Ctrl+C again to exit.', t(
'You must select an auth method to proceed. Press Ctrl+C again to exit.',
),
); );
return; return;
} }
@@ -116,9 +123,9 @@ export function AuthDialog(): React.JSX.Element {
padding={1} padding={1}
width="100%" width="100%"
> >
<Text bold>Get started</Text> <Text bold>{t('Get started')}</Text>
<Box marginTop={1}> <Box marginTop={1}>
<Text>How would you like to authenticate for this project?</Text> <Text>{t('How would you like to authenticate for this project?')}</Text>
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>
<RadioButtonSelect <RadioButtonSelect
@@ -134,19 +141,19 @@ export function AuthDialog(): React.JSX.Element {
</Box> </Box>
)} )}
<Box marginTop={1}> <Box marginTop={1}>
<Text color={Colors.AccentPurple}>(Use Enter to Set Auth)</Text> <Text color={Colors.AccentPurple}>{t('(Use Enter to Set Auth)')}</Text>
</Box> </Box>
{hasApiKey && currentSelectedAuthType === AuthType.QWEN_OAUTH && ( {hasApiKey && currentSelectedAuthType === AuthType.QWEN_OAUTH && (
<Box marginTop={1}> <Box marginTop={1}>
<Text color={Colors.Gray}> <Text color={Colors.Gray}>
Note: Your existing API key in settings.json will not be cleared {t(
when using Qwen OAuth. You can switch back to OpenAI authentication 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.',
later if needed. )}
</Text> </Text>
</Box> </Box>
)} )}
<Box marginTop={1}> <Box marginTop={1}>
<Text>Terms of Services and Privacy Notice for Qwen Code</Text> <Text>{t('Terms of Services and Privacy Notice for Qwen Code')}</Text>
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>
<Text color={Colors.AccentBlue}> <Text color={Colors.AccentBlue}>

View File

@@ -10,6 +10,7 @@ import { Box, Text } from 'ink';
import Spinner from 'ink-spinner'; import Spinner from 'ink-spinner';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
interface AuthInProgressProps { interface AuthInProgressProps {
onTimeout: () => void; onTimeout: () => void;
@@ -48,13 +49,13 @@ export function AuthInProgress({
> >
{timedOut ? ( {timedOut ? (
<Text color={theme.status.error}> <Text color={theme.status.error}>
Authentication timed out. Please try again. {t('Authentication timed out. Please try again.')}
</Text> </Text>
) : ( ) : (
<Box> <Box>
<Text> <Text>
<Spinner type="dots" /> Waiting for auth... (Press ESC or CTRL+C to <Spinner type="dots" />{' '}
cancel) {t('Waiting for auth... (Press ESC or CTRL+C to cancel)')}
</Text> </Text>
</Box> </Box>
)} )}

View File

@@ -18,6 +18,7 @@ import type { OpenAICredentials } from '../components/OpenAIKeyPrompt.js';
import { useQwenAuth } from '../hooks/useQwenAuth.js'; import { useQwenAuth } from '../hooks/useQwenAuth.js';
import { AuthState, MessageType } from '../types.js'; import { AuthState, MessageType } from '../types.js';
import type { HistoryItem } from '../types.js'; import type { HistoryItem } from '../types.js';
import { t } from '../../i18n/index.js';
export type { QwenAuthState } from '../hooks/useQwenAuth.js'; export type { QwenAuthState } from '../hooks/useQwenAuth.js';
@@ -60,7 +61,9 @@ export const useAuthCommand = (
const handleAuthFailure = useCallback( const handleAuthFailure = useCallback(
(error: unknown) => { (error: unknown) => {
setIsAuthenticating(false); setIsAuthenticating(false);
const errorMessage = `Failed to authenticate. Message: ${getErrorMessage(error)}`; const errorMessage = t('Failed to authenticate. Message: {{message}}', {
message: getErrorMessage(error),
});
onAuthError(errorMessage); onAuthError(errorMessage);
// Log authentication failure // Log authentication failure
@@ -127,7 +130,9 @@ export const useAuthCommand = (
addItem( addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Authenticated successfully with ${authType} credentials.`, text: t('Authenticated successfully with {{authType}} credentials.', {
authType,
}),
}, },
Date.now(), Date.now(),
); );
@@ -225,7 +230,13 @@ export const useAuthCommand = (
) )
) { ) {
onAuthError( onAuthError(
`Invalid QWEN_DEFAULT_AUTH_TYPE value: "${defaultAuthType}". Valid values are: ${[AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].join(', ')}`, t(
'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}',
{
value: defaultAuthType,
validValues: [AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].join(', '),
},
),
); );
} }
}, [onAuthError]); }, [onAuthError]);

View File

@@ -8,10 +8,13 @@ import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { MessageType, type HistoryItemAbout } from '../types.js'; import { MessageType, type HistoryItemAbout } from '../types.js';
import { getExtendedSystemInfo } from '../../utils/systemInfo.js'; import { getExtendedSystemInfo } from '../../utils/systemInfo.js';
import { t } from '../../i18n/index.js';
export const aboutCommand: SlashCommand = { export const aboutCommand: SlashCommand = {
name: 'about', name: 'about',
description: 'show version info', get description() {
return t('show version info');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
const systemInfo = await getExtendedSystemInfo(context); const systemInfo = await getExtendedSystemInfo(context);

View File

@@ -9,15 +9,20 @@ import {
type SlashCommand, type SlashCommand,
type OpenDialogActionReturn, type OpenDialogActionReturn,
} from './types.js'; } from './types.js';
import { t } from '../../i18n/index.js';
export const agentsCommand: SlashCommand = { export const agentsCommand: SlashCommand = {
name: 'agents', name: 'agents',
description: 'Manage subagents for specialized task delegation.', get description() {
return t('Manage subagents for specialized task delegation.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [ subCommands: [
{ {
name: 'manage', name: 'manage',
description: 'Manage existing subagents (view, edit, delete).', get description() {
return t('Manage existing subagents (view, edit, delete).');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (): OpenDialogActionReturn => ({ action: (): OpenDialogActionReturn => ({
type: 'dialog', type: 'dialog',
@@ -26,7 +31,9 @@ export const agentsCommand: SlashCommand = {
}, },
{ {
name: 'create', name: 'create',
description: 'Create a new subagent with guided setup.', get description() {
return t('Create a new subagent with guided setup.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (): OpenDialogActionReturn => ({ action: (): OpenDialogActionReturn => ({
type: 'dialog', type: 'dialog',

View File

@@ -10,10 +10,13 @@ import type {
OpenDialogActionReturn, OpenDialogActionReturn,
} from './types.js'; } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const approvalModeCommand: SlashCommand = { export const approvalModeCommand: SlashCommand = {
name: 'approval-mode', name: 'approval-mode',
description: 'View or change the approval mode for tool usage', get description() {
return t('View or change the approval mode for tool usage');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async ( action: async (
_context: CommandContext, _context: CommandContext,

View File

@@ -6,10 +6,13 @@
import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import type { OpenDialogActionReturn, SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const authCommand: SlashCommand = { export const authCommand: SlashCommand = {
name: 'auth', name: 'auth',
description: 'change the auth method', get description() {
return t('change the auth method');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (_context, _args): OpenDialogActionReturn => ({ action: (_context, _args): OpenDialogActionReturn => ({
type: 'dialog', type: 'dialog',

View File

@@ -16,10 +16,13 @@ import {
getSystemInfoFields, getSystemInfoFields,
getFieldValue, getFieldValue,
} from '../../utils/systemInfoFields.js'; } from '../../utils/systemInfoFields.js';
import { t } from '../../i18n/index.js';
export const bugCommand: SlashCommand = { export const bugCommand: SlashCommand = {
name: 'bug', name: 'bug',
description: 'submit a bug report', get description() {
return t('submit a bug report');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context: CommandContext, args?: string): Promise<void> => { action: async (context: CommandContext, args?: string): Promise<void> => {
const bugDescription = (args || '').trim(); const bugDescription = (args || '').trim();

View File

@@ -7,7 +7,6 @@
import * as fsPromises from 'node:fs/promises'; import * as fsPromises from 'node:fs/promises';
import React from 'react'; import React from 'react';
import { Text } from 'ink'; import { Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { import type {
CommandContext, CommandContext,
SlashCommand, SlashCommand,
@@ -20,6 +19,7 @@ import path from 'node:path';
import type { HistoryItemWithoutId } from '../types.js'; import type { HistoryItemWithoutId } from '../types.js';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
import type { Content } from '@google/genai'; import type { Content } from '@google/genai';
import { t } from '../../i18n/index.js';
interface ChatDetail { interface ChatDetail {
name: string; name: string;
@@ -67,7 +67,9 @@ const getSavedChatTags = async (
const listCommand: SlashCommand = { const listCommand: SlashCommand = {
name: 'list', name: 'list',
description: 'List saved conversation checkpoints', get description() {
return t('List saved conversation checkpoints');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context): Promise<MessageActionReturn> => { action: async (context): Promise<MessageActionReturn> => {
const chatDetails = await getSavedChatTags(context, false); const chatDetails = await getSavedChatTags(context, false);
@@ -75,7 +77,7 @@ const listCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: 'No saved conversation checkpoints found.', content: t('No saved conversation checkpoints found.'),
}; };
} }
@@ -83,7 +85,7 @@ const listCommand: SlashCommand = {
...chatDetails.map((chat) => chat.name.length), ...chatDetails.map((chat) => chat.name.length),
); );
let message = 'List of saved conversations:\n\n'; let message = t('List of saved conversations:') + '\n\n';
for (const chat of chatDetails) { for (const chat of chatDetails) {
const paddedName = chat.name.padEnd(maxNameLength, ' '); const paddedName = chat.name.padEnd(maxNameLength, ' ');
const isoString = chat.mtime.toISOString(); const isoString = chat.mtime.toISOString();
@@ -91,7 +93,7 @@ const listCommand: SlashCommand = {
const formattedDate = match ? `${match[1]} ${match[2]}` : 'Invalid Date'; const formattedDate = match ? `${match[1]} ${match[2]}` : 'Invalid Date';
message += ` - ${paddedName} (saved on ${formattedDate})\n`; message += ` - ${paddedName} (saved on ${formattedDate})\n`;
} }
message += `\nNote: Newest last, oldest first`; message += `\n${t('Note: Newest last, oldest first')}`;
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
@@ -102,8 +104,11 @@ const listCommand: SlashCommand = {
const saveCommand: SlashCommand = { const saveCommand: SlashCommand = {
name: 'save', name: 'save',
description: get description() {
'Save the current conversation as a checkpoint. Usage: /chat save <tag>', return t(
'Save the current conversation as a checkpoint. Usage: /chat save <tag>',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context, args): Promise<SlashCommandActionReturn | void> => { action: async (context, args): Promise<SlashCommandActionReturn | void> => {
const tag = args.trim(); const tag = args.trim();
@@ -111,7 +116,7 @@ const saveCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Missing tag. Usage: /chat save <tag>', content: t('Missing tag. Usage: /chat save <tag>'),
}; };
} }
@@ -126,9 +131,12 @@ const saveCommand: SlashCommand = {
prompt: React.createElement( prompt: React.createElement(
Text, Text,
null, null,
'A checkpoint with the tag ', t(
React.createElement(Text, { color: theme.text.accent }, tag), 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?',
' already exists. Do you want to overwrite it?', {
tag,
},
),
), ),
originalInvocation: { originalInvocation: {
raw: context.invocation?.raw || `/chat save ${tag}`, raw: context.invocation?.raw || `/chat save ${tag}`,
@@ -142,7 +150,7 @@ const saveCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'No chat client available to save conversation.', content: t('No chat client available to save conversation.'),
}; };
} }
@@ -152,13 +160,15 @@ const saveCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: `Conversation checkpoint saved with tag: ${decodeTagName(tag)}.`, content: t('Conversation checkpoint saved with tag: {{tag}}.', {
tag: decodeTagName(tag),
}),
}; };
} else { } else {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: 'No conversation found to save.', content: t('No conversation found to save.'),
}; };
} }
}, },
@@ -167,8 +177,11 @@ const saveCommand: SlashCommand = {
const resumeCommand: SlashCommand = { const resumeCommand: SlashCommand = {
name: 'resume', name: 'resume',
altNames: ['load'], altNames: ['load'],
description: get description() {
'Resume a conversation from a checkpoint. Usage: /chat resume <tag>', return t(
'Resume a conversation from a checkpoint. Usage: /chat resume <tag>',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context, args) => { action: async (context, args) => {
const tag = args.trim(); const tag = args.trim();
@@ -176,7 +189,7 @@ const resumeCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Missing tag. Usage: /chat resume <tag>', content: t('Missing tag. Usage: /chat resume <tag>'),
}; };
} }
@@ -188,7 +201,9 @@ const resumeCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: `No saved checkpoint found with tag: ${decodeTagName(tag)}.`, content: t('No saved checkpoint found with tag: {{tag}}.', {
tag: decodeTagName(tag),
}),
}; };
} }
@@ -237,7 +252,9 @@ const resumeCommand: SlashCommand = {
const deleteCommand: SlashCommand = { const deleteCommand: SlashCommand = {
name: 'delete', name: 'delete',
description: 'Delete a conversation checkpoint. Usage: /chat delete <tag>', get description() {
return t('Delete a conversation checkpoint. Usage: /chat delete <tag>');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context, args): Promise<MessageActionReturn> => { action: async (context, args): Promise<MessageActionReturn> => {
const tag = args.trim(); const tag = args.trim();
@@ -245,7 +262,7 @@ const deleteCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Missing tag. Usage: /chat delete <tag>', content: t('Missing tag. Usage: /chat delete <tag>'),
}; };
} }
@@ -257,13 +274,17 @@ const deleteCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: `Conversation checkpoint '${decodeTagName(tag)}' has been deleted.`, content: t("Conversation checkpoint '{{tag}}' has been deleted.", {
tag: decodeTagName(tag),
}),
}; };
} else { } else {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: `Error: No checkpoint found with tag '${decodeTagName(tag)}'.`, content: t("Error: No checkpoint found with tag '{{tag}}'.", {
tag: decodeTagName(tag),
}),
}; };
} }
}, },
@@ -309,8 +330,11 @@ export function serializeHistoryToMarkdown(history: Content[]): string {
const shareCommand: SlashCommand = { const shareCommand: SlashCommand = {
name: 'share', name: 'share',
description: get description() {
'Share the current conversation to a markdown or json file. Usage: /chat share <file>', return t(
'Share the current conversation to a markdown or json file. Usage: /chat share <file>',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context, args): Promise<MessageActionReturn> => { action: async (context, args): Promise<MessageActionReturn> => {
let filePathArg = args.trim(); let filePathArg = args.trim();
@@ -324,7 +348,7 @@ const shareCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Invalid file format. Only .md and .json are supported.', content: t('Invalid file format. Only .md and .json are supported.'),
}; };
} }
@@ -333,7 +357,7 @@ const shareCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'No chat client available to share conversation.', content: t('No chat client available to share conversation.'),
}; };
} }
@@ -346,7 +370,7 @@ const shareCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: 'No conversation found to share.', content: t('No conversation found to share.'),
}; };
} }
@@ -362,14 +386,18 @@ const shareCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: `Conversation shared to ${filePath}`, content: t('Conversation shared to {{filePath}}', {
filePath,
}),
}; };
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: `Error sharing conversation: ${errorMessage}`, content: t('Error sharing conversation: {{error}}', {
error: errorMessage,
}),
}; };
} }
}, },
@@ -377,7 +405,9 @@ const shareCommand: SlashCommand = {
export const chatCommand: SlashCommand = { export const chatCommand: SlashCommand = {
name: 'chat', name: 'chat',
description: 'Manage conversation history.', get description() {
return t('Manage conversation history.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [ subCommands: [
listCommand, listCommand,

View File

@@ -7,21 +7,24 @@
import { uiTelemetryService } from '@qwen-code/qwen-code-core'; import { uiTelemetryService } from '@qwen-code/qwen-code-core';
import type { SlashCommand } from './types.js'; import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const clearCommand: SlashCommand = { export const clearCommand: SlashCommand = {
name: 'clear', name: 'clear',
description: 'clear the screen and conversation history', get description() {
return t('clear the screen and conversation history');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context, _args) => { action: async (context, _args) => {
const geminiClient = context.services.config?.getGeminiClient(); const geminiClient = context.services.config?.getGeminiClient();
if (geminiClient) { if (geminiClient) {
context.ui.setDebugMessage('Clearing terminal and resetting chat.'); context.ui.setDebugMessage(t('Clearing terminal and resetting chat.'));
// If resetChat fails, the exception will propagate and halt the command, // If resetChat fails, the exception will propagate and halt the command,
// which is the correct behavior to signal a failure to the user. // which is the correct behavior to signal a failure to the user.
await geminiClient.resetChat(); await geminiClient.resetChat();
} else { } else {
context.ui.setDebugMessage('Clearing terminal.'); context.ui.setDebugMessage(t('Clearing terminal.'));
} }
uiTelemetryService.setLastPromptTokenCount(0); uiTelemetryService.setLastPromptTokenCount(0);

View File

@@ -8,11 +8,14 @@ import type { HistoryItemCompression } from '../types.js';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
import type { SlashCommand } from './types.js'; import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const compressCommand: SlashCommand = { export const compressCommand: SlashCommand = {
name: 'compress', name: 'compress',
altNames: ['summarize'], altNames: ['summarize'],
description: 'Compresses the context by replacing it with a summary.', get description() {
return t('Compresses the context by replacing it with a summary.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
const { ui } = context; const { ui } = context;
@@ -20,7 +23,7 @@ export const compressCommand: SlashCommand = {
ui.addItem( ui.addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: 'Already compressing, wait for previous request to complete', text: t('Already compressing, wait for previous request to complete'),
}, },
Date.now(), Date.now(),
); );
@@ -60,7 +63,7 @@ export const compressCommand: SlashCommand = {
ui.addItem( ui.addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: 'Failed to compress chat history.', text: t('Failed to compress chat history.'),
}, },
Date.now(), Date.now(),
); );
@@ -69,9 +72,9 @@ export const compressCommand: SlashCommand = {
ui.addItem( ui.addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: `Failed to compress chat history: ${ text: t('Failed to compress chat history: {{error}}', {
e instanceof Error ? e.message : String(e) error: e instanceof Error ? e.message : String(e),
}`, }),
}, },
Date.now(), Date.now(),
); );

View File

@@ -7,10 +7,13 @@
import { copyToClipboard } from '../utils/commandUtils.js'; import { copyToClipboard } from '../utils/commandUtils.js';
import type { SlashCommand, SlashCommandActionReturn } from './types.js'; import type { SlashCommand, SlashCommandActionReturn } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const copyCommand: SlashCommand = { export const copyCommand: SlashCommand = {
name: 'copy', name: 'copy',
description: 'Copy the last result or code snippet to clipboard', get description() {
return t('Copy the last result or code snippet to clipboard');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context, _args): Promise<SlashCommandActionReturn | void> => { action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
const chat = await context.services.config?.getGeminiClient()?.getChat(); const chat = await context.services.config?.getGeminiClient()?.getChat();

View File

@@ -10,6 +10,7 @@ import { MessageType } from '../types.js';
import * as os from 'node:os'; import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import { loadServerHierarchicalMemory } from '@qwen-code/qwen-code-core'; import { loadServerHierarchicalMemory } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
export function expandHomeDir(p: string): string { export function expandHomeDir(p: string): string {
if (!p) { if (!p) {
@@ -27,13 +28,18 @@ export function expandHomeDir(p: string): string {
export const directoryCommand: SlashCommand = { export const directoryCommand: SlashCommand = {
name: 'directory', name: 'directory',
altNames: ['dir'], altNames: ['dir'],
description: 'Manage workspace directories', get description() {
return t('Manage workspace directories');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [ subCommands: [
{ {
name: 'add', name: 'add',
description: get description() {
'Add directories to the workspace. Use comma to separate multiple paths', return t(
'Add directories to the workspace. Use comma to separate multiple paths',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context: CommandContext, args: string) => { action: async (context: CommandContext, args: string) => {
const { const {
@@ -46,7 +52,7 @@ export const directoryCommand: SlashCommand = {
addItem( addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: 'Configuration is not available.', text: t('Configuration is not available.'),
}, },
Date.now(), Date.now(),
); );
@@ -63,7 +69,7 @@ export const directoryCommand: SlashCommand = {
addItem( addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: 'Please provide at least one path to add.', text: t('Please provide at least one path to add.'),
}, },
Date.now(), Date.now(),
); );
@@ -74,8 +80,9 @@ export const directoryCommand: SlashCommand = {
return { return {
type: 'message' as const, type: 'message' as const,
messageType: 'error' as const, messageType: 'error' as const,
content: content: t(
'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.', 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
),
}; };
} }
@@ -88,7 +95,12 @@ export const directoryCommand: SlashCommand = {
added.push(pathToAdd.trim()); added.push(pathToAdd.trim());
} catch (e) { } catch (e) {
const error = e as Error; const error = e as Error;
errors.push(`Error adding '${pathToAdd.trim()}': ${error.message}`); errors.push(
t("Error adding '{{path}}': {{error}}", {
path: pathToAdd.trim(),
error: error.message,
}),
);
} }
} }
@@ -117,12 +129,21 @@ export const directoryCommand: SlashCommand = {
addItem( addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Successfully added GEMINI.md files from the following directories if there are:\n- ${added.join('\n- ')}`, text: t(
'Successfully added GEMINI.md files from the following directories if there are:\n- {{directories}}',
{
directories: added.join('\n- '),
},
),
}, },
Date.now(), Date.now(),
); );
} catch (error) { } catch (error) {
errors.push(`Error refreshing memory: ${(error as Error).message}`); errors.push(
t('Error refreshing memory: {{error}}', {
error: (error as Error).message,
}),
);
} }
if (added.length > 0) { if (added.length > 0) {
@@ -133,7 +154,9 @@ export const directoryCommand: SlashCommand = {
addItem( addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Successfully added directories:\n- ${added.join('\n- ')}`, text: t('Successfully added directories:\n- {{directories}}', {
directories: added.join('\n- '),
}),
}, },
Date.now(), Date.now(),
); );
@@ -150,7 +173,9 @@ export const directoryCommand: SlashCommand = {
}, },
{ {
name: 'show', name: 'show',
description: 'Show all directories in the workspace', get description() {
return t('Show all directories in the workspace');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => { action: async (context: CommandContext) => {
const { const {
@@ -161,7 +186,7 @@ export const directoryCommand: SlashCommand = {
addItem( addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: 'Configuration is not available.', text: t('Configuration is not available.'),
}, },
Date.now(), Date.now(),
); );
@@ -173,7 +198,9 @@ export const directoryCommand: SlashCommand = {
addItem( addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Current workspace directories:\n${directoryList}`, text: t('Current workspace directories:\n{{directories}}', {
directories: directoryList,
}),
}, },
Date.now(), Date.now(),
); );

View File

@@ -12,19 +12,28 @@ import {
CommandKind, CommandKind,
} from './types.js'; } from './types.js';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
import { t, getCurrentLanguage } from '../../i18n/index.js';
export const docsCommand: SlashCommand = { export const docsCommand: SlashCommand = {
name: 'docs', name: 'docs',
description: 'open full Qwen Code documentation in your browser', get description() {
return t('open full Qwen Code documentation in your browser');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context: CommandContext): Promise<void> => { action: async (context: CommandContext): Promise<void> => {
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en'; const langPath = getCurrentLanguage()?.startsWith('zh') ? 'zh' : 'en';
const docsUrl = `https://qwenlm.github.io/qwen-code-docs/${langPath}`;
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') { if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`, text: t(
'Please open the following URL in your browser to view the documentation:\n{{url}}',
{
url: docsUrl,
},
),
}, },
Date.now(), Date.now(),
); );
@@ -32,7 +41,9 @@ export const docsCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Opening documentation in your browser: ${docsUrl}`, text: t('Opening documentation in your browser: {{url}}', {
url: docsUrl,
}),
}, },
Date.now(), Date.now(),
); );

View File

@@ -9,10 +9,13 @@ import {
type OpenDialogActionReturn, type OpenDialogActionReturn,
type SlashCommand, type SlashCommand,
} from './types.js'; } from './types.js';
import { t } from '../../i18n/index.js';
export const editorCommand: SlashCommand = { export const editorCommand: SlashCommand = {
name: 'editor', name: 'editor',
description: 'set external editor preference', get description() {
return t('set external editor preference');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (): OpenDialogActionReturn => ({ action: (): OpenDialogActionReturn => ({
type: 'dialog', type: 'dialog',

View File

@@ -19,6 +19,7 @@ import {
type SlashCommand, type SlashCommand,
CommandKind, CommandKind,
} from './types.js'; } from './types.js';
import { t } from '../../i18n/index.js';
async function listAction(context: CommandContext) { async function listAction(context: CommandContext) {
context.ui.addItem( context.ui.addItem(
@@ -131,14 +132,18 @@ async function updateAction(context: CommandContext, args: string) {
const listExtensionsCommand: SlashCommand = { const listExtensionsCommand: SlashCommand = {
name: 'list', name: 'list',
description: 'List active extensions', get description() {
return t('List active extensions');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: listAction, action: listAction,
}; };
const updateExtensionsCommand: SlashCommand = { const updateExtensionsCommand: SlashCommand = {
name: 'update', name: 'update',
description: 'Update extensions. Usage: update <extension-names>|--all', get description() {
return t('Update extensions. Usage: update <extension-names>|--all');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: updateAction, action: updateAction,
completion: async (context, partialArg) => { completion: async (context, partialArg) => {
@@ -158,7 +163,9 @@ const updateExtensionsCommand: SlashCommand = {
export const extensionsCommand: SlashCommand = { export const extensionsCommand: SlashCommand = {
name: 'extensions', name: 'extensions',
description: 'Manage extensions', get description() {
return t('Manage extensions');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [listExtensionsCommand, updateExtensionsCommand], subCommands: [listExtensionsCommand, updateExtensionsCommand],
action: (context, args) => action: (context, args) =>

View File

@@ -7,12 +7,15 @@
import type { SlashCommand } from './types.js'; import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { MessageType, type HistoryItemHelp } from '../types.js'; import { MessageType, type HistoryItemHelp } from '../types.js';
import { t } from '../../i18n/index.js';
export const helpCommand: SlashCommand = { export const helpCommand: SlashCommand = {
name: 'help', name: 'help',
altNames: ['?'], altNames: ['?'],
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
description: 'for help on Qwen Code', get description() {
return t('for help on Qwen Code');
},
action: async (context) => { action: async (context) => {
const helpItem: Omit<HistoryItemHelp, 'id'> = { const helpItem: Omit<HistoryItemHelp, 'id'> = {
type: MessageType.HELP, type: MessageType.HELP,

View File

@@ -26,6 +26,7 @@ import type {
} from './types.js'; } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { SettingScope } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js';
import { t } from '../../i18n/index.js';
function getIdeStatusMessage(ideClient: IdeClient): { function getIdeStatusMessage(ideClient: IdeClient): {
messageType: 'info' | 'error'; messageType: 'info' | 'error';
@@ -138,27 +139,35 @@ export const ideCommand = async (): Promise<SlashCommand> => {
if (!currentIDE) { if (!currentIDE) {
return { return {
name: 'ide', name: 'ide',
description: 'manage IDE integration', get description() {
return t('manage IDE integration');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (): SlashCommandActionReturn => action: (): SlashCommandActionReturn =>
({ ({
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: `IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.`, content: t(
'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.',
),
}) as const, }) as const,
}; };
} }
const ideSlashCommand: SlashCommand = { const ideSlashCommand: SlashCommand = {
name: 'ide', name: 'ide',
description: 'manage IDE integration', get description() {
return t('manage IDE integration');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [], subCommands: [],
}; };
const statusCommand: SlashCommand = { const statusCommand: SlashCommand = {
name: 'status', name: 'status',
description: 'check status of IDE integration', get description() {
return t('check status of IDE integration');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (): Promise<SlashCommandActionReturn> => { action: async (): Promise<SlashCommandActionReturn> => {
const { messageType, content } = const { messageType, content } =
@@ -173,7 +182,12 @@ export const ideCommand = async (): Promise<SlashCommand> => {
const installCommand: SlashCommand = { const installCommand: SlashCommand = {
name: 'install', name: 'install',
description: `install required IDE companion for ${ideClient.getDetectedIdeDisplayName()}`, get description() {
const ideName = ideClient.getDetectedIdeDisplayName() ?? 'IDE';
return t('install required IDE companion for {{ideName}}', {
ideName,
});
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
const installer = getIdeInstaller(currentIDE); const installer = getIdeInstaller(currentIDE);
@@ -246,7 +260,9 @@ export const ideCommand = async (): Promise<SlashCommand> => {
const enableCommand: SlashCommand = { const enableCommand: SlashCommand = {
name: 'enable', name: 'enable',
description: 'enable IDE integration', get description() {
return t('enable IDE integration');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => { action: async (context: CommandContext) => {
context.services.settings.setValue( context.services.settings.setValue(
@@ -268,7 +284,9 @@ export const ideCommand = async (): Promise<SlashCommand> => {
const disableCommand: SlashCommand = { const disableCommand: SlashCommand = {
name: 'disable', name: 'disable',
description: 'disable IDE integration', get description() {
return t('disable IDE integration');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => { action: async (context: CommandContext) => {
context.services.settings.setValue( context.services.settings.setValue(

View File

@@ -15,10 +15,13 @@ import { getCurrentGeminiMdFilename } from '@qwen-code/qwen-code-core';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { Text } from 'ink'; import { Text } from 'ink';
import React from 'react'; import React from 'react';
import { t } from '../../i18n/index.js';
export const initCommand: SlashCommand = { export const initCommand: SlashCommand = {
name: 'init', name: 'init',
description: 'Analyzes the project and creates a tailored QWEN.md file.', get description() {
return t('Analyzes the project and creates a tailored QWEN.md file.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async ( action: async (
context: CommandContext, context: CommandContext,
@@ -28,7 +31,7 @@ export const initCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Configuration not available.', content: t('Configuration not available.'),
}; };
} }
const targetDir = context.services.config.getTargetDir(); const targetDir = context.services.config.getTargetDir();

View File

@@ -0,0 +1,458 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {
SlashCommand,
CommandContext,
SlashCommandActionReturn,
MessageActionReturn,
} from './types.js';
import { CommandKind } from './types.js';
import { SettingScope } from '../../config/settings.js';
import {
setLanguageAsync,
getCurrentLanguage,
type SupportedLanguage,
t,
} from '../../i18n/index.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Storage } from '@qwen-code/qwen-code-core';
const LLM_OUTPUT_LANGUAGE_RULE_FILENAME = 'output-language.md';
/**
* Generates the LLM output language rule template based on the language name.
*/
function generateLlmOutputLanguageRule(language: string): string {
return `# ⚠️ CRITICAL: ${language} Output Language Rule - HIGHEST PRIORITY ⚠️
## 🚨 MANDATORY RULE - NO EXCEPTIONS 🚨
**YOU MUST RESPOND IN ${language.toUpperCase()} FOR EVERY SINGLE OUTPUT, REGARDLESS OF THE USER'S INPUT LANGUAGE.**
This is a **NON-NEGOTIABLE** requirement. Even if the user writes in English, says "hi", asks a simple question, or explicitly requests another language, **YOU MUST ALWAYS RESPOND IN ${language.toUpperCase()}.**
## What Must Be in ${language}
**EVERYTHING** you output: conversation replies, tool call descriptions, success/error messages, generated file content (comments, documentation), and all explanatory text.
**Tool outputs**: All descriptive text from \`read_file\`, \`write_file\`, \`codebase_search\`, \`run_terminal_cmd\`, \`todo_write\`, \`web_search\`, etc. MUST be in ${language}.
## Examples
### ✅ CORRECT:
- User says "hi" → Respond in ${language} (e.g., "Bonjour" if ${language} is French)
- Tool result → "已成功读取文件 config.json" (if ${language} is Chinese)
- Error → "无法找到指定的文件" (if ${language} is Chinese)
### ❌ WRONG:
- User says "hi" → "Hello" in English
- Tool result → "Successfully read file" in English
- Error → "File not found" in English
## Notes
- Code elements (variable/function names, syntax) can remain in English
- Comments, documentation, and all other text MUST be in ${language}
**THIS RULE IS ACTIVE NOW. ALL OUTPUTS MUST BE IN ${language.toUpperCase()}. NO EXCEPTIONS.**
`;
}
/**
* Gets the path to the LLM output language rule file.
*/
function getLlmOutputLanguageRulePath(): string {
return path.join(
Storage.getGlobalQwenDir(),
LLM_OUTPUT_LANGUAGE_RULE_FILENAME,
);
}
/**
* Gets the current LLM output language from the rule file if it exists.
*/
function getCurrentLlmOutputLanguage(): string | null {
const filePath = getLlmOutputLanguageRulePath();
if (fs.existsSync(filePath)) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
// Extract language name from the first line (e.g., "# Chinese Response Rules" -> "Chinese")
const match = content.match(/^#\s+(.+?)\s+Response Rules/i);
if (match) {
return match[1];
}
} catch {
// Ignore errors
}
}
return null;
}
/**
* Sets the UI language and persists it to settings.
*/
async function setUiLanguage(
context: CommandContext,
lang: SupportedLanguage,
): Promise<MessageActionReturn> {
const { services } = context;
const { settings } = services;
if (!services.config) {
return {
type: 'message',
messageType: 'error',
content: t('Configuration not available.'),
};
}
// Set language in i18n system (async to support JS translation files)
await setLanguageAsync(lang);
// Persist to settings (user scope)
if (settings && typeof settings.setValue === 'function') {
try {
settings.setValue(SettingScope.User, 'general.language', lang);
} catch (error) {
console.warn('Failed to save language setting:', error);
}
}
// Reload commands to update their descriptions with the new language
context.ui.reloadCommands();
// Map language codes to friendly display names
const langDisplayNames: Record<SupportedLanguage, string> = {
zh: '中文zh-CN',
en: 'Englishen-US',
};
return {
type: 'message',
messageType: 'info',
content: t('UI language changed to {{lang}}', {
lang: langDisplayNames[lang],
}),
};
}
/**
* Generates the LLM output language rule file.
*/
function generateLlmOutputLanguageRuleFile(
language: string,
): Promise<MessageActionReturn> {
try {
const filePath = getLlmOutputLanguageRulePath();
const content = generateLlmOutputLanguageRule(language);
// Ensure directory exists
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
// Write file (overwrite if exists)
fs.writeFileSync(filePath, content, 'utf-8');
return Promise.resolve({
type: 'message',
messageType: 'info',
content: [
t('LLM output language rule file generated at {{path}}', {
path: filePath,
}),
'',
t('Please restart the application for the changes to take effect.'),
].join('\n'),
});
} catch (error) {
return Promise.resolve({
type: 'message',
messageType: 'error',
content: t(
'Failed to generate LLM output language rule file: {{error}}',
{
error: error instanceof Error ? error.message : String(error),
},
),
});
}
}
export const languageCommand: SlashCommand = {
name: 'language',
get description() {
return t('View or change the language setting');
},
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn> => {
const { services } = context;
if (!services.config) {
return {
type: 'message',
messageType: 'error',
content: t('Configuration not available.'),
};
}
const trimmedArgs = args.trim();
// If no arguments, show current language settings and usage
if (!trimmedArgs) {
const currentUiLang = getCurrentLanguage();
const currentLlmLang = getCurrentLlmOutputLanguage();
const message = [
t('Current UI language: {{lang}}', { lang: currentUiLang }),
currentLlmLang
? t('Current LLM output language: {{lang}}', { lang: currentLlmLang })
: t('LLM output language not set'),
'',
t('Available subcommands:'),
` /language ui [zh-CN|en-US] - ${t('Set UI language')}`,
` /language output <language> - ${t('Set LLM output language')}`,
].join('\n');
return {
type: 'message',
messageType: 'info',
content: message,
};
}
// Parse subcommand
const parts = trimmedArgs.split(/\s+/);
const subcommand = parts[0].toLowerCase();
if (subcommand === 'ui') {
// Handle /language ui [zh-CN|en-US]
if (parts.length === 1) {
// Show UI language subcommand help
return {
type: 'message',
messageType: 'info',
content: [
t('Set UI language'),
'',
t('Usage: /language ui [zh-CN|en-US]'),
'',
t('Available options:'),
t(' - zh-CN: Simplified Chinese'),
t(' - en-US: English'),
'',
t(
'To request additional UI language packs, please open an issue on GitHub.',
),
].join('\n'),
};
}
const langArg = parts[1].toLowerCase();
let targetLang: SupportedLanguage | null = null;
if (langArg === 'en' || langArg === 'english' || langArg === 'en-us') {
targetLang = 'en';
} else if (
langArg === 'zh' ||
langArg === 'chinese' ||
langArg === '中文' ||
langArg === 'zh-cn'
) {
targetLang = 'zh';
} else {
return {
type: 'message',
messageType: 'error',
content: t('Invalid language. Available: en-US, zh-CN'),
};
}
return setUiLanguage(context, targetLang);
} else if (subcommand === 'output') {
// Handle /language output <language>
if (parts.length === 1) {
return {
type: 'message',
messageType: 'info',
content: [
t('Set LLM output language'),
'',
t('Usage: /language output <language>'),
` ${t('Example: /language output 中文')}`,
].join('\n'),
};
}
// Join all parts after "output" as the language name
const language = parts.slice(1).join(' ');
return generateLlmOutputLanguageRuleFile(language);
} else {
// Backward compatibility: treat as UI language
const langArg = trimmedArgs.toLowerCase();
let targetLang: SupportedLanguage | null = null;
if (langArg === 'en' || langArg === 'english' || langArg === 'en-us') {
targetLang = 'en';
} else if (
langArg === 'zh' ||
langArg === 'chinese' ||
langArg === '中文' ||
langArg === 'zh-cn'
) {
targetLang = 'zh';
} else {
return {
type: 'message',
messageType: 'error',
content: [
t('Invalid command. Available subcommands:'),
' - /language ui [zh-CN|en-US] - ' + t('Set UI language'),
' - /language output <language> - ' + t('Set LLM output language'),
].join('\n'),
};
}
return setUiLanguage(context, targetLang);
}
},
subCommands: [
{
name: 'ui',
get description() {
return t('Set UI language');
},
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
args: string,
): Promise<MessageActionReturn> => {
const trimmedArgs = args.trim();
if (!trimmedArgs) {
return {
type: 'message',
messageType: 'info',
content: [
t('Set UI language'),
'',
t('Usage: /language ui [zh-CN|en-US]'),
'',
t('Available options:'),
t(' - zh-CN: Simplified Chinese'),
t(' - en-US: English'),
'',
t(
'To request additional UI language packs, please open an issue on GitHub.',
),
].join('\n'),
};
}
const langArg = trimmedArgs.toLowerCase();
let targetLang: SupportedLanguage | null = null;
if (langArg === 'en' || langArg === 'english' || langArg === 'en-us') {
targetLang = 'en';
} else if (
langArg === 'zh' ||
langArg === 'chinese' ||
langArg === '中文' ||
langArg === 'zh-cn'
) {
targetLang = 'zh';
} else {
return {
type: 'message',
messageType: 'error',
content: t('Invalid language. Available: en-US, zh-CN'),
};
}
return setUiLanguage(context, targetLang);
},
subCommands: [
{
name: 'zh-CN',
altNames: ['zh', 'chinese', '中文'],
get description() {
return t('Set UI language to Simplified Chinese (zh-CN)');
},
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
args: string,
): Promise<MessageActionReturn> => {
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<MessageActionReturn> => {
if (args.trim().length > 0) {
return {
type: 'message',
messageType: 'error',
content: t(
'Language subcommands do not accept additional arguments.',
),
};
}
return setUiLanguage(context, 'en');
},
},
],
},
{
name: 'output',
get description() {
return t('Set LLM output language');
},
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
args: string,
): Promise<MessageActionReturn> => {
const trimmedArgs = args.trim();
if (!trimmedArgs) {
return {
type: 'message',
messageType: 'info',
content: [
t('Set LLM output language'),
'',
t('Usage: /language output <language>'),
` ${t('Example: /language output 中文')}`,
` ${t('Example: /language output English')}`,
` ${t('Example: /language output 日本語')}`,
].join('\n'),
};
}
return generateLlmOutputLanguageRuleFile(trimmedArgs);
},
},
],
};

View File

@@ -24,10 +24,13 @@ import {
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { appEvents, AppEvent } from '../../utils/events.js'; import { appEvents, AppEvent } from '../../utils/events.js';
import { MessageType, type HistoryItemMcpStatus } from '../types.js'; import { MessageType, type HistoryItemMcpStatus } from '../types.js';
import { t } from '../../i18n/index.js';
const authCommand: SlashCommand = { const authCommand: SlashCommand = {
name: 'auth', name: 'auth',
description: 'Authenticate with an OAuth-enabled MCP server', get description() {
return t('Authenticate with an OAuth-enabled MCP server');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async ( action: async (
context: CommandContext, context: CommandContext,
@@ -40,7 +43,7 @@ const authCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Config not loaded.', content: t('Config not loaded.'),
}; };
} }
@@ -56,14 +59,14 @@ const authCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: 'No MCP servers configured with OAuth authentication.', content: t('No MCP servers configured with OAuth authentication.'),
}; };
} }
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: `MCP servers with OAuth authentication:\n${oauthServers.map((s) => ` - ${s}`).join('\n')}\n\nUse /mcp auth <server-name> to authenticate.`, content: `${t('MCP servers with OAuth authentication:')}\n${oauthServers.map((s) => ` - ${s}`).join('\n')}\n\n${t('Use /mcp auth <server-name> to authenticate.')}`,
}; };
} }
@@ -72,7 +75,7 @@ const authCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: `MCP server '${serverName}' not found.`, content: t("MCP server '{{name}}' not found.", { name: serverName }),
}; };
} }
@@ -89,7 +92,12 @@ const authCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: 'info', type: 'info',
text: `Starting OAuth authentication for MCP server '${serverName}'...`, text: t(
"Starting OAuth authentication for MCP server '{{name}}'...",
{
name: serverName,
},
),
}, },
Date.now(), Date.now(),
); );
@@ -111,7 +119,12 @@ const authCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: 'info', type: 'info',
text: `✅ Successfully authenticated with MCP server '${serverName}'!`, text: t(
"Successfully authenticated and refreshed tools for '{{name}}'.",
{
name: serverName,
},
),
}, },
Date.now(), Date.now(),
); );
@@ -122,7 +135,9 @@ const authCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: 'info', type: 'info',
text: `Re-discovering tools from '${serverName}'...`, text: t("Re-discovering tools from '{{name}}'...", {
name: serverName,
}),
}, },
Date.now(), Date.now(),
); );
@@ -140,13 +155,24 @@ const authCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: `Successfully authenticated and refreshed tools for '${serverName}'.`, content: t(
"Successfully authenticated and refreshed tools for '{{name}}'.",
{
name: serverName,
},
),
}; };
} catch (error) { } catch (error) {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: `Failed to authenticate with MCP server '${serverName}': ${getErrorMessage(error)}`, content: t(
"Failed to authenticate with MCP server '{{name}}': {{error}}",
{
name: serverName,
error: getErrorMessage(error),
},
),
}; };
} finally { } finally {
appEvents.removeListener(AppEvent.OauthDisplayMessage, displayListener); appEvents.removeListener(AppEvent.OauthDisplayMessage, displayListener);
@@ -165,7 +191,9 @@ const authCommand: SlashCommand = {
const listCommand: SlashCommand = { const listCommand: SlashCommand = {
name: 'list', name: 'list',
description: 'List configured MCP servers and tools', get description() {
return t('List configured MCP servers and tools');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async ( action: async (
context: CommandContext, context: CommandContext,
@@ -176,7 +204,7 @@ const listCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Config not loaded.', content: t('Config not loaded.'),
}; };
} }
@@ -185,7 +213,7 @@ const listCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Could not retrieve tool registry.', content: t('Could not retrieve tool registry.'),
}; };
} }
@@ -276,7 +304,9 @@ const listCommand: SlashCommand = {
const refreshCommand: SlashCommand = { const refreshCommand: SlashCommand = {
name: 'refresh', name: 'refresh',
description: 'Restarts MCP servers.', get description() {
return t('Restarts MCP servers.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async ( action: async (
context: CommandContext, context: CommandContext,
@@ -286,7 +316,7 @@ const refreshCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Config not loaded.', content: t('Config not loaded.'),
}; };
} }
@@ -295,14 +325,14 @@ const refreshCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Could not retrieve tool registry.', content: t('Could not retrieve tool registry.'),
}; };
} }
context.ui.addItem( context.ui.addItem(
{ {
type: 'info', type: 'info',
text: 'Restarting MCP servers...', text: t('Restarting MCP servers...'),
}, },
Date.now(), Date.now(),
); );
@@ -324,8 +354,11 @@ const refreshCommand: SlashCommand = {
export const mcpCommand: SlashCommand = { export const mcpCommand: SlashCommand = {
name: 'mcp', name: 'mcp',
description: get description() {
'list configured MCP servers and tools, or authenticate with OAuth-enabled servers', return t(
'list configured MCP servers and tools, or authenticate with OAuth-enabled servers',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [listCommand, authCommand, refreshCommand], subCommands: [listCommand, authCommand, refreshCommand],
// Default action when no subcommand is provided // Default action when no subcommand is provided

View File

@@ -15,15 +15,20 @@ import fs from 'fs/promises';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
import type { SlashCommand, SlashCommandActionReturn } from './types.js'; import type { SlashCommand, SlashCommandActionReturn } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const memoryCommand: SlashCommand = { export const memoryCommand: SlashCommand = {
name: 'memory', name: 'memory',
description: 'Commands for interacting with memory.', get description() {
return t('Commands for interacting with memory.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
subCommands: [ subCommands: [
{ {
name: 'show', name: 'show',
description: 'Show the current memory contents.', get description() {
return t('Show the current memory contents.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
const memoryContent = context.services.config?.getUserMemory() || ''; const memoryContent = context.services.config?.getUserMemory() || '';
@@ -31,8 +36,8 @@ export const memoryCommand: SlashCommand = {
const messageContent = const messageContent =
memoryContent.length > 0 memoryContent.length > 0
? `Current memory content from ${fileCount} file(s):\n\n---\n${memoryContent}\n---` ? `${t('Current memory content from {{count}} file(s):', { count: String(fileCount) })}\n\n---\n${memoryContent}\n---`
: 'Memory is currently empty.'; : t('Memory is currently empty.');
context.ui.addItem( context.ui.addItem(
{ {
@@ -45,7 +50,9 @@ export const memoryCommand: SlashCommand = {
subCommands: [ subCommands: [
{ {
name: '--project', name: '--project',
description: 'Show project-level memory contents.', get description() {
return t('Show project-level memory contents.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
try { try {
@@ -57,8 +64,14 @@ export const memoryCommand: SlashCommand = {
const messageContent = const messageContent =
memoryContent.trim().length > 0 memoryContent.trim().length > 0
? `Project memory content from ${projectMemoryPath}:\n\n---\n${memoryContent}\n---` ? t(
: 'Project memory is currently empty.'; 'Project memory content from {{path}}:\n\n---\n{{content}}\n---',
{
path: projectMemoryPath,
content: memoryContent,
},
)
: t('Project memory is currently empty.');
context.ui.addItem( context.ui.addItem(
{ {
@@ -71,7 +84,9 @@ export const memoryCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: 'Project memory file not found or is currently empty.', text: t(
'Project memory file not found or is currently empty.',
),
}, },
Date.now(), Date.now(),
); );
@@ -80,7 +95,9 @@ export const memoryCommand: SlashCommand = {
}, },
{ {
name: '--global', name: '--global',
description: 'Show global memory contents.', get description() {
return t('Show global memory contents.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
try { try {
@@ -96,8 +113,10 @@ export const memoryCommand: SlashCommand = {
const messageContent = const messageContent =
globalMemoryContent.trim().length > 0 globalMemoryContent.trim().length > 0
? `Global memory content:\n\n---\n${globalMemoryContent}\n---` ? t('Global memory content:\n\n---\n{{content}}\n---', {
: 'Global memory is currently empty.'; content: globalMemoryContent,
})
: t('Global memory is currently empty.');
context.ui.addItem( context.ui.addItem(
{ {
@@ -110,7 +129,9 @@ export const memoryCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: 'Global memory file not found or is currently empty.', text: t(
'Global memory file not found or is currently empty.',
),
}, },
Date.now(), Date.now(),
); );
@@ -121,16 +142,20 @@ export const memoryCommand: SlashCommand = {
}, },
{ {
name: 'add', name: 'add',
description: get description() {
'Add content to the memory. Use --global for global memory or --project for project memory.', return t(
'Add content to the memory. Use --global for global memory or --project for project memory.',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context, args): SlashCommandActionReturn | void => { action: (context, args): SlashCommandActionReturn | void => {
if (!args || args.trim() === '') { if (!args || args.trim() === '') {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: content: t(
'Usage: /memory add [--global|--project] <text to remember>', 'Usage: /memory add [--global|--project] <text to remember>',
),
}; };
} }
@@ -150,8 +175,9 @@ export const memoryCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: content: t(
'Usage: /memory add [--global|--project] <text to remember>', 'Usage: /memory add [--global|--project] <text to remember>',
),
}; };
} else { } else {
// No scope specified, will be handled by the tool // No scope specified, will be handled by the tool
@@ -162,8 +188,9 @@ export const memoryCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: content: t(
'Usage: /memory add [--global|--project] <text to remember>', 'Usage: /memory add [--global|--project] <text to remember>',
),
}; };
} }
@@ -171,7 +198,10 @@ export const memoryCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Attempting to save to memory ${scopeText}: "${fact}"`, text: t('Attempting to save to memory {{scope}}: "{{fact}}"', {
scope: scopeText,
fact,
}),
}, },
Date.now(), Date.now(),
); );
@@ -185,21 +215,25 @@ export const memoryCommand: SlashCommand = {
subCommands: [ subCommands: [
{ {
name: '--project', name: '--project',
description: 'Add content to project-level memory.', get description() {
return t('Add content to project-level memory.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context, args): SlashCommandActionReturn | void => { action: (context, args): SlashCommandActionReturn | void => {
if (!args || args.trim() === '') { if (!args || args.trim() === '') {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Usage: /memory add --project <text to remember>', content: t('Usage: /memory add --project <text to remember>'),
}; };
} }
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Attempting to save to project memory: "${args.trim()}"`, text: t('Attempting to save to project memory: "{{text}}"', {
text: args.trim(),
}),
}, },
Date.now(), Date.now(),
); );
@@ -213,21 +247,25 @@ export const memoryCommand: SlashCommand = {
}, },
{ {
name: '--global', name: '--global',
description: 'Add content to global memory.', get description() {
return t('Add content to global memory.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context, args): SlashCommandActionReturn | void => { action: (context, args): SlashCommandActionReturn | void => {
if (!args || args.trim() === '') { if (!args || args.trim() === '') {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Usage: /memory add --global <text to remember>', content: t('Usage: /memory add --global <text to remember>'),
}; };
} }
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: `Attempting to save to global memory: "${args.trim()}"`, text: t('Attempting to save to global memory: "{{text}}"', {
text: args.trim(),
}),
}, },
Date.now(), Date.now(),
); );
@@ -243,13 +281,15 @@ export const memoryCommand: SlashCommand = {
}, },
{ {
name: 'refresh', name: 'refresh',
description: 'Refresh the memory from the source.', get description() {
return t('Refresh the memory from the source.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: 'Refreshing memory from source files...', text: t('Refreshing memory from source files...'),
}, },
Date.now(), Date.now(),
); );

View File

@@ -12,10 +12,13 @@ import type {
} from './types.js'; } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { getAvailableModelsForAuthType } from '../models/availableModels.js'; import { getAvailableModelsForAuthType } from '../models/availableModels.js';
import { t } from '../../i18n/index.js';
export const modelCommand: SlashCommand = { export const modelCommand: SlashCommand = {
name: 'model', name: 'model',
description: 'Switch the model for this session', get description() {
return t('Switch the model for this session');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async ( action: async (
context: CommandContext, context: CommandContext,
@@ -36,7 +39,7 @@ export const modelCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Content generator configuration not available.', content: t('Content generator configuration not available.'),
}; };
} }
@@ -45,7 +48,7 @@ export const modelCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Authentication type not available.', content: t('Authentication type not available.'),
}; };
} }
@@ -55,7 +58,12 @@ export const modelCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: `No models available for the current authentication type (${authType}).`, content: t(
'No models available for the current authentication type ({{authType}}).',
{
authType,
},
),
}; };
} }

View File

@@ -6,10 +6,13 @@
import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import type { OpenDialogActionReturn, SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const permissionsCommand: SlashCommand = { export const permissionsCommand: SlashCommand = {
name: 'permissions', name: 'permissions',
description: 'Manage folder trust settings', get description() {
return t('Manage folder trust settings');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (): OpenDialogActionReturn => ({ action: (): OpenDialogActionReturn => ({
type: 'dialog', type: 'dialog',

View File

@@ -6,10 +6,13 @@
import { formatDuration } from '../utils/formatters.js'; import { formatDuration } from '../utils/formatters.js';
import { CommandKind, type SlashCommand } from './types.js'; import { CommandKind, type SlashCommand } from './types.js';
import { t } from '../../i18n/index.js';
export const quitConfirmCommand: SlashCommand = { export const quitConfirmCommand: SlashCommand = {
name: 'quit-confirm', name: 'quit-confirm',
description: 'Show quit confirmation dialog', get description() {
return t('Show quit confirmation dialog');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context) => { action: (context) => {
const now = Date.now(); const now = Date.now();
@@ -37,7 +40,9 @@ export const quitConfirmCommand: SlashCommand = {
export const quitCommand: SlashCommand = { export const quitCommand: SlashCommand = {
name: 'quit', name: 'quit',
altNames: ['exit'], altNames: ['exit'],
description: 'exit the cli', get description() {
return t('exit the cli');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context) => { action: (context) => {
const now = Date.now(); const now = Date.now();

View File

@@ -6,10 +6,13 @@
import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import type { OpenDialogActionReturn, SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const settingsCommand: SlashCommand = { export const settingsCommand: SlashCommand = {
name: 'settings', name: 'settings',
description: 'View and edit Qwen Code settings', get description() {
return t('View and edit Qwen Code settings');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (_context, _args): OpenDialogActionReturn => ({ action: (_context, _args): OpenDialogActionReturn => ({
type: 'dialog', type: 'dialog',

View File

@@ -20,6 +20,7 @@ import {
import type { SlashCommand, SlashCommandActionReturn } from './types.js'; import type { SlashCommand, SlashCommandActionReturn } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js'; import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js';
import { t } from '../../i18n/index.js';
export const GITHUB_WORKFLOW_PATHS = [ export const GITHUB_WORKFLOW_PATHS = [
'gemini-dispatch/gemini-dispatch.yml', 'gemini-dispatch/gemini-dispatch.yml',
@@ -91,7 +92,9 @@ export async function updateGitignore(gitRepoRoot: string): Promise<void> {
export const setupGithubCommand: SlashCommand = { export const setupGithubCommand: SlashCommand = {
name: 'setup-github', name: 'setup-github',
description: 'Set up GitHub Actions', get description() {
return t('Set up GitHub Actions');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async ( action: async (
context: CommandContext, context: CommandContext,

View File

@@ -12,11 +12,14 @@ import {
type SlashCommand, type SlashCommand,
CommandKind, CommandKind,
} from './types.js'; } from './types.js';
import { t } from '../../i18n/index.js';
export const statsCommand: SlashCommand = { export const statsCommand: SlashCommand = {
name: 'stats', name: 'stats',
altNames: ['usage'], altNames: ['usage'],
description: 'check session stats. Usage: /stats [model|tools]', get description() {
return t('check session stats. Usage: /stats [model|tools]');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context: CommandContext) => { action: (context: CommandContext) => {
const now = new Date(); const now = new Date();
@@ -25,7 +28,7 @@ export const statsCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: 'Session start time is unavailable, cannot calculate stats.', text: t('Session start time is unavailable, cannot calculate stats.'),
}, },
Date.now(), Date.now(),
); );
@@ -43,7 +46,9 @@ export const statsCommand: SlashCommand = {
subCommands: [ subCommands: [
{ {
name: 'model', name: 'model',
description: 'Show model-specific usage statistics.', get description() {
return t('Show model-specific usage statistics.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context: CommandContext) => { action: (context: CommandContext) => {
context.ui.addItem( context.ui.addItem(
@@ -56,7 +61,9 @@ export const statsCommand: SlashCommand = {
}, },
{ {
name: 'tools', name: 'tools',
description: 'Show tool-specific usage statistics.', get description() {
return t('Show tool-specific usage statistics.');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (context: CommandContext) => { action: (context: CommandContext) => {
context.ui.addItem( context.ui.addItem(

View File

@@ -13,11 +13,15 @@ import {
} from './types.js'; } from './types.js';
import { getProjectSummaryPrompt } from '@qwen-code/qwen-code-core'; import { getProjectSummaryPrompt } from '@qwen-code/qwen-code-core';
import type { HistoryItemSummary } from '../types.js'; import type { HistoryItemSummary } from '../types.js';
import { t } from '../../i18n/index.js';
export const summaryCommand: SlashCommand = { export const summaryCommand: SlashCommand = {
name: 'summary', name: 'summary',
description: get description() {
'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md', return t(
'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context): Promise<SlashCommandActionReturn> => { action: async (context): Promise<SlashCommandActionReturn> => {
const { config } = context.services; const { config } = context.services;
@@ -26,7 +30,7 @@ export const summaryCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Config not loaded.', content: t('Config not loaded.'),
}; };
} }
@@ -35,7 +39,7 @@ export const summaryCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'No chat client available to generate summary.', content: t('No chat client available to generate summary.'),
}; };
} }
@@ -44,15 +48,18 @@ export const summaryCommand: SlashCommand = {
ui.addItem( ui.addItem(
{ {
type: 'error' as const, type: 'error' as const,
text: 'Already generating summary, wait for previous request to complete', text: t(
'Already generating summary, wait for previous request to complete',
),
}, },
Date.now(), Date.now(),
); );
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: content: t(
'Already generating summary, wait for previous request to complete', 'Already generating summary, wait for previous request to complete',
),
}; };
} }
@@ -65,7 +72,7 @@ export const summaryCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: 'No conversation found to summarize.', content: t('No conversation found to summarize.'),
}; };
} }
@@ -171,9 +178,12 @@ export const summaryCommand: SlashCommand = {
ui.addItem( ui.addItem(
{ {
type: 'error' as const, type: 'error' as const,
text: `Failed to generate project context summary: ${ text: `${t(
error instanceof Error ? error.message : String(error) 'Failed to generate project context summary: {{error}}',
}`, {
error: error instanceof Error ? error.message : String(error),
},
)}`,
}, },
Date.now(), Date.now(),
); );
@@ -181,9 +191,9 @@ export const summaryCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: `Failed to generate project context summary: ${ content: t('Failed to generate project context summary: {{error}}', {
error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error),
}`, }),
}; };
} }
}, },

View File

@@ -7,6 +7,7 @@
import type { MessageActionReturn, SlashCommand } from './types.js'; import type { MessageActionReturn, SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { terminalSetup } from '../utils/terminalSetup.js'; import { terminalSetup } from '../utils/terminalSetup.js';
import { t } from '../../i18n/index.js';
/** /**
* Command to configure terminal keybindings for multiline input support. * Command to configure terminal keybindings for multiline input support.
@@ -16,8 +17,11 @@ import { terminalSetup } from '../utils/terminalSetup.js';
*/ */
export const terminalSetupCommand: SlashCommand = { export const terminalSetupCommand: SlashCommand = {
name: 'terminal-setup', name: 'terminal-setup',
description: get description() {
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)', return t(
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)',
);
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (): Promise<MessageActionReturn> => { action: async (): Promise<MessageActionReturn> => {
@@ -27,7 +31,8 @@ export const terminalSetupCommand: SlashCommand = {
let content = result.message; let content = result.message;
if (result.requiresRestart) { if (result.requiresRestart) {
content += content +=
'\n\nPlease restart your terminal for the changes to take effect.'; '\n\n' +
t('Please restart your terminal for the changes to take effect.');
} }
return { return {
@@ -38,7 +43,9 @@ export const terminalSetupCommand: SlashCommand = {
} catch (error) { } catch (error) {
return { return {
type: 'message', type: 'message',
content: `Failed to configure terminal: ${error}`, content: t('Failed to configure terminal: {{error}}', {
error: String(error),
}),
messageType: 'error', messageType: 'error',
}; };
} }

View File

@@ -6,10 +6,13 @@
import type { OpenDialogActionReturn, SlashCommand } from './types.js'; import type { OpenDialogActionReturn, SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const themeCommand: SlashCommand = { export const themeCommand: SlashCommand = {
name: 'theme', name: 'theme',
description: 'change the theme', get description() {
return t('change the theme');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (_context, _args): OpenDialogActionReturn => ({ action: (_context, _args): OpenDialogActionReturn => ({
type: 'dialog', type: 'dialog',

View File

@@ -10,10 +10,13 @@ import {
CommandKind, CommandKind,
} from './types.js'; } from './types.js';
import { MessageType, type HistoryItemToolsList } from '../types.js'; import { MessageType, type HistoryItemToolsList } from '../types.js';
import { t } from '../../i18n/index.js';
export const toolsCommand: SlashCommand = { export const toolsCommand: SlashCommand = {
name: 'tools', name: 'tools',
description: 'list available Qwen Code tools. Usage: /tools [desc]', get description() {
return t('list available Qwen Code tools. Usage: /tools [desc]');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context: CommandContext, args?: string): Promise<void> => { action: async (context: CommandContext, args?: string): Promise<void> => {
const subCommand = args?.trim(); const subCommand = args?.trim();
@@ -29,7 +32,7 @@ export const toolsCommand: SlashCommand = {
context.ui.addItem( context.ui.addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
text: 'Could not retrieve tool registry.', text: t('Could not retrieve tool registry.'),
}, },
Date.now(), Date.now(),
); );

View File

@@ -6,10 +6,13 @@
import type { SlashCommand } from './types.js'; import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
export const vimCommand: SlashCommand = { export const vimCommand: SlashCommand = {
name: 'vim', name: 'vim',
description: 'toggle vim mode on/off', get description() {
return t('toggle vim mode on/off');
},
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context, _args) => { action: async (context, _args) => {
const newVimState = await context.ui.toggleVimEnabled(); const newVimState = await context.ui.toggleVimEnabled();

View File

@@ -13,6 +13,7 @@ import {
getFieldValue, getFieldValue,
type SystemInfoField, type SystemInfoField,
} from '../../utils/systemInfoFields.js'; } from '../../utils/systemInfoFields.js';
import { t } from '../../i18n/index.js';
type AboutBoxProps = ExtendedSystemInfo; type AboutBoxProps = ExtendedSystemInfo;
@@ -30,7 +31,7 @@ export const AboutBox: React.FC<AboutBoxProps> = (props) => {
> >
<Box marginBottom={1}> <Box marginBottom={1}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
About Qwen Code {t('About Qwen Code')}
</Text> </Text>
</Box> </Box>
{fields.map((field: SystemInfoField) => ( {fields.map((field: SystemInfoField) => (

View File

@@ -15,6 +15,7 @@ import { SettingScope } from '../../config/settings.js';
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { ScopeSelector } from './shared/ScopeSelector.js'; import { ScopeSelector } from './shared/ScopeSelector.js';
import { t } from '../../i18n/index.js';
interface ApprovalModeDialogProps { interface ApprovalModeDialogProps {
/** Callback function when an approval mode is selected */ /** Callback function when an approval mode is selected */
@@ -33,15 +34,15 @@ interface ApprovalModeDialogProps {
const formatModeDescription = (mode: ApprovalMode): string => { const formatModeDescription = (mode: ApprovalMode): string => {
switch (mode) { switch (mode) {
case ApprovalMode.PLAN: case ApprovalMode.PLAN:
return 'Analyze only, do not modify files or execute commands'; return t('Analyze only, do not modify files or execute commands');
case ApprovalMode.DEFAULT: case ApprovalMode.DEFAULT:
return 'Require approval for file edits or shell commands'; return t('Require approval for file edits or shell commands');
case ApprovalMode.AUTO_EDIT: case ApprovalMode.AUTO_EDIT:
return 'Automatically approve file edits'; return t('Automatically approve file edits');
case ApprovalMode.YOLO: case ApprovalMode.YOLO:
return 'Automatically approve all tools'; return t('Automatically approve all tools');
default: default:
return `${mode} mode`; return t('{{mode}} mode', { mode });
} }
}; };
@@ -134,7 +135,8 @@ export function ApprovalModeDialog({
<Box flexDirection="column" flexGrow={1}> <Box flexDirection="column" flexGrow={1}>
{/* Approval Mode Selection */} {/* Approval Mode Selection */}
<Text bold={focusSection === 'mode'} wrap="truncate"> <Text bold={focusSection === 'mode'} wrap="truncate">
{focusSection === 'mode' ? '> ' : ' '}Approval Mode{' '} {focusSection === 'mode' ? '> ' : ' '}
{t('Approval Mode')}{' '}
<Text color={theme.text.secondary}>{otherScopeModifiedMessage}</Text> <Text color={theme.text.secondary}>{otherScopeModifiedMessage}</Text>
</Text> </Text>
<Box height={1} /> <Box height={1} />
@@ -167,15 +169,17 @@ export function ApprovalModeDialog({
{showWorkspacePriorityWarning && ( {showWorkspacePriorityWarning && (
<> <>
<Text color={theme.status.warning} wrap="wrap"> <Text color={theme.status.warning} wrap="wrap">
Workspace approval mode exists and takes priority. User-level {' '}
change will have no effect. {t(
'Workspace approval mode exists and takes priority. User-level change will have no effect.',
)}
</Text> </Text>
<Box height={1} /> <Box height={1} />
</> </>
)} )}
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
(Use Enter to select, Tab to change focus) {t('(Use Enter to select, Tab to change focus)')}
</Text> </Text>
</Box> </Box>
</Box> </Box>

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { ApprovalMode } from '@qwen-code/qwen-code-core'; import { ApprovalMode } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
interface AutoAcceptIndicatorProps { interface AutoAcceptIndicatorProps {
approvalMode: ApprovalMode; approvalMode: ApprovalMode;
@@ -23,18 +24,18 @@ export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
switch (approvalMode) { switch (approvalMode) {
case ApprovalMode.PLAN: case ApprovalMode.PLAN:
textColor = theme.status.success; textColor = theme.status.success;
textContent = 'plan mode'; textContent = t('plan mode');
subText = ' (shift + tab to cycle)'; subText = ` ${t('(shift + tab to cycle)')}`;
break; break;
case ApprovalMode.AUTO_EDIT: case ApprovalMode.AUTO_EDIT:
textColor = theme.status.warning; textColor = theme.status.warning;
textContent = 'auto-accept edits'; textContent = t('auto-accept edits');
subText = ' (shift + tab to cycle)'; subText = ` ${t('(shift + tab to cycle)')}`;
break; break;
case ApprovalMode.YOLO: case ApprovalMode.YOLO:
textColor = theme.status.error; textColor = theme.status.error;
textContent = 'YOLO mode'; textContent = t('YOLO mode');
subText = ' (shift + tab to cycle)'; subText = ` ${t('(shift + tab to cycle)')}`;
break; break;
case ApprovalMode.DEFAULT: case ApprovalMode.DEFAULT:
default: default:

View File

@@ -26,6 +26,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
import { ApprovalMode } from '@qwen-code/qwen-code-core'; import { ApprovalMode } from '@qwen-code/qwen-code-core';
import { StreamingState } from '../types.js'; import { StreamingState } from '../types.js';
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
import { t } from '../../i18n/index.js';
export const Composer = () => { export const Composer = () => {
const config = useConfig(); const config = useConfig();
@@ -86,14 +87,16 @@ export const Composer = () => {
)} )}
{uiState.ctrlCPressedOnce ? ( {uiState.ctrlCPressedOnce ? (
<Text color={theme.status.warning}> <Text color={theme.status.warning}>
Press Ctrl+C again to exit. {t('Press Ctrl+C again to exit.')}
</Text> </Text>
) : uiState.ctrlDPressedOnce ? ( ) : uiState.ctrlDPressedOnce ? (
<Text color={theme.status.warning}> <Text color={theme.status.warning}>
Press Ctrl+D again to exit. {t('Press Ctrl+D again to exit.')}
</Text> </Text>
) : uiState.showEscapePrompt ? ( ) : uiState.showEscapePrompt ? (
<Text color={theme.text.secondary}>Press Esc again to clear.</Text> <Text color={theme.text.secondary}>
{t('Press Esc again to clear.')}
</Text>
) : ( ) : (
!settings.merged.ui?.hideContextSummary && ( !settings.merged.ui?.hideContextSummary && (
<ContextSummaryDisplay <ContextSummaryDisplay
@@ -151,8 +154,8 @@ export const Composer = () => {
isEmbeddedShellFocused={uiState.embeddedShellFocused} isEmbeddedShellFocused={uiState.embeddedShellFocused}
placeholder={ placeholder={
vimEnabled vimEnabled
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode." ? ' ' + t("Press 'i' for INSERT mode and 'Esc' for NORMAL mode.")
: ' Type your message or @path/to/file' : ' ' + t('Type your message or @path/to/file')
} }
/> />
)} )}

View File

@@ -11,15 +11,16 @@ import { useConfig } from '../contexts/ConfigContext.js';
import { type McpClient, MCPServerStatus } from '@qwen-code/qwen-code-core'; import { type McpClient, MCPServerStatus } from '@qwen-code/qwen-code-core';
import { GeminiSpinner } from './GeminiRespondingSpinner.js'; import { GeminiSpinner } from './GeminiRespondingSpinner.js';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { t } from '../../i18n/index.js';
export const ConfigInitDisplay = () => { export const ConfigInitDisplay = () => {
const config = useConfig(); const config = useConfig();
const [message, setMessage] = useState('Initializing...'); const [message, setMessage] = useState(t('Initializing...'));
useEffect(() => { useEffect(() => {
const onChange = (clients?: Map<string, McpClient>) => { const onChange = (clients?: Map<string, McpClient>) => {
if (!clients || clients.size === 0) { if (!clients || clients.size === 0) {
setMessage(`Initializing...`); setMessage(t('Initializing...'));
return; return;
} }
let connected = 0; let connected = 0;
@@ -28,7 +29,12 @@ export const ConfigInitDisplay = () => {
connected++; connected++;
} }
} }
setMessage(`Connecting to MCP servers... (${connected}/${clients.size})`); setMessage(
t('Connecting to MCP servers... ({{connected}}/{{total}})', {
connected: String(connected),
total: String(clients.size),
}),
);
}; };
appEvents.on('mcp-client-update', onChange); appEvents.on('mcp-client-update', onChange);

View File

@@ -13,6 +13,7 @@ import {
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { isNarrowWidth } from '../utils/isNarrowWidth.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { t } from '../../i18n/index.js';
interface ContextSummaryDisplayProps { interface ContextSummaryDisplayProps {
geminiMdFileCount: number; geminiMdFileCount: number;
@@ -50,9 +51,11 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
if (openFileCount === 0) { if (openFileCount === 0) {
return ''; return '';
} }
return `${openFileCount} open file${ const fileText =
openFileCount > 1 ? 's' : '' openFileCount === 1
} (ctrl+g to view)`; ? t('{{count}} open file', { count: String(openFileCount) })
: t('{{count}} open files', { count: String(openFileCount) });
return `${fileText} ${t('(ctrl+g to view)')}`;
})(); })();
const geminiMdText = (() => { const geminiMdText = (() => {
@@ -61,9 +64,15 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
} }
const allNamesTheSame = new Set(contextFileNames).size < 2; const allNamesTheSame = new Set(contextFileNames).size < 2;
const name = allNamesTheSame ? contextFileNames[0] : 'context'; const name = allNamesTheSame ? contextFileNames[0] : 'context';
return `${geminiMdFileCount} ${name} file${ return geminiMdFileCount === 1
geminiMdFileCount > 1 ? 's' : '' ? t('{{count}} {{name}} file', {
}`; count: String(geminiMdFileCount),
name,
})
: t('{{count}} {{name}} files', {
count: String(geminiMdFileCount),
name,
});
})(); })();
const mcpText = (() => { const mcpText = (() => {
@@ -73,15 +82,27 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
const parts = []; const parts = [];
if (mcpServerCount > 0) { if (mcpServerCount > 0) {
parts.push( const serverText =
`${mcpServerCount} MCP server${mcpServerCount > 1 ? 's' : ''}`, mcpServerCount === 1
); ? t('{{count}} MCP server', { count: String(mcpServerCount) })
: t('{{count}} MCP servers', { count: String(mcpServerCount) });
parts.push(serverText);
} }
if (blockedMcpServerCount > 0) { if (blockedMcpServerCount > 0) {
let blockedText = `${blockedMcpServerCount} Blocked`; let blockedText = t('{{count}} Blocked', {
count: String(blockedMcpServerCount),
});
if (mcpServerCount === 0) { if (mcpServerCount === 0) {
blockedText += ` MCP server${blockedMcpServerCount > 1 ? 's' : ''}`; const serverText =
blockedMcpServerCount === 1
? t('{{count}} MCP server', {
count: String(blockedMcpServerCount),
})
: t('{{count}} MCP servers', {
count: String(blockedMcpServerCount),
});
blockedText += ` ${serverText}`;
} }
parts.push(blockedText); parts.push(blockedText);
} }
@@ -89,9 +110,9 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
// Add ctrl+t hint when MCP servers are available // Add ctrl+t hint when MCP servers are available
if (mcpServers && Object.keys(mcpServers).length > 0) { if (mcpServers && Object.keys(mcpServers).length > 0) {
if (showToolDescriptions) { if (showToolDescriptions) {
text += ' (ctrl+t to toggle)'; text += ` ${t('(ctrl+t to toggle)')}`;
} else { } else {
text += ' (ctrl+t to view)'; text += ` ${t('(ctrl+t to view)')}`;
} }
} }
return text; return text;
@@ -102,7 +123,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
if (isNarrow) { if (isNarrow) {
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
<Text color={theme.text.secondary}>Using:</Text> <Text color={theme.text.secondary}>{t('Using:')}</Text>
{summaryParts.map((part, index) => ( {summaryParts.map((part, index) => (
<Text key={index} color={theme.text.secondary}> <Text key={index} color={theme.text.secondary}>
{' '}- {part} {' '}- {part}
@@ -115,7 +136,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
return ( return (
<Box> <Box>
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Using: {summaryParts.join(' | ')} {t('Using:')} {summaryParts.join(' | ')}
</Text> </Text>
</Box> </Box>
); );

View File

@@ -19,6 +19,7 @@ import { SettingScope } from '../../config/settings.js';
import type { EditorType } from '@qwen-code/qwen-code-core'; import type { EditorType } from '@qwen-code/qwen-code-core';
import { isEditorAvailable } from '@qwen-code/qwen-code-core'; import { isEditorAvailable } from '@qwen-code/qwen-code-core';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
interface EditorDialogProps { interface EditorDialogProps {
onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void; onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void;
@@ -66,12 +67,16 @@ export function EditorSettingsDialog({
const scopeItems = [ const scopeItems = [
{ {
label: 'User Settings', get label() {
return t('User Settings');
},
value: SettingScope.User, value: SettingScope.User,
key: SettingScope.User, key: SettingScope.User,
}, },
{ {
label: 'Workspace Settings', get label() {
return t('Workspace Settings');
},
value: SettingScope.Workspace, value: SettingScope.Workspace,
key: SettingScope.Workspace, key: SettingScope.Workspace,
}, },
@@ -145,7 +150,8 @@ export function EditorSettingsDialog({
<Box marginTop={1} flexDirection="column"> <Box marginTop={1} flexDirection="column">
<Text bold={focusedSection === 'scope'}> <Text bold={focusedSection === 'scope'}>
{focusedSection === 'scope' ? '> ' : ' '}Apply To {focusedSection === 'scope' ? '> ' : ' '}
{t('Apply To')}
</Text> </Text>
<RadioButtonSelect <RadioButtonSelect
items={scopeItems} items={scopeItems}

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { type SlashCommand, CommandKind } from '../commands/types.js'; import { type SlashCommand, CommandKind } from '../commands/types.js';
import { t } from '../../i18n/index.js';
interface Help { interface Help {
commands: readonly SlashCommand[]; commands: readonly SlashCommand[];
@@ -23,46 +24,41 @@ export const Help: React.FC<Help> = ({ commands }) => (
> >
{/* Basics */} {/* Basics */}
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Basics: {t('Basics:')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Add context {t('Add context')}
</Text> </Text>
: Use{' '} :{' '}
<Text bold color={theme.text.accent}> {t(
@ 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.',
</Text>{' '} {
to specify files for context (e.g.,{' '} symbol: t('@'),
<Text bold color={theme.text.accent}> example: t('@src/myFile.ts'),
@src/myFile.ts },
</Text> )}
) to target specific files or folders.
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Shell mode {t('Shell mode')}
</Text> </Text>
: Execute shell commands via{' '} :{' '}
<Text bold color={theme.text.accent}> {t(
! 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).',
</Text>{' '} {
(e.g.,{' '} symbol: t('!'),
<Text bold color={theme.text.accent}> example1: t('!npm run start'),
!npm run start example2: t('start server'),
</Text> },
) or use natural language (e.g.{' '} )}
<Text bold color={theme.text.accent}>
start server
</Text>
).
</Text> </Text>
<Box height={1} /> <Box height={1} />
{/* Commands */} {/* Commands */}
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Commands: {t('Commands:')}
</Text> </Text>
{commands {commands
.filter((command) => command.description && !command.hidden) .filter((command) => command.description && !command.hidden)
@@ -97,81 +93,81 @@ export const Help: React.FC<Help> = ({ commands }) => (
{' '} {' '}
!{' '} !{' '}
</Text> </Text>
- shell command - {t('shell command')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text color={theme.text.secondary}>[MCP]</Text> - Model Context Protocol <Text color={theme.text.secondary}>[MCP]</Text> -{' '}
command (from external servers) {t('Model Context Protocol command (from external servers)')}
</Text> </Text>
<Box height={1} /> <Box height={1} />
{/* Shortcuts */} {/* Shortcuts */}
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Keyboard Shortcuts: {t('Keyboard Shortcuts:')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Alt+Left/Right Alt+Left/Right
</Text>{' '} </Text>{' '}
- Jump through words in the input - {t('Jump through words in the input')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Ctrl+C Ctrl+C
</Text>{' '} </Text>{' '}
- Close dialogs, cancel requests, or quit application - {t('Close dialogs, cancel requests, or quit application')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
{process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'} {process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'}
</Text>{' '} </Text>{' '}
-{' '}
{process.platform === 'linux' {process.platform === 'linux'
? '- New line (Alt+Enter works for certain linux distros)' ? t('New line (Alt+Enter works for certain linux distros)')
: '- New line'} : t('New line')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Ctrl+L Ctrl+L
</Text>{' '} </Text>{' '}
- Clear the screen - {t('Clear the screen')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
{process.platform === 'darwin' ? 'Ctrl+X / Meta+Enter' : 'Ctrl+X'} {process.platform === 'darwin' ? 'Ctrl+X / Meta+Enter' : 'Ctrl+X'}
</Text>{' '} </Text>{' '}
- Open input in external editor - {t('Open input in external editor')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Enter Enter
</Text>{' '} </Text>{' '}
- Send message - {t('Send message')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Esc Esc
</Text>{' '} </Text>{' '}
- Cancel operation / Clear input (double press) - {t('Cancel operation / Clear input (double press)')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Shift+Tab Shift+Tab
</Text>{' '} </Text>{' '}
- Cycle approval modes - {t('Cycle approval modes')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Up/Down Up/Down
</Text>{' '} </Text>{' '}
- Cycle through your prompt history - {t('Cycle through your prompt history')}
</Text> </Text>
<Box height={1} /> <Box height={1} />
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
For a full list of shortcuts, see{' '} {t('For a full list of shortcuts, see {{docPath}}', {
<Text bold color={theme.text.accent}> docPath: t('docs/keyboard-shortcuts.md'),
docs/keyboard-shortcuts.md })}
</Text>
</Text> </Text>
</Box> </Box>
); );

View File

@@ -210,6 +210,7 @@ describe('InputPrompt', () => {
inputWidth: 80, inputWidth: 80,
suggestionsWidth: 80, suggestionsWidth: 80,
focus: true, focus: true,
placeholder: ' Type your message or @path/to/file',
}; };
}); });

View File

@@ -27,6 +27,7 @@ import {
parseInputForHighlighting, parseInputForHighlighting,
buildSegmentsForVisualSlice, buildSegmentsForVisualSlice,
} from '../utils/highlight.js'; } from '../utils/highlight.js';
import { t } from '../../i18n/index.js';
import { import {
clipboardHasImage, clipboardHasImage,
saveClipboardImage, saveClipboardImage,
@@ -88,7 +89,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
config, config,
slashCommands, slashCommands,
commandContext, commandContext,
placeholder = ' Type your message or @path/to/file', placeholder,
focus = true, focus = true,
suggestionsWidth, suggestionsWidth,
shellModeActive, shellModeActive,
@@ -697,13 +698,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
let statusText = ''; let statusText = '';
if (shellModeActive) { if (shellModeActive) {
statusColor = theme.ui.symbol; statusColor = theme.ui.symbol;
statusText = 'Shell mode'; statusText = t('Shell mode');
} else if (showYoloStyling) { } else if (showYoloStyling) {
statusColor = theme.status.error; statusColor = theme.status.error;
statusText = 'YOLO mode'; statusText = t('YOLO mode');
} else if (showAutoAcceptStyling) { } else if (showAutoAcceptStyling) {
statusColor = theme.status.warning; statusColor = theme.status.warning;
statusText = 'Accepting edits'; statusText = t('Accepting edits');
} }
return ( return (

View File

@@ -14,6 +14,7 @@ import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
import { formatDuration } from '../utils/formatters.js'; import { formatDuration } from '../utils/formatters.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { isNarrowWidth } from '../utils/isNarrowWidth.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { t } from '../../i18n/index.js';
interface LoadingIndicatorProps { interface LoadingIndicatorProps {
currentLoadingPhrase?: string; currentLoadingPhrase?: string;
@@ -40,7 +41,12 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
const cancelAndTimerContent = const cancelAndTimerContent =
streamingState !== StreamingState.WaitingForConfirmation streamingState !== StreamingState.WaitingForConfirmation
? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})` ? t('(esc to cancel, {{time}})', {
time:
elapsedTime < 60
? `${elapsedTime}s`
: formatDuration(elapsedTime * 1000),
})
: null; : null;
return ( return (

View File

@@ -20,6 +20,7 @@ import {
getAvailableModelsForAuthType, getAvailableModelsForAuthType,
MAINLINE_CODER, MAINLINE_CODER,
} from '../models/availableModels.js'; } from '../models/availableModels.js';
import { t } from '../../i18n/index.js';
interface ModelDialogProps { interface ModelDialogProps {
onClose: () => void; onClose: () => void;
@@ -87,7 +88,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
padding={1} padding={1}
width="100%" width="100%"
> >
<Text bold>Select Model</Text> <Text bold>{t('Select Model')}</Text>
<Box marginTop={1}> <Box marginTop={1}>
<DescriptiveRadioButtonSelect <DescriptiveRadioButtonSelect
items={MODEL_OPTIONS} items={MODEL_OPTIONS}
@@ -97,7 +98,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
/> />
</Box> </Box>
<Box marginTop={1} flexDirection="column"> <Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>(Press Esc to close)</Text> <Text color={theme.text.secondary}>{t('(Press Esc to close)')}</Text>
</Box> </Box>
</Box> </Box>
); );

View File

@@ -15,6 +15,7 @@ import {
} from '../utils/computeStats.js'; } from '../utils/computeStats.js';
import type { ModelMetrics } from '../contexts/SessionContext.js'; import type { ModelMetrics } from '../contexts/SessionContext.js';
import { useSessionStats } from '../contexts/SessionContext.js'; import { useSessionStats } from '../contexts/SessionContext.js';
import { t } from '../../i18n/index.js';
const METRIC_COL_WIDTH = 28; const METRIC_COL_WIDTH = 28;
const MODEL_COL_WIDTH = 22; const MODEL_COL_WIDTH = 22;
@@ -65,7 +66,7 @@ export const ModelStatsDisplay: React.FC = () => {
paddingX={2} paddingX={2}
> >
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
No API calls have been made in this session. {t('No API calls have been made in this session.')}
</Text> </Text>
</Box> </Box>
); );
@@ -94,7 +95,7 @@ export const ModelStatsDisplay: React.FC = () => {
paddingX={2} paddingX={2}
> >
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Model Stats For Nerds {t('Model Stats For Nerds')}
</Text> </Text>
<Box height={1} /> <Box height={1} />
@@ -102,7 +103,7 @@ export const ModelStatsDisplay: React.FC = () => {
<Box> <Box>
<Box width={METRIC_COL_WIDTH}> <Box width={METRIC_COL_WIDTH}>
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Metric {t('Metric')}
</Text> </Text>
</Box> </Box>
{modelNames.map((name) => ( {modelNames.map((name) => (
@@ -125,13 +126,13 @@ export const ModelStatsDisplay: React.FC = () => {
/> />
{/* API Section */} {/* API Section */}
<StatRow title="API" values={[]} isSection /> <StatRow title={t('API')} values={[]} isSection />
<StatRow <StatRow
title="Requests" title={t('Requests')}
values={getModelValues((m) => m.api.totalRequests.toLocaleString())} values={getModelValues((m) => m.api.totalRequests.toLocaleString())}
/> />
<StatRow <StatRow
title="Errors" title={t('Errors')}
values={getModelValues((m) => { values={getModelValues((m) => {
const errorRate = calculateErrorRate(m); const errorRate = calculateErrorRate(m);
return ( return (
@@ -146,7 +147,7 @@ export const ModelStatsDisplay: React.FC = () => {
})} })}
/> />
<StatRow <StatRow
title="Avg Latency" title={t('Avg Latency')}
values={getModelValues((m) => { values={getModelValues((m) => {
const avgLatency = calculateAverageLatency(m); const avgLatency = calculateAverageLatency(m);
return formatDuration(avgLatency); return formatDuration(avgLatency);
@@ -156,9 +157,9 @@ export const ModelStatsDisplay: React.FC = () => {
<Box height={1} /> <Box height={1} />
{/* Tokens Section */} {/* Tokens Section */}
<StatRow title="Tokens" values={[]} isSection /> <StatRow title={t('Tokens')} values={[]} isSection />
<StatRow <StatRow
title="Total" title={t('Total')}
values={getModelValues((m) => ( values={getModelValues((m) => (
<Text color={theme.status.warning}> <Text color={theme.status.warning}>
{m.tokens.total.toLocaleString()} {m.tokens.total.toLocaleString()}
@@ -166,13 +167,13 @@ export const ModelStatsDisplay: React.FC = () => {
))} ))}
/> />
<StatRow <StatRow
title="Prompt" title={t('Prompt')}
isSubtle isSubtle
values={getModelValues((m) => m.tokens.prompt.toLocaleString())} values={getModelValues((m) => m.tokens.prompt.toLocaleString())}
/> />
{hasCached && ( {hasCached && (
<StatRow <StatRow
title="Cached" title={t('Cached')}
isSubtle isSubtle
values={getModelValues((m) => { values={getModelValues((m) => {
const cacheHitRate = calculateCacheHitRate(m); const cacheHitRate = calculateCacheHitRate(m);
@@ -186,20 +187,20 @@ export const ModelStatsDisplay: React.FC = () => {
)} )}
{hasThoughts && ( {hasThoughts && (
<StatRow <StatRow
title="Thoughts" title={t('Thoughts')}
isSubtle isSubtle
values={getModelValues((m) => m.tokens.thoughts.toLocaleString())} values={getModelValues((m) => m.tokens.thoughts.toLocaleString())}
/> />
)} )}
{hasTool && ( {hasTool && (
<StatRow <StatRow
title="Tool" title={t('Tool')}
isSubtle isSubtle
values={getModelValues((m) => m.tokens.tool.toLocaleString())} values={getModelValues((m) => m.tokens.tool.toLocaleString())}
/> />
)} )}
<StatRow <StatRow
title="Output" title={t('Output')}
isSubtle isSubtle
values={getModelValues((m) => m.tokens.candidates.toLocaleString())} values={getModelValues((m) => m.tokens.candidates.toLocaleString())}
/> />

View File

@@ -10,6 +10,7 @@ import { z } from 'zod';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { Colors } from '../colors.js'; import { Colors } from '../colors.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
interface OpenAIKeyPromptProps { interface OpenAIKeyPromptProps {
onSubmit: (apiKey: string, baseUrl: string, model: string) => void; onSubmit: (apiKey: string, baseUrl: string, model: string) => void;
@@ -64,9 +65,11 @@ export function OpenAIKeyPrompt({
const errorMessage = error.errors const errorMessage = error.errors
.map((e) => `${e.path.join('.')}: ${e.message}`) .map((e) => `${e.path.join('.')}: ${e.message}`)
.join(', '); .join(', ');
setValidationError(`Invalid credentials: ${errorMessage}`); setValidationError(
t('Invalid credentials: {{errorMessage}}', { errorMessage }),
);
} else { } else {
setValidationError('Failed to validate credentials'); setValidationError(t('Failed to validate credentials'));
} }
} }
}; };
@@ -205,7 +208,7 @@ export function OpenAIKeyPrompt({
width="100%" width="100%"
> >
<Text bold color={Colors.AccentBlue}> <Text bold color={Colors.AccentBlue}>
OpenAI Configuration Required {t('OpenAI Configuration Required')}
</Text> </Text>
{validationError && ( {validationError && (
<Box marginTop={1}> <Box marginTop={1}>
@@ -214,7 +217,9 @@ export function OpenAIKeyPrompt({
)} )}
<Box marginTop={1}> <Box marginTop={1}>
<Text> <Text>
Please enter your OpenAI configuration. You can get an API key from{' '} {t(
'Please enter your OpenAI configuration. You can get an API key from',
)}{' '}
<Text color={Colors.AccentBlue}> <Text color={Colors.AccentBlue}>
https://bailian.console.aliyun.com/?tab=model#/api-key https://bailian.console.aliyun.com/?tab=model#/api-key
</Text> </Text>
@@ -225,7 +230,7 @@ export function OpenAIKeyPrompt({
<Text <Text
color={currentField === 'apiKey' ? Colors.AccentBlue : Colors.Gray} color={currentField === 'apiKey' ? Colors.AccentBlue : Colors.Gray}
> >
API Key: {t('API Key:')}
</Text> </Text>
</Box> </Box>
<Box flexGrow={1}> <Box flexGrow={1}>
@@ -240,7 +245,7 @@ export function OpenAIKeyPrompt({
<Text <Text
color={currentField === 'baseUrl' ? Colors.AccentBlue : Colors.Gray} color={currentField === 'baseUrl' ? Colors.AccentBlue : Colors.Gray}
> >
Base URL: {t('Base URL:')}
</Text> </Text>
</Box> </Box>
<Box flexGrow={1}> <Box flexGrow={1}>
@@ -255,7 +260,7 @@ export function OpenAIKeyPrompt({
<Text <Text
color={currentField === 'model' ? Colors.AccentBlue : Colors.Gray} color={currentField === 'model' ? Colors.AccentBlue : Colors.Gray}
> >
Model: {t('Model:')}
</Text> </Text>
</Box> </Box>
<Box flexGrow={1}> <Box flexGrow={1}>
@@ -267,7 +272,7 @@ export function OpenAIKeyPrompt({
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>
<Text color={Colors.Gray}> <Text color={Colors.Gray}>
Press Enter to continue, Tab/ to navigate, Esc to cancel {t('Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel')}
</Text> </Text>
</Box> </Box>
</Box> </Box>

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { t } from '../../i18n/index.js';
interface ProQuotaDialogProps { interface ProQuotaDialogProps {
failedModel: string; failedModel: string;
@@ -22,12 +23,12 @@ export function ProQuotaDialog({
}: ProQuotaDialogProps): React.JSX.Element { }: ProQuotaDialogProps): React.JSX.Element {
const items = [ const items = [
{ {
label: 'Change auth (executes the /auth command)', label: t('Change auth (executes the /auth command)'),
value: 'auth' as const, value: 'auth' as const,
key: 'auth', key: 'auth',
}, },
{ {
label: `Continue with ${fallbackModel}`, label: t('Continue with {{model}}', { model: fallbackModel }),
value: 'continue' as const, value: 'continue' as const,
key: 'continue', key: 'continue',
}, },
@@ -40,7 +41,7 @@ export function ProQuotaDialog({
return ( return (
<Box borderStyle="round" flexDirection="column" paddingX={1}> <Box borderStyle="round" flexDirection="column" paddingX={1}>
<Text bold color={theme.status.warning}> <Text bold color={theme.status.warning}>
Pro quota limit reached for {failedModel}. {t('Pro quota limit reached for {{model}}.', { model: failedModel })}
</Text> </Text>
<Box marginTop={1}> <Box marginTop={1}>
<RadioButtonSelect <RadioButtonSelect

View File

@@ -12,6 +12,7 @@ import {
type RadioSelectItem, type RadioSelectItem,
} from './shared/RadioButtonSelect.js'; } from './shared/RadioButtonSelect.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
export enum QuitChoice { export enum QuitChoice {
CANCEL = 'cancel', CANCEL = 'cancel',
@@ -39,22 +40,22 @@ export const QuitConfirmationDialog: React.FC<QuitConfirmationDialogProps> = ({
const options: Array<RadioSelectItem<QuitChoice>> = [ const options: Array<RadioSelectItem<QuitChoice>> = [
{ {
key: 'quit', key: 'quit',
label: 'Quit immediately (/quit)', label: t('Quit immediately (/quit)'),
value: QuitChoice.QUIT, value: QuitChoice.QUIT,
}, },
{ {
key: 'summary-and-quit', key: 'summary-and-quit',
label: 'Generate summary and quit (/summary)', label: t('Generate summary and quit (/summary)'),
value: QuitChoice.SUMMARY_AND_QUIT, value: QuitChoice.SUMMARY_AND_QUIT,
}, },
{ {
key: 'save-and-quit', key: 'save-and-quit',
label: 'Save conversation and quit (/chat save)', label: t('Save conversation and quit (/chat save)'),
value: QuitChoice.SAVE_AND_QUIT, value: QuitChoice.SAVE_AND_QUIT,
}, },
{ {
key: 'cancel', key: 'cancel',
label: 'Cancel (stay in application)', label: t('Cancel (stay in application)'),
value: QuitChoice.CANCEL, value: QuitChoice.CANCEL,
}, },
]; ];
@@ -69,7 +70,7 @@ export const QuitConfirmationDialog: React.FC<QuitConfirmationDialogProps> = ({
marginLeft={1} marginLeft={1}
> >
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
<Text>What would you like to do before exiting?</Text> <Text>{t('What would you like to do before exiting?')}</Text>
</Box> </Box>
<RadioButtonSelect items={options} onSelect={onSelect} isFocused /> <RadioButtonSelect items={options} onSelect={onSelect} isFocused />

View File

@@ -13,6 +13,7 @@ import qrcode from 'qrcode-terminal';
import { Colors } from '../colors.js'; import { Colors } from '../colors.js';
import type { DeviceAuthorizationData } from '@qwen-code/qwen-code-core'; import type { DeviceAuthorizationData } from '@qwen-code/qwen-code-core';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
interface QwenOAuthProgressProps { interface QwenOAuthProgressProps {
onTimeout: () => void; onTimeout: () => void;
@@ -52,11 +53,11 @@ function QrCodeDisplay({
width="100%" width="100%"
> >
<Text bold color={Colors.AccentBlue}> <Text bold color={Colors.AccentBlue}>
Qwen OAuth Authentication {t('Qwen OAuth Authentication')}
</Text> </Text>
<Box marginTop={1}> <Box marginTop={1}>
<Text>Please visit this URL to authorize:</Text> <Text>{t('Please visit this URL to authorize:')}</Text>
</Box> </Box>
<Link url={verificationUrl} fallback={false}> <Link url={verificationUrl} fallback={false}>
@@ -66,7 +67,7 @@ function QrCodeDisplay({
</Link> </Link>
<Box marginTop={1}> <Box marginTop={1}>
<Text>Or scan the QR code below:</Text> <Text>{t('Or scan the QR code below:')}</Text>
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>
@@ -103,15 +104,18 @@ function StatusDisplay({
> >
<Box marginTop={1}> <Box marginTop={1}>
<Text> <Text>
<Spinner type="dots" /> Waiting for authorization{dots} <Spinner type="dots" /> {t('Waiting for authorization')}
{dots}
</Text> </Text>
</Box> </Box>
<Box marginTop={1} justifyContent="space-between"> <Box marginTop={1} justifyContent="space-between">
<Text color={Colors.Gray}> <Text color={Colors.Gray}>
Time remaining: {formatTime(timeRemaining)} {t('Time remaining:')} {formatTime(timeRemaining)}
</Text>
<Text color={Colors.AccentPurple}>
{t('(Press ESC or CTRL+C to cancel)')}
</Text> </Text>
<Text color={Colors.AccentPurple}>(Press ESC or CTRL+C to cancel)</Text>
</Box> </Box>
</Box> </Box>
); );
@@ -215,19 +219,24 @@ export function QwenOAuthProgress({
width="100%" width="100%"
> >
<Text bold color={Colors.AccentRed}> <Text bold color={Colors.AccentRed}>
Qwen OAuth Authentication Timeout {t('Qwen OAuth Authentication Timeout')}
</Text> </Text>
<Box marginTop={1}> <Box marginTop={1}>
<Text> <Text>
{authMessage || {authMessage ||
`OAuth token expired (over ${defaultTimeout} seconds). Please select authentication method again.`} t(
'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.',
{
seconds: defaultTimeout.toString(),
},
)}
</Text> </Text>
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>
<Text color={Colors.Gray}> <Text color={Colors.Gray}>
Press any key to return to authentication type selection. {t('Press any key to return to authentication type selection.')}
</Text> </Text>
</Box> </Box>
</Box> </Box>
@@ -275,16 +284,17 @@ export function QwenOAuthProgress({
> >
<Box> <Box>
<Text> <Text>
<Spinner type="dots" /> Waiting for Qwen OAuth authentication... <Spinner type="dots" />
{t('Waiting for Qwen OAuth authentication...')}
</Text> </Text>
</Box> </Box>
<Box marginTop={1} justifyContent="space-between"> <Box marginTop={1} justifyContent="space-between">
<Text color={Colors.Gray}> <Text color={Colors.Gray}>
Time remaining: {Math.floor(timeRemaining / 60)}: {t('Time remaining:')} {Math.floor(timeRemaining / 60)}:
{(timeRemaining % 60).toString().padStart(2, '0')} {(timeRemaining % 60).toString().padStart(2, '0')}
</Text> </Text>
<Text color={Colors.AccentPurple}> <Text color={Colors.AccentPurple}>
(Press ESC or CTRL+C to cancel) {t('(Press ESC or CTRL+C to cancel)')}
</Text> </Text>
</Box> </Box>
</Box> </Box>

View File

@@ -6,6 +6,7 @@
import type React from 'react'; import type React from 'react';
import { StatsDisplay } from './StatsDisplay.js'; import { StatsDisplay } from './StatsDisplay.js';
import { t } from '../../i18n/index.js';
interface SessionSummaryDisplayProps { interface SessionSummaryDisplayProps {
duration: string; duration: string;
@@ -14,5 +15,8 @@ interface SessionSummaryDisplayProps {
export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({ export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
duration, duration,
}) => ( }) => (
<StatsDisplay title="Agent powering down. Goodbye!" duration={duration} /> <StatsDisplay
title={t('Agent powering down. Goodbye!')}
duration={duration}
/>
); );

View File

@@ -11,6 +11,7 @@ import type { LoadedSettings, Settings } from '../../config/settings.js';
import { SettingScope } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js';
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
import { ScopeSelector } from './shared/ScopeSelector.js'; import { ScopeSelector } from './shared/ScopeSelector.js';
import { t } from '../../i18n/index.js';
import { import {
getDialogSettingKeys, getDialogSettingKeys,
setPendingSettingValue, setPendingSettingValue,
@@ -124,7 +125,9 @@ export function SettingsDialog({
const definition = getSettingDefinition(key); const definition = getSettingDefinition(key);
return { return {
label: definition?.label || key, label: definition?.label
? t(definition.label) || definition.label
: key,
value: key, value: key,
type: definition?.type, type: definition?.type,
toggle: () => { toggle: () => {
@@ -779,7 +782,8 @@ export function SettingsDialog({
> >
<Box flexDirection="column" flexGrow={1}> <Box flexDirection="column" flexGrow={1}>
<Text bold={focusSection === 'settings'} wrap="truncate"> <Text bold={focusSection === 'settings'} wrap="truncate">
{focusSection === 'settings' ? '> ' : ' '}Settings {focusSection === 'settings' ? '> ' : ' '}
{t('Settings')}
</Text> </Text>
<Box height={1} /> <Box height={1} />
{showScrollUp && <Text color={theme.text.secondary}></Text>} {showScrollUp && <Text color={theme.text.secondary}></Text>}
@@ -916,13 +920,15 @@ export function SettingsDialog({
<Box height={1} /> <Box height={1} />
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
(Use Enter to select {t('(Use Enter to select{{tabText}})', {
{showScopeSelection ? ', Tab to change focus' : ''}) tabText: showScopeSelection ? t(', Tab to change focus') : '',
})}
</Text> </Text>
{showRestartPrompt && ( {showRestartPrompt && (
<Text color={theme.status.warning}> <Text color={theme.status.warning}>
To see changes, Qwen Code must be restarted. Press r to exit and {t(
apply changes now. 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.',
)}
</Text> </Text>
)} )}
</Box> </Box>

View File

@@ -12,6 +12,7 @@ import { RenderInline } from '../utils/InlineMarkdownRenderer.js';
import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
export interface ShellConfirmationRequest { export interface ShellConfirmationRequest {
commands: string[]; commands: string[];
@@ -51,17 +52,17 @@ export const ShellConfirmationDialog: React.FC<
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [ const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [
{ {
label: 'Yes, allow once', label: t('Yes, allow once'),
value: ToolConfirmationOutcome.ProceedOnce, value: ToolConfirmationOutcome.ProceedOnce,
key: 'Yes, allow once', key: 'Yes, allow once',
}, },
{ {
label: 'Yes, allow always for this session', label: t('Yes, allow always for this session'),
value: ToolConfirmationOutcome.ProceedAlways, value: ToolConfirmationOutcome.ProceedAlways,
key: 'Yes, allow always for this session', key: 'Yes, allow always for this session',
}, },
{ {
label: 'No (esc)', label: t('No (esc)'),
value: ToolConfirmationOutcome.Cancel, value: ToolConfirmationOutcome.Cancel,
key: 'No (esc)', key: 'No (esc)',
}, },
@@ -78,10 +79,10 @@ export const ShellConfirmationDialog: React.FC<
> >
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Shell Command Execution {t('Shell Command Execution')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
A custom command wants to run the following shell commands: {t('A custom command wants to run the following shell commands:')}
</Text> </Text>
<Box <Box
flexDirection="column" flexDirection="column"
@@ -99,7 +100,7 @@ export const ShellConfirmationDialog: React.FC<
</Box> </Box>
<Box marginBottom={1}> <Box marginBottom={1}>
<Text color={theme.text.primary}>Do you want to proceed?</Text> <Text color={theme.text.primary}>{t('Do you want to proceed?')}</Text>
</Box> </Box>
<RadioButtonSelect items={options} onSelect={handleSelect} isFocused /> <RadioButtonSelect items={options} onSelect={handleSelect} isFocused />

View File

@@ -19,6 +19,7 @@ import {
USER_AGREEMENT_RATE_MEDIUM, USER_AGREEMENT_RATE_MEDIUM,
} from '../utils/displayUtils.js'; } from '../utils/displayUtils.js';
import { computeSessionStats } from '../utils/computeStats.js'; import { computeSessionStats } from '../utils/computeStats.js';
import { t } from '../../i18n/index.js';
// A more flexible and powerful StatRow component // A more flexible and powerful StatRow component
interface StatRowProps { interface StatRowProps {
@@ -85,22 +86,22 @@ const ModelUsageTable: React.FC<{
<Box> <Box>
<Box width={nameWidth}> <Box width={nameWidth}>
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Model Usage {t('Model Usage')}
</Text> </Text>
</Box> </Box>
<Box width={requestsWidth} justifyContent="flex-end"> <Box width={requestsWidth} justifyContent="flex-end">
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Reqs {t('Reqs')}
</Text> </Text>
</Box> </Box>
<Box width={inputTokensWidth} justifyContent="flex-end"> <Box width={inputTokensWidth} justifyContent="flex-end">
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Input Tokens {t('Input Tokens')}
</Text> </Text>
</Box> </Box>
<Box width={outputTokensWidth} justifyContent="flex-end"> <Box width={outputTokensWidth} justifyContent="flex-end">
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Output Tokens {t('Output Tokens')}
</Text> </Text>
</Box> </Box>
</Box> </Box>
@@ -141,13 +142,14 @@ const ModelUsageTable: React.FC<{
{cacheEfficiency > 0 && ( {cacheEfficiency > 0 && (
<Box flexDirection="column" marginTop={1}> <Box flexDirection="column" marginTop={1}>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text color={theme.status.success}>Savings Highlight:</Text>{' '} <Text color={theme.status.success}>{t('Savings Highlight:')}</Text>{' '}
{totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)} {totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)}
%) of input tokens were served from the cache, reducing costs. %){' '}
{t('of input tokens were served from the cache, reducing costs.')}
</Text> </Text>
<Box height={1} /> <Box height={1} />
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
» Tip: For a full token breakdown, run `/stats model`. » {t('Tip: For a full token breakdown, run `/stats model`.')}
</Text> </Text>
</Box> </Box>
)} )}
@@ -199,7 +201,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
} }
return ( return (
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Session Stats {t('Session Stats')}
</Text> </Text>
); );
}; };
@@ -215,33 +217,33 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
{renderTitle()} {renderTitle()}
<Box height={1} /> <Box height={1} />
<Section title="Interaction Summary"> <Section title={t('Interaction Summary')}>
<StatRow title="Session ID:"> <StatRow title={t('Session ID:')}>
<Text color={theme.text.primary}>{stats.sessionId}</Text> <Text color={theme.text.primary}>{stats.sessionId}</Text>
</StatRow> </StatRow>
<StatRow title="Tool Calls:"> <StatRow title={t('Tool Calls:')}>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
{tools.totalCalls} ({' '} {tools.totalCalls} ({' '}
<Text color={theme.status.success}> {tools.totalSuccess}</Text>{' '} <Text color={theme.status.success}> {tools.totalSuccess}</Text>{' '}
<Text color={theme.status.error}>x {tools.totalFail}</Text> ) <Text color={theme.status.error}>x {tools.totalFail}</Text> )
</Text> </Text>
</StatRow> </StatRow>
<StatRow title="Success Rate:"> <StatRow title={t('Success Rate:')}>
<Text color={successColor}>{computed.successRate.toFixed(1)}%</Text> <Text color={successColor}>{computed.successRate.toFixed(1)}%</Text>
</StatRow> </StatRow>
{computed.totalDecisions > 0 && ( {computed.totalDecisions > 0 && (
<StatRow title="User Agreement:"> <StatRow title={t('User Agreement:')}>
<Text color={agreementColor}> <Text color={agreementColor}>
{computed.agreementRate.toFixed(1)}%{' '} {computed.agreementRate.toFixed(1)}%{' '}
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
({computed.totalDecisions} reviewed) ({computed.totalDecisions} {t('reviewed')})
</Text> </Text>
</Text> </Text>
</StatRow> </StatRow>
)} )}
{files && {files &&
(files.totalLinesAdded > 0 || files.totalLinesRemoved > 0) && ( (files.totalLinesAdded > 0 || files.totalLinesRemoved > 0) && (
<StatRow title="Code Changes:"> <StatRow title={t('Code Changes:')}>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
<Text color={theme.status.success}> <Text color={theme.status.success}>
+{files.totalLinesAdded} +{files.totalLinesAdded}
@@ -254,16 +256,16 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
)} )}
</Section> </Section>
<Section title="Performance"> <Section title={t('Performance')}>
<StatRow title="Wall Time:"> <StatRow title={t('Wall Time:')}>
<Text color={theme.text.primary}>{duration}</Text> <Text color={theme.text.primary}>{duration}</Text>
</StatRow> </StatRow>
<StatRow title="Agent Active:"> <StatRow title={t('Agent Active:')}>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
{formatDuration(computed.agentActiveTime)} {formatDuration(computed.agentActiveTime)}
</Text> </Text>
</StatRow> </StatRow>
<SubStatRow title="API Time:"> <SubStatRow title={t('API Time:')}>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
{formatDuration(computed.totalApiTime)}{' '} {formatDuration(computed.totalApiTime)}{' '}
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
@@ -271,7 +273,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
</Text> </Text>
</Text> </Text>
</SubStatRow> </SubStatRow>
<SubStatRow title="Tool Time:"> <SubStatRow title={t('Tool Time:')}>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
{formatDuration(computed.totalToolTime)}{' '} {formatDuration(computed.totalToolTime)}{' '}
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>

View File

@@ -17,6 +17,7 @@ import { SettingScope } from '../../config/settings.js';
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { ScopeSelector } from './shared/ScopeSelector.js'; import { ScopeSelector } from './shared/ScopeSelector.js';
import { t } from '../../i18n/index.js';
interface ThemeDialogProps { interface ThemeDialogProps {
/** Callback function when a theme is selected */ /** Callback function when a theme is selected */
@@ -198,7 +199,8 @@ export function ThemeDialog({
{/* Left Column: Selection */} {/* Left Column: Selection */}
<Box flexDirection="column" width="45%" paddingRight={2}> <Box flexDirection="column" width="45%" paddingRight={2}>
<Text bold={mode === 'theme'} wrap="truncate"> <Text bold={mode === 'theme'} wrap="truncate">
{mode === 'theme' ? '> ' : ' '}Select Theme{' '} {mode === 'theme' ? '> ' : ' '}
{t('Select Theme')}{' '}
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
{otherScopeModifiedMessage} {otherScopeModifiedMessage}
</Text> </Text>
@@ -218,7 +220,7 @@ export function ThemeDialog({
{/* Right Column: Preview */} {/* Right Column: Preview */}
<Box flexDirection="column" width="55%" paddingLeft={2}> <Box flexDirection="column" width="55%" paddingLeft={2}>
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Preview {t('Preview')}
</Text> </Text>
{/* Get the Theme object for the highlighted theme, fall back to default if not found */} {/* Get the Theme object for the highlighted theme, fall back to default if not found */}
{(() => { {(() => {
@@ -274,8 +276,9 @@ def fibonacci(n):
)} )}
<Box marginTop={1}> <Box marginTop={1}>
<Text color={theme.text.secondary} wrap="truncate"> <Text color={theme.text.secondary} wrap="truncate">
(Use Enter to {mode === 'theme' ? 'select' : 'apply scope'}, Tab to{' '} {mode === 'theme'
{mode === 'theme' ? 'configure scope' : 'select theme'}) ? t('(Use Enter to select, Tab to configure scope)')
: t('(Use Enter to apply scope, Tab to select theme)')}
</Text> </Text>
</Box> </Box>
</Box> </Box>

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { type Config } from '@qwen-code/qwen-code-core'; import { type Config } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
interface TipsProps { interface TipsProps {
config: Config; config: Config;
@@ -17,12 +18,12 @@ export const Tips: React.FC<TipsProps> = ({ config }) => {
const geminiMdFileCount = config.getGeminiMdFileCount(); const geminiMdFileCount = config.getGeminiMdFileCount();
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
<Text color={theme.text.primary}>Tips for getting started:</Text> <Text color={theme.text.primary}>{t('Tips for getting started:')}</Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
1. Ask questions, edit files, or run commands. {t('1. Ask questions, edit files, or run commands.')}
</Text> </Text>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
2. Be specific for the best results. {t('2. Be specific for the best results.')}
</Text> </Text>
{geminiMdFileCount === 0 && ( {geminiMdFileCount === 0 && (
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
@@ -30,7 +31,7 @@ export const Tips: React.FC<TipsProps> = ({ config }) => {
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
QWEN.md QWEN.md
</Text>{' '} </Text>{' '}
files to customize your interactions with Qwen Code. {t('files to customize your interactions with Qwen Code.')}
</Text> </Text>
)} )}
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
@@ -38,7 +39,7 @@ export const Tips: React.FC<TipsProps> = ({ config }) => {
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
/help /help
</Text>{' '} </Text>{' '}
for more information. {t('for more information.')}
</Text> </Text>
</Box> </Box>
); );

View File

@@ -17,6 +17,7 @@ import {
} from '../utils/displayUtils.js'; } from '../utils/displayUtils.js';
import { useSessionStats } from '../contexts/SessionContext.js'; import { useSessionStats } from '../contexts/SessionContext.js';
import type { ToolCallStats } from '@qwen-code/qwen-code-core'; import type { ToolCallStats } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
const TOOL_NAME_COL_WIDTH = 25; const TOOL_NAME_COL_WIDTH = 25;
const CALLS_COL_WIDTH = 8; const CALLS_COL_WIDTH = 8;
@@ -68,7 +69,7 @@ export const ToolStatsDisplay: React.FC = () => {
paddingX={2} paddingX={2}
> >
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
No tool calls have been made in this session. {t('No tool calls have been made in this session.')}
</Text> </Text>
</Box> </Box>
); );
@@ -103,7 +104,7 @@ export const ToolStatsDisplay: React.FC = () => {
width={70} width={70}
> >
<Text bold color={theme.text.accent}> <Text bold color={theme.text.accent}>
Tool Stats For Nerds {t('Tool Stats For Nerds')}
</Text> </Text>
<Box height={1} /> <Box height={1} />
@@ -111,22 +112,22 @@ export const ToolStatsDisplay: React.FC = () => {
<Box> <Box>
<Box width={TOOL_NAME_COL_WIDTH}> <Box width={TOOL_NAME_COL_WIDTH}>
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Tool Name {t('Tool Name')}
</Text> </Text>
</Box> </Box>
<Box width={CALLS_COL_WIDTH} justifyContent="flex-end"> <Box width={CALLS_COL_WIDTH} justifyContent="flex-end">
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Calls {t('Calls')}
</Text> </Text>
</Box> </Box>
<Box width={SUCCESS_RATE_COL_WIDTH} justifyContent="flex-end"> <Box width={SUCCESS_RATE_COL_WIDTH} justifyContent="flex-end">
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Success Rate {t('Success Rate')}
</Text> </Text>
</Box> </Box>
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end"> <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Avg Duration {t('Avg Duration')}
</Text> </Text>
</Box> </Box>
</Box> </Box>
@@ -151,13 +152,15 @@ export const ToolStatsDisplay: React.FC = () => {
{/* User Decision Summary */} {/* User Decision Summary */}
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
User Decision Summary {t('User Decision Summary')}
</Text> </Text>
<Box> <Box>
<Box <Box
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH} width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
> >
<Text color={theme.text.link}>Total Reviewed Suggestions:</Text> <Text color={theme.text.link}>
{t('Total Reviewed Suggestions:')}
</Text>
</Box> </Box>
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end"> <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
<Text color={theme.text.primary}>{totalReviewed}</Text> <Text color={theme.text.primary}>{totalReviewed}</Text>
@@ -167,7 +170,7 @@ export const ToolStatsDisplay: React.FC = () => {
<Box <Box
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH} width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
> >
<Text color={theme.text.primary}> » Accepted:</Text> <Text color={theme.text.primary}>{t(' » Accepted:')}</Text>
</Box> </Box>
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end"> <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
<Text color={theme.status.success}>{totalDecisions.accept}</Text> <Text color={theme.status.success}>{totalDecisions.accept}</Text>
@@ -177,7 +180,7 @@ export const ToolStatsDisplay: React.FC = () => {
<Box <Box
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH} width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
> >
<Text color={theme.text.primary}> » Rejected:</Text> <Text color={theme.text.primary}>{t(' » Rejected:')}</Text>
</Box> </Box>
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end"> <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
<Text color={theme.status.error}>{totalDecisions.reject}</Text> <Text color={theme.status.error}>{totalDecisions.reject}</Text>
@@ -187,7 +190,7 @@ export const ToolStatsDisplay: React.FC = () => {
<Box <Box
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH} width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
> >
<Text color={theme.text.primary}> » Modified:</Text> <Text color={theme.text.primary}>{t(' » Modified:')}</Text>
</Box> </Box>
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end"> <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
<Text color={theme.status.warning}>{totalDecisions.modify}</Text> <Text color={theme.status.warning}>{totalDecisions.modify}</Text>
@@ -209,7 +212,9 @@ export const ToolStatsDisplay: React.FC = () => {
<Box <Box
width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH} width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
> >
<Text color={theme.text.primary}> Overall Agreement Rate:</Text> <Text color={theme.text.primary}>
{t(' Overall Agreement Rate:')}
</Text>
</Box> </Box>
<Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end"> <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
<Text bold color={totalReviewed > 0 ? agreementColor : undefined}> <Text bold color={totalReviewed > 0 ? agreementColor : undefined}>

View File

@@ -12,6 +12,7 @@ import {
type RadioSelectItem, type RadioSelectItem,
} from './shared/RadioButtonSelect.js'; } from './shared/RadioButtonSelect.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { t } from '../../i18n/index.js';
interface WelcomeBackDialogProps { interface WelcomeBackDialogProps {
welcomeBackInfo: ProjectSummaryInfo; welcomeBackInfo: ProjectSummaryInfo;
@@ -36,12 +37,12 @@ export function WelcomeBackDialog({
const options: Array<RadioSelectItem<'restart' | 'continue'>> = [ const options: Array<RadioSelectItem<'restart' | 'continue'>> = [
{ {
key: 'restart', key: 'restart',
label: 'Start new chat session', label: t('Start new chat session'),
value: 'restart', value: 'restart',
}, },
{ {
key: 'continue', key: 'continue',
label: 'Continue previous conversation', label: t('Continue previous conversation'),
value: 'continue', value: 'continue',
}, },
]; ];
@@ -67,7 +68,9 @@ export function WelcomeBackDialog({
> >
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
<Text color={Colors.AccentBlue} bold> <Text color={Colors.AccentBlue} bold>
👋 Welcome back! (Last updated: {timeAgo}) {t('👋 Welcome back! (Last updated: {{timeAgo}})', {
timeAgo: timeAgo || '',
})}
</Text> </Text>
</Box> </Box>
@@ -75,7 +78,7 @@ export function WelcomeBackDialog({
{goalContent && ( {goalContent && (
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
<Text color={Colors.Foreground} bold> <Text color={Colors.Foreground} bold>
🎯 Overall Goal: {t('🎯 Overall Goal:')}
</Text> </Text>
<Box marginTop={1} paddingLeft={2}> <Box marginTop={1} paddingLeft={2}>
<Text color={Colors.Gray}>{goalContent}</Text> <Text color={Colors.Gray}>{goalContent}</Text>
@@ -87,19 +90,25 @@ export function WelcomeBackDialog({
{totalTasks > 0 && ( {totalTasks > 0 && (
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
<Text color={Colors.Foreground} bold> <Text color={Colors.Foreground} bold>
📋 Current Plan: 📋 {t('Current Plan:')}
</Text> </Text>
<Box marginTop={1} paddingLeft={2}> <Box marginTop={1} paddingLeft={2}>
<Text color={Colors.Gray}> <Text color={Colors.Gray}>
Progress: {doneCount}/{totalTasks} tasks completed {t('Progress: {{done}}/{{total}} tasks completed', {
{inProgressCount > 0 && `, ${inProgressCount} in progress`} done: String(doneCount),
total: String(totalTasks),
})}
{inProgressCount > 0 &&
t(', {{inProgress}} in progress', {
inProgress: String(inProgressCount),
})}
</Text> </Text>
</Box> </Box>
{pendingTasks.length > 0 && ( {pendingTasks.length > 0 && (
<Box flexDirection="column" marginTop={1} paddingLeft={2}> <Box flexDirection="column" marginTop={1} paddingLeft={2}>
<Text color={Colors.Foreground} bold> <Text color={Colors.Foreground} bold>
Pending Tasks: {t('Pending Tasks:')}
</Text> </Text>
{pendingTasks.map((task: string, index: number) => ( {pendingTasks.map((task: string, index: number) => (
<Text key={index} color={Colors.Gray}> <Text key={index} color={Colors.Gray}>
@@ -113,8 +122,8 @@ export function WelcomeBackDialog({
{/* Action Selection */} {/* Action Selection */}
<Box flexDirection="column" marginTop={1}> <Box flexDirection="column" marginTop={1}>
<Text bold>What would you like to do?</Text> <Text bold>{t('What would you like to do?')}</Text>
<Text>Choose how to proceed with your session:</Text> <Text>{t('Choose how to proceed with your session:')}</Text>
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>

View File

@@ -12,6 +12,8 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
│ │ │ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false │ │ Hide Window Title false │
@@ -20,8 +22,6 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -46,6 +46,8 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
│ │ │ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false │ │ Hide Window Title false │
@@ -54,8 +56,6 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -80,6 +80,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
│ │ │ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false │ │ Hide Window Title false │
@@ -88,8 +90,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -114,6 +114,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
│ │ │ │
│ Debug Keystroke Logging false* │ │ Debug Keystroke Logging false* │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false* │ │ Hide Window Title false* │
@@ -122,8 +124,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
│ │ │ │
│ Hide Tips false* │ │ Hide Tips false* │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -148,6 +148,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false │ │ Hide Window Title false │
@@ -156,8 +158,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -182,6 +182,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Debug Keystroke Logging (Modified in Workspace) false │ │ Debug Keystroke Logging (Modified in Workspace) false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false │ │ Hide Window Title false │
@@ -190,8 +192,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -216,6 +216,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
│ │ │ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false │ │ Hide Window Title false │
@@ -224,8 +226,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -250,6 +250,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
│ │ │ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false* │ │ Hide Window Title false* │
@@ -258,8 +260,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -284,6 +284,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
│ │ │ │
│ Debug Keystroke Logging false │ │ Debug Keystroke Logging false │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title false │ │ Hide Window Title false │
@@ -292,8 +294,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
│ │ │ │
│ Hide Tips false │ │ Hide Tips false │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │
@@ -318,6 +318,8 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
│ │ │ │
│ Debug Keystroke Logging true* │ │ Debug Keystroke Logging true* │
│ │ │ │
│ Language Auto (detect from system) │
│ │
│ Output Format Text │ │ Output Format Text │
│ │ │ │
│ Hide Window Title true* │ │ Hide Window Title true* │
@@ -326,8 +328,6 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
│ │ │ │
│ Hide Tips true* │ │ Hide Tips true* │
│ │ │ │
│ Hide Banner false │
│ │
│ ▼ │ │ ▼ │
│ │ │ │
│ │ │ │

View File

@@ -10,6 +10,7 @@ import Spinner from 'ink-spinner';
import { theme } from '../../semantic-colors.js'; import { theme } from '../../semantic-colors.js';
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js'; import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
import { CompressionStatus } from '@qwen-code/qwen-code-core'; import { CompressionStatus } from '@qwen-code/qwen-code-core';
import { t } from '../../../i18n/index.js';
export interface CompressionDisplayProps { export interface CompressionDisplayProps {
compression: CompressionProps; compression: CompressionProps;
@@ -30,22 +31,32 @@ export function CompressionMessage({
const getCompressionText = () => { const getCompressionText = () => {
if (isPending) { if (isPending) {
return 'Compressing chat history'; return t('Compressing chat history');
} }
switch (compressionStatus) { switch (compressionStatus) {
case CompressionStatus.COMPRESSED: case CompressionStatus.COMPRESSED:
return `Chat history compressed from ${originalTokens} to ${newTokens} tokens.`; return t(
'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.',
{
originalTokens: String(originalTokens),
newTokens: String(newTokens),
},
);
case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT: case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
// For smaller histories (< 50k tokens), compression overhead likely exceeds benefits // For smaller histories (< 50k tokens), compression overhead likely exceeds benefits
if (originalTokens < 50000) { if (originalTokens < 50000) {
return 'Compression was not beneficial for this history size.'; return t('Compression was not beneficial for this history size.');
} }
// For larger histories where compression should work but didn't, // For larger histories where compression should work but didn't,
// this suggests an issue with the compression process itself // this suggests an issue with the compression process itself
return 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.'; return t(
'Chat history compression did not reduce size. This may indicate issues with the compression prompt.',
);
case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR: case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR:
return 'Could not compress chat history due to a token counting error.'; return t(
'Could not compress chat history due to a token counting error.',
);
case CompressionStatus.NOOP: case CompressionStatus.NOOP:
return 'Nothing to compress.'; return 'Nothing to compress.';
default: default:

View File

@@ -24,6 +24,7 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { useKeypress } from '../../hooks/useKeypress.js'; import { useKeypress } from '../../hooks/useKeypress.js';
import { useSettings } from '../../contexts/SettingsContext.js'; import { useSettings } from '../../contexts/SettingsContext.js';
import { theme } from '../../semantic-colors.js'; import { theme } from '../../semantic-colors.js';
import { t } from '../../../i18n/index.js';
export interface ToolConfirmationMessageProps { export interface ToolConfirmationMessageProps {
confirmationDetails: ToolCallConfirmationDetails; confirmationDetails: ToolCallConfirmationDetails;
@@ -105,17 +106,17 @@ export const ToolConfirmationMessage: React.FC<
const compactOptions: Array<RadioSelectItem<ToolConfirmationOutcome>> = [ const compactOptions: Array<RadioSelectItem<ToolConfirmationOutcome>> = [
{ {
key: 'proceed-once', key: 'proceed-once',
label: 'Yes, allow once', label: t('Yes, allow once'),
value: ToolConfirmationOutcome.ProceedOnce, value: ToolConfirmationOutcome.ProceedOnce,
}, },
{ {
key: 'proceed-always', key: 'proceed-always',
label: 'Allow always', label: t('Allow always'),
value: ToolConfirmationOutcome.ProceedAlways, value: ToolConfirmationOutcome.ProceedAlways,
}, },
{ {
key: 'cancel', key: 'cancel',
label: 'No', label: t('No'),
value: ToolConfirmationOutcome.Cancel, value: ToolConfirmationOutcome.Cancel,
}, },
]; ];
@@ -123,7 +124,7 @@ export const ToolConfirmationMessage: React.FC<
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
<Box> <Box>
<Text wrap="truncate">Do you want to proceed?</Text> <Text wrap="truncate">{t('Do you want to proceed?')}</Text>
</Box> </Box>
<Box> <Box>
<RadioButtonSelect <RadioButtonSelect
@@ -185,37 +186,37 @@ export const ToolConfirmationMessage: React.FC<
padding={1} padding={1}
overflow="hidden" overflow="hidden"
> >
<Text color={theme.text.primary}>Modify in progress: </Text> <Text color={theme.text.primary}>{t('Modify in progress:')} </Text>
<Text color={theme.status.success}> <Text color={theme.status.success}>
Save and close external editor to continue {t('Save and close external editor to continue')}
</Text> </Text>
</Box> </Box>
); );
} }
question = `Apply this change?`; question = t('Apply this change?');
options.push({ options.push({
label: 'Yes, allow once', label: t('Yes, allow once'),
value: ToolConfirmationOutcome.ProceedOnce, value: ToolConfirmationOutcome.ProceedOnce,
key: 'Yes, allow once', key: 'Yes, allow once',
}); });
if (isTrustedFolder) { if (isTrustedFolder) {
options.push({ options.push({
label: 'Yes, allow always', label: t('Yes, allow always'),
value: ToolConfirmationOutcome.ProceedAlways, value: ToolConfirmationOutcome.ProceedAlways,
key: 'Yes, allow always', key: 'Yes, allow always',
}); });
} }
if ((!config.getIdeMode() || !isDiffingEnabled) && preferredEditor) { if ((!config.getIdeMode() || !isDiffingEnabled) && preferredEditor) {
options.push({ options.push({
label: 'Modify with external editor', label: t('Modify with external editor'),
value: ToolConfirmationOutcome.ModifyWithEditor, value: ToolConfirmationOutcome.ModifyWithEditor,
key: 'Modify with external editor', key: 'Modify with external editor',
}); });
} }
options.push({ options.push({
label: 'No, suggest changes (esc)', label: t('No, suggest changes (esc)'),
value: ToolConfirmationOutcome.Cancel, value: ToolConfirmationOutcome.Cancel,
key: 'No, suggest changes (esc)', key: 'No, suggest changes (esc)',
}); });
@@ -232,21 +233,23 @@ export const ToolConfirmationMessage: React.FC<
const executionProps = const executionProps =
confirmationDetails as ToolExecuteConfirmationDetails; confirmationDetails as ToolExecuteConfirmationDetails;
question = `Allow execution of: '${executionProps.rootCommand}'?`; question = t("Allow execution of: '{{command}}'?", {
command: executionProps.rootCommand,
});
options.push({ options.push({
label: 'Yes, allow once', label: t('Yes, allow once'),
value: ToolConfirmationOutcome.ProceedOnce, value: ToolConfirmationOutcome.ProceedOnce,
key: 'Yes, allow once', key: 'Yes, allow once',
}); });
if (isTrustedFolder) { if (isTrustedFolder) {
options.push({ options.push({
label: `Yes, allow always ...`, label: t('Yes, allow always ...'),
value: ToolConfirmationOutcome.ProceedAlways, value: ToolConfirmationOutcome.ProceedAlways,
key: `Yes, allow always ...`, key: 'Yes, allow always ...',
}); });
} }
options.push({ options.push({
label: 'No, suggest changes (esc)', label: t('No, suggest changes (esc)'),
value: ToolConfirmationOutcome.Cancel, value: ToolConfirmationOutcome.Cancel,
key: 'No, suggest changes (esc)', key: 'No, suggest changes (esc)',
}); });
@@ -275,17 +278,17 @@ export const ToolConfirmationMessage: React.FC<
question = planProps.title; question = planProps.title;
options.push({ options.push({
key: 'proceed-always', key: 'proceed-always',
label: 'Yes, and auto-accept edits', label: t('Yes, and auto-accept edits'),
value: ToolConfirmationOutcome.ProceedAlways, value: ToolConfirmationOutcome.ProceedAlways,
}); });
options.push({ options.push({
key: 'proceed-once', key: 'proceed-once',
label: 'Yes, and manually approve edits', label: t('Yes, and manually approve edits'),
value: ToolConfirmationOutcome.ProceedOnce, value: ToolConfirmationOutcome.ProceedOnce,
}); });
options.push({ options.push({
key: 'cancel', key: 'cancel',
label: 'No, keep planning (esc)', label: t('No, keep planning (esc)'),
value: ToolConfirmationOutcome.Cancel, value: ToolConfirmationOutcome.Cancel,
}); });
@@ -305,21 +308,21 @@ export const ToolConfirmationMessage: React.FC<
infoProps.urls && infoProps.urls &&
!(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt); !(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt);
question = `Do you want to proceed?`; question = t('Do you want to proceed?');
options.push({ options.push({
label: 'Yes, allow once', label: t('Yes, allow once'),
value: ToolConfirmationOutcome.ProceedOnce, value: ToolConfirmationOutcome.ProceedOnce,
key: 'Yes, allow once', key: 'Yes, allow once',
}); });
if (isTrustedFolder) { if (isTrustedFolder) {
options.push({ options.push({
label: 'Yes, allow always', label: t('Yes, allow always'),
value: ToolConfirmationOutcome.ProceedAlways, value: ToolConfirmationOutcome.ProceedAlways,
key: 'Yes, allow always', key: 'Yes, allow always',
}); });
} }
options.push({ options.push({
label: 'No, suggest changes (esc)', label: t('No, suggest changes (esc)'),
value: ToolConfirmationOutcome.Cancel, value: ToolConfirmationOutcome.Cancel,
key: 'No, suggest changes (esc)', key: 'No, suggest changes (esc)',
}); });
@@ -331,7 +334,7 @@ export const ToolConfirmationMessage: React.FC<
</Text> </Text>
{displayUrls && infoProps.urls && infoProps.urls.length > 0 && ( {displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
<Box flexDirection="column" marginTop={1}> <Box flexDirection="column" marginTop={1}>
<Text color={theme.text.primary}>URLs to fetch:</Text> <Text color={theme.text.primary}>{t('URLs to fetch:')}</Text>
{infoProps.urls.map((url) => ( {infoProps.urls.map((url) => (
<Text key={url}> <Text key={url}>
{' '} {' '}
@@ -348,31 +351,46 @@ export const ToolConfirmationMessage: React.FC<
bodyContent = ( bodyContent = (
<Box flexDirection="column" paddingX={1} marginLeft={1}> <Box flexDirection="column" paddingX={1} marginLeft={1}>
<Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text> <Text color={theme.text.link}>
<Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text> {t('MCP Server: {{server}}', { server: mcpProps.serverName })}
</Text>
<Text color={theme.text.link}>
{t('Tool: {{tool}}', { tool: mcpProps.toolName })}
</Text>
</Box> </Box>
); );
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`; question = t(
'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?',
{
tool: mcpProps.toolName,
server: mcpProps.serverName,
},
);
options.push({ options.push({
label: 'Yes, allow once', label: t('Yes, allow once'),
value: ToolConfirmationOutcome.ProceedOnce, value: ToolConfirmationOutcome.ProceedOnce,
key: 'Yes, allow once', key: 'Yes, allow once',
}); });
if (isTrustedFolder) { if (isTrustedFolder) {
options.push({ options.push({
label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`, label: t('Yes, always allow tool "{{tool}}" from server "{{server}}"', {
tool: mcpProps.toolName,
server: mcpProps.serverName,
}),
value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
key: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`, key: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
}); });
options.push({ options.push({
label: `Yes, always allow all tools from server "${mcpProps.serverName}"`, label: t('Yes, always allow all tools from server "{{server}}"', {
server: mcpProps.serverName,
}),
value: ToolConfirmationOutcome.ProceedAlwaysServer, value: ToolConfirmationOutcome.ProceedAlwaysServer,
key: `Yes, always allow all tools from server "${mcpProps.serverName}"`, key: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
}); });
} }
options.push({ options.push({
label: 'No, suggest changes (esc)', label: t('No, suggest changes (esc)'),
value: ToolConfirmationOutcome.Cancel, value: ToolConfirmationOutcome.Cancel,
key: 'No, suggest changes (esc)', key: 'No, suggest changes (esc)',
}); });

View File

@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
import type { SettingScope } from '../../../config/settings.js'; import type { SettingScope } from '../../../config/settings.js';
import { getScopeItems } from '../../../utils/dialogScopeUtils.js'; import { getScopeItems } from '../../../utils/dialogScopeUtils.js';
import { RadioButtonSelect } from './RadioButtonSelect.js'; import { RadioButtonSelect } from './RadioButtonSelect.js';
import { t } from '../../../i18n/index.js';
interface ScopeSelectorProps { interface ScopeSelectorProps {
/** Callback function when a scope is selected */ /** Callback function when a scope is selected */
@@ -29,6 +30,7 @@ export function ScopeSelector({
}: ScopeSelectorProps): React.JSX.Element { }: ScopeSelectorProps): React.JSX.Element {
const scopeItems = getScopeItems().map((item) => ({ const scopeItems = getScopeItems().map((item) => ({
...item, ...item,
label: t(item.label),
key: item.value, key: item.value,
})); }));
@@ -40,7 +42,8 @@ export function ScopeSelector({
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
<Text bold={isFocused} wrap="truncate"> <Text bold={isFocused} wrap="truncate">
{isFocused ? '> ' : ' '}Apply To {isFocused ? '> ' : ' '}
{t('Apply To')}
</Text> </Text>
<RadioButtonSelect <RadioButtonSelect
items={scopeItems} items={scopeItems}

View File

@@ -20,6 +20,7 @@ import type { Config } from '@qwen-code/qwen-code-core';
import { theme } from '../../../semantic-colors.js'; import { theme } from '../../../semantic-colors.js';
import { TextEntryStep } from './TextEntryStep.js'; import { TextEntryStep } from './TextEntryStep.js';
import { useKeypress } from '../../../hooks/useKeypress.js'; import { useKeypress } from '../../../hooks/useKeypress.js';
import { t } from '../../../../i18n/index.js';
interface AgentCreationWizardProps { interface AgentCreationWizardProps {
onClose: () => void; onClose: () => void;
@@ -90,25 +91,25 @@ export function AgentCreationWizard({
const n = state.currentStep; const n = state.currentStep;
switch (kind) { switch (kind) {
case 'LOCATION': case 'LOCATION':
return `Step ${n}: Choose Location`; return t('Step {{n}}: Choose Location', { n: n.toString() });
case 'GEN_METHOD': case 'GEN_METHOD':
return `Step ${n}: Choose Generation Method`; return t('Step {{n}}: Choose Generation Method', { n: n.toString() });
case 'LLM_DESC': case 'LLM_DESC':
return `Step ${n}: Describe Your Subagent`; return t('Step {{n}}: Describe Your Subagent', { n: n.toString() });
case 'MANUAL_NAME': case 'MANUAL_NAME':
return `Step ${n}: Enter Subagent Name`; return t('Step {{n}}: Enter Subagent Name', { n: n.toString() });
case 'MANUAL_PROMPT': case 'MANUAL_PROMPT':
return `Step ${n}: Enter System Prompt`; return t('Step {{n}}: Enter System Prompt', { n: n.toString() });
case 'MANUAL_DESC': case 'MANUAL_DESC':
return `Step ${n}: Enter Description`; return t('Step {{n}}: Enter Description', { n: n.toString() });
case 'TOOLS': case 'TOOLS':
return `Step ${n}: Select Tools`; return t('Step {{n}}: Select Tools', { n: n.toString() });
case 'COLOR': case 'COLOR':
return `Step ${n}: Choose Background Color`; return t('Step {{n}}: Choose Background Color', { n: n.toString() });
case 'FINAL': case 'FINAL':
return `Step ${n}: Confirm and Save`; return t('Step {{n}}: Confirm and Save', { n: n.toString() });
default: default:
return 'Unknown Step'; return t('Unknown Step');
} }
}; };
@@ -163,11 +164,11 @@ export function AgentCreationWizard({
// Special case: During generation in description input step, only show cancel option // Special case: During generation in description input step, only show cancel option
const kind = getStepKind(state.generationMethod, state.currentStep); const kind = getStepKind(state.generationMethod, state.currentStep);
if (kind === 'LLM_DESC' && state.isGenerating) { if (kind === 'LLM_DESC' && state.isGenerating) {
return 'Esc to cancel'; return t('Esc to cancel');
} }
if (getStepKind(state.generationMethod, state.currentStep) === 'FINAL') { if (getStepKind(state.generationMethod, state.currentStep) === 'FINAL') {
return 'Press Enter to save, e to save and edit, Esc to go back'; return t('Press Enter to save, e to save and edit, Esc to go back');
} }
// Steps that have ↑↓ navigation (RadioButtonSelect components) // Steps that have ↑↓ navigation (RadioButtonSelect components)
@@ -177,14 +178,17 @@ export function AgentCreationWizard({
kindForNav === 'GEN_METHOD' || kindForNav === 'GEN_METHOD' ||
kindForNav === 'TOOLS' || kindForNav === 'TOOLS' ||
kindForNav === 'COLOR'; kindForNav === 'COLOR';
const navigationPart = hasNavigation ? '↑↓ to navigate, ' : ''; const navigationPart = hasNavigation ? t('↑↓ to navigate, ') : '';
const escAction = const escAction =
state.currentStep === WIZARD_STEPS.LOCATION_SELECTION state.currentStep === WIZARD_STEPS.LOCATION_SELECTION
? 'cancel' ? t('cancel')
: 'go back'; : t('go back');
return `Press Enter to continue, ${navigationPart}Esc to ${escAction}`; return t('Press Enter to continue, {{navigation}}Esc to {{action}}', {
navigation: navigationPart,
action: escAction,
});
}; };
return ( return (
@@ -210,16 +214,16 @@ export function AgentCreationWizard({
state={state} state={state}
dispatch={dispatch} dispatch={dispatch}
onNext={handleNext} onNext={handleNext}
description="Enter a clear, unique name for this subagent." description={t('Enter a clear, unique name for this subagent.')}
placeholder="e.g., Code Reviewer" placeholder={t('e.g., Code Reviewer')}
height={1} height={1}
initialText={state.generatedName} initialText={state.generatedName}
onChange={(t) => { onChange={(text) => {
const value = t; // keep raw, trim later when validating const value = text; // keep raw, trim later when validating
dispatch({ type: 'SET_GENERATED_NAME', name: value }); dispatch({ type: 'SET_GENERATED_NAME', name: value });
}} }}
validate={(t) => validate={(text) =>
t.trim().length === 0 ? 'Name cannot be empty.' : null text.trim().length === 0 ? t('Name cannot be empty.') : null
} }
/> />
); );
@@ -230,18 +234,22 @@ export function AgentCreationWizard({
state={state} state={state}
dispatch={dispatch} dispatch={dispatch}
onNext={handleNext} onNext={handleNext}
description="Write the system prompt that defines this subagent's behavior. Be comprehensive for best results." description={t(
placeholder="e.g., You are an expert code reviewer..." "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.",
)}
placeholder={t('e.g., You are an expert code reviewer...')}
height={10} height={10}
initialText={state.generatedSystemPrompt} initialText={state.generatedSystemPrompt}
onChange={(t) => { onChange={(text) => {
dispatch({ dispatch({
type: 'SET_GENERATED_SYSTEM_PROMPT', type: 'SET_GENERATED_SYSTEM_PROMPT',
systemPrompt: t, systemPrompt: text,
}); });
}} }}
validate={(t) => validate={(text) =>
t.trim().length === 0 ? 'System prompt cannot be empty.' : null text.trim().length === 0
? t('System prompt cannot be empty.')
: null
} }
/> />
); );
@@ -252,15 +260,24 @@ export function AgentCreationWizard({
state={state} state={state}
dispatch={dispatch} dispatch={dispatch}
onNext={handleNext} onNext={handleNext}
description="Describe when and how this subagent should be used." description={t(
placeholder="e.g., Reviews code for best practices and potential bugs." 'Describe when and how this subagent should be used.',
)}
placeholder={t(
'e.g., Reviews code for best practices and potential bugs.',
)}
height={6} height={6}
initialText={state.generatedDescription} initialText={state.generatedDescription}
onChange={(t) => { onChange={(text) => {
dispatch({ type: 'SET_GENERATED_DESCRIPTION', description: t }); dispatch({
type: 'SET_GENERATED_DESCRIPTION',
description: text,
});
}} }}
validate={(t) => validate={(text) =>
t.trim().length === 0 ? 'Description cannot be empty.' : null text.trim().length === 0
? t('Description cannot be empty.')
: null
} }
/> />
); );
@@ -292,7 +309,9 @@ export function AgentCreationWizard({
return ( return (
<Box> <Box>
<Text color={theme.status.error}> <Text color={theme.status.error}>
Invalid step: {state.currentStep} {t('Invalid step: {{step}}', {
step: state.currentStep.toString(),
})}
</Text> </Text>
</Box> </Box>
); );

View File

@@ -15,6 +15,7 @@ import { theme } from '../../../semantic-colors.js';
import { shouldShowColor, getColorForDisplay } from '../utils.js'; import { shouldShowColor, getColorForDisplay } from '../utils.js';
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js'; import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
import { useKeypress } from '../../../hooks/useKeypress.js'; import { useKeypress } from '../../../hooks/useKeypress.js';
import { t } from '../../../../i18n/index.js';
/** /**
* Step 6: Final confirmation and actions. * Step 6: Final confirmation and actions.
@@ -62,15 +63,24 @@ export function CreationSummary({
if (conflictLevel === targetLevel) { if (conflictLevel === targetLevel) {
allWarnings.push( allWarnings.push(
`Name "${state.generatedName}" already exists at ${conflictLevel} level - will overwrite existing subagent`, t(
'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent',
{ name: state.generatedName, level: conflictLevel },
),
); );
} else if (targetLevel === 'project') { } else if (targetLevel === 'project') {
allWarnings.push( allWarnings.push(
`Name "${state.generatedName}" exists at user level - project level will take precedence`, t(
'Name "{{name}}" exists at user level - project level will take precedence',
{ name: state.generatedName },
),
); );
} else { } else {
allWarnings.push( allWarnings.push(
`Name "${state.generatedName}" exists at project level - existing subagent will take precedence`, t(
'Name "{{name}}" exists at project level - existing subagent will take precedence',
{ name: state.generatedName },
),
); );
} }
} }
@@ -83,12 +93,16 @@ export function CreationSummary({
// Check length warnings // Check length warnings
if (state.generatedDescription.length > 300) { if (state.generatedDescription.length > 300) {
allWarnings.push( allWarnings.push(
`Description is over ${state.generatedDescription.length} characters`, t('Description is over {{length}} characters', {
length: state.generatedDescription.length.toString(),
}),
); );
} }
if (state.generatedSystemPrompt.length > 10000) { if (state.generatedSystemPrompt.length > 10000) {
allWarnings.push( allWarnings.push(
`System prompt is over ${state.generatedSystemPrompt.length} characters`, t('System prompt is over {{length}} characters', {
length: state.generatedSystemPrompt.length.toString(),
}),
); );
} }
@@ -181,7 +195,9 @@ export function CreationSummary({
showSuccessAndClose(); showSuccessAndClose();
} catch (error) { } catch (error) {
setSaveError( setSaveError(
`Failed to save and edit subagent: ${error instanceof Error ? error.message : 'Unknown error'}`, t('Failed to save and edit subagent: {{error}}', {
error: error instanceof Error ? error.message : 'Unknown error',
}),
); );
} }
}, [ }, [
@@ -215,13 +231,15 @@ export function CreationSummary({
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Box> <Box>
<Text bold color={theme.status.success}> <Text bold color={theme.status.success}>
Subagent Created Successfully! {t('✅ Subagent Created Successfully!')}
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Text> <Text>
Subagent &quot;{state.generatedName}&quot; has been saved to{' '} {t('Subagent "{{name}}" has been saved to {{level}} level.', {
{state.location} level. name: state.generatedName,
level: state.location,
})}
</Text> </Text>
</Box> </Box>
</Box> </Box>
@@ -232,35 +250,35 @@ export function CreationSummary({
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Box flexDirection="column"> <Box flexDirection="column">
<Box> <Box>
<Text color={theme.text.primary}>Name: </Text> <Text color={theme.text.primary}>{t('Name: ')}</Text>
<Text color={getColorForDisplay(state.color)}> <Text color={getColorForDisplay(state.color)}>
{state.generatedName} {state.generatedName}
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Text color={theme.text.primary}>Location: </Text> <Text color={theme.text.primary}>{t('Location: ')}</Text>
<Text> <Text>
{state.location === 'project' {state.location === 'project'
? 'Project Level (.qwen/agents/)' ? t('Project Level (.qwen/agents/)')
: 'User Level (~/.qwen/agents/)'} : t('User Level (~/.qwen/agents/)')}
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Text color={theme.text.primary}>Tools: </Text> <Text color={theme.text.primary}>{t('Tools: ')}</Text>
<Text>{toolsDisplay}</Text> <Text>{toolsDisplay}</Text>
</Box> </Box>
{shouldShowColor(state.color) && ( {shouldShowColor(state.color) && (
<Box> <Box>
<Text color={theme.text.primary}>Color: </Text> <Text color={theme.text.primary}>{t('Color: ')}</Text>
<Text color={getColorForDisplay(state.color)}>{state.color}</Text> <Text color={getColorForDisplay(state.color)}>{state.color}</Text>
</Box> </Box>
)} )}
<Box marginTop={1}> <Box marginTop={1}>
<Text color={theme.text.primary}>Description:</Text> <Text color={theme.text.primary}>{t('Description:')}</Text>
</Box> </Box>
<Box padding={1} paddingBottom={0}> <Box padding={1} paddingBottom={0}>
<Text wrap="wrap"> <Text wrap="wrap">
@@ -269,7 +287,7 @@ export function CreationSummary({
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>
<Text color={theme.text.primary}>System Prompt:</Text> <Text color={theme.text.primary}>{t('System Prompt:')}</Text>
</Box> </Box>
<Box padding={1} paddingBottom={0}> <Box padding={1} paddingBottom={0}>
<Text wrap="wrap"> <Text wrap="wrap">
@@ -281,7 +299,7 @@ export function CreationSummary({
{saveError && ( {saveError && (
<Box flexDirection="column"> <Box flexDirection="column">
<Text bold color={theme.status.error}> <Text bold color={theme.status.error}>
Error saving subagent: {t('❌ Error saving subagent:')}
</Text> </Text>
<Box flexDirection="column" padding={1} paddingBottom={0}> <Box flexDirection="column" padding={1} paddingBottom={0}>
<Text color={theme.status.error} wrap="wrap"> <Text color={theme.status.error} wrap="wrap">
@@ -294,7 +312,7 @@ export function CreationSummary({
{warnings.length > 0 && ( {warnings.length > 0 && (
<Box flexDirection="column"> <Box flexDirection="column">
<Text bold color={theme.status.warning}> <Text bold color={theme.status.warning}>
Warnings: {t('Warnings:')}
</Text> </Text>
<Box flexDirection="column" padding={1} paddingBottom={0}> <Box flexDirection="column" padding={1} paddingBottom={0}>
{warnings.map((warning, index) => ( {warnings.map((warning, index) => (

View File

@@ -14,6 +14,7 @@ import { useKeypress, type Key } from '../../../hooks/useKeypress.js';
import { keyMatchers, Command } from '../../../keyMatchers.js'; import { keyMatchers, Command } from '../../../keyMatchers.js';
import { theme } from '../../../semantic-colors.js'; import { theme } from '../../../semantic-colors.js';
import { TextInput } from '../../shared/TextInput.js'; import { TextInput } from '../../shared/TextInput.js';
import { t } from '../../../../i18n/index.js';
/** /**
* Step 3: Description input with LLM generation. * Step 3: Description input with LLM generation.
@@ -103,7 +104,9 @@ export function DescriptionInput({
dispatch({ dispatch({
type: 'SET_VALIDATION_ERRORS', type: 'SET_VALIDATION_ERRORS',
errors: [ errors: [
`Failed to generate subagent: ${error instanceof Error ? error.message : 'Unknown error'}`, t('Failed to generate subagent: {{error}}', {
error: error instanceof Error ? error.message : 'Unknown error',
}),
], ],
}); });
} }
@@ -135,15 +138,17 @@ export function DescriptionInput({
isActive: state.isGenerating, isActive: state.isGenerating,
}); });
const placeholder = const placeholder = t(
'e.g., Expert code reviewer that reviews code based on best practices...'; 'e.g., Expert code reviewer that reviews code based on best practices...',
);
return ( return (
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Box> <Box>
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Describe what this subagent should do and when it should be used. (Be {t(
comprehensive for best results) 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)',
)}
</Text> </Text>
</Box> </Box>
@@ -153,7 +158,7 @@ export function DescriptionInput({
<Spinner /> <Spinner />
</Box> </Box>
<Text color={theme.text.accent}> <Text color={theme.text.accent}>
Generating subagent configuration... {t('Generating subagent configuration...')}
</Text> </Text>
</Box> </Box>
) : ( ) : (

View File

@@ -7,6 +7,7 @@
import { Box } from 'ink'; import { Box } from 'ink';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import type { WizardStepProps } from '../types.js'; import type { WizardStepProps } from '../types.js';
import { t } from '../../../../i18n/index.js';
interface GenerationOption { interface GenerationOption {
label: string; label: string;
@@ -15,11 +16,15 @@ interface GenerationOption {
const generationOptions: GenerationOption[] = [ const generationOptions: GenerationOption[] = [
{ {
label: 'Generate with Qwen Code (Recommended)', get label() {
return t('Generate with Qwen Code (Recommended)');
},
value: 'qwen', value: 'qwen',
}, },
{ {
label: 'Manual Creation', get label() {
return t('Manual Creation');
},
value: 'manual', value: 'manual',
}, },
]; ];

View File

@@ -7,6 +7,7 @@
import { Box } from 'ink'; import { Box } from 'ink';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import type { WizardStepProps } from '../types.js'; import type { WizardStepProps } from '../types.js';
import { t } from '../../../../i18n/index.js';
interface LocationOption { interface LocationOption {
label: string; label: string;
@@ -15,11 +16,15 @@ interface LocationOption {
const locationOptions: LocationOption[] = [ const locationOptions: LocationOption[] = [
{ {
label: 'Project Level (.qwen/agents/)', get label() {
return t('Project Level (.qwen/agents/)');
},
value: 'project', value: 'project',
}, },
{ {
label: 'User Level (~/.qwen/agents/)', get label() {
return t('User Level (~/.qwen/agents/)');
},
value: 'user', value: 'user',
}, },
]; ];

View File

@@ -10,6 +10,7 @@ import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import type { ToolCategory } from '../types.js'; import type { ToolCategory } from '../types.js';
import { Kind, type Config } from '@qwen-code/qwen-code-core'; import { Kind, type Config } from '@qwen-code/qwen-code-core';
import { theme } from '../../../semantic-colors.js'; import { theme } from '../../../semantic-colors.js';
import { t } from '../../../../i18n/index.js';
interface ToolOption { interface ToolOption {
label: string; label: string;
@@ -45,7 +46,7 @@ export function ToolSelector({
toolCategories: [ toolCategories: [
{ {
id: 'all', id: 'all',
name: 'All Tools (Default)', name: t('All Tools (Default)'),
tools: [], tools: [],
}, },
], ],
@@ -89,22 +90,22 @@ export function ToolSelector({
const toolCategories = [ const toolCategories = [
{ {
id: 'all', id: 'all',
name: 'All Tools', name: t('All Tools'),
tools: [], tools: [],
}, },
{ {
id: 'read', id: 'read',
name: 'Read-only Tools', name: t('Read-only Tools'),
tools: readTools, tools: readTools,
}, },
{ {
id: 'edit', id: 'edit',
name: 'Read & Edit Tools', name: t('Read & Edit Tools'),
tools: [...readTools, ...editTools], tools: [...readTools, ...editTools],
}, },
{ {
id: 'execute', id: 'execute',
name: 'Read & Edit & Execution Tools', name: t('Read & Edit & Execution Tools'),
tools: [...readTools, ...editTools, ...executeTools], tools: [...readTools, ...editTools, ...executeTools],
}, },
].filter((category) => category.id === 'all' || category.tools.length > 0); ].filter((category) => category.id === 'all' || category.tools.length > 0);
@@ -202,11 +203,11 @@ export function ToolSelector({
<Box flexDirection="column"> <Box flexDirection="column">
{currentCategory.id === 'all' ? ( {currentCategory.id === 'all' ? (
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
All tools selected, including MCP tools {t('All tools selected, including MCP tools')}
</Text> </Text>
) : currentCategory.tools.length > 0 ? ( ) : currentCategory.tools.length > 0 ? (
<> <>
<Text color={theme.text.secondary}>Selected tools:</Text> <Text color={theme.text.secondary}>{t('Selected tools:')}</Text>
<Box flexDirection="column" marginLeft={2}> <Box flexDirection="column" marginLeft={2}>
{(() => { {(() => {
// Filter the already categorized tools to show only those in current category // Filter the already categorized tools to show only those in current category
@@ -224,17 +225,19 @@ export function ToolSelector({
<> <>
{categoryReadTools.length > 0 && ( {categoryReadTools.length > 0 && (
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Read-only tools: {categoryReadTools.join(', ')} {t('Read-only tools:')}{' '}
{categoryReadTools.join(', ')}
</Text> </Text>
)} )}
{categoryEditTools.length > 0 && ( {categoryEditTools.length > 0 && (
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Edit tools: {categoryEditTools.join(', ')} {t('Edit tools:')} {categoryEditTools.join(', ')}
</Text> </Text>
)} )}
{categoryExecuteTools.length > 0 && ( {categoryExecuteTools.length > 0 && (
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Execution tools: {categoryExecuteTools.join(', ')} {t('Execution tools:')}{' '}
{categoryExecuteTools.join(', ')}
</Text> </Text>
)} )}
</> </>

View File

@@ -9,6 +9,7 @@ import { Box } from 'ink';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { MANAGEMENT_STEPS } from '../types.js'; import { MANAGEMENT_STEPS } from '../types.js';
import { type SubagentConfig } from '@qwen-code/qwen-code-core'; import { type SubagentConfig } from '@qwen-code/qwen-code-core';
import { t } from '../../../../i18n/index.js';
interface ActionSelectionStepProps { interface ActionSelectionStepProps {
selectedAgent: SubagentConfig | null; selectedAgent: SubagentConfig | null;
@@ -27,10 +28,34 @@ export const ActionSelectionStep = ({
// Filter actions based on whether the agent is built-in // Filter actions based on whether the agent is built-in
const allActions = [ const allActions = [
{ key: 'view', label: 'View Agent', value: 'view' as const }, {
{ key: 'edit', label: 'Edit Agent', value: 'edit' as const }, key: 'view',
{ key: 'delete', label: 'Delete Agent', value: 'delete' as const }, get label() {
{ key: 'back', label: 'Back', value: 'back' as const }, return t('View Agent');
},
value: 'view' as const,
},
{
key: 'edit',
get label() {
return t('Edit Agent');
},
value: 'edit' as const,
},
{
key: 'delete',
get label() {
return t('Delete Agent');
},
value: 'delete' as const,
},
{
key: 'back',
get label() {
return t('Back');
},
value: 'back' as const,
},
]; ];
const actions = selectedAgent?.isBuiltin const actions = selectedAgent?.isBuiltin

View File

@@ -9,6 +9,7 @@ import { type SubagentConfig } from '@qwen-code/qwen-code-core';
import type { StepNavigationProps } from '../types.js'; import type { StepNavigationProps } from '../types.js';
import { theme } from '../../../semantic-colors.js'; import { theme } from '../../../semantic-colors.js';
import { useKeypress } from '../../../hooks/useKeypress.js'; import { useKeypress } from '../../../hooks/useKeypress.js';
import { t } from '../../../../i18n/index.js';
interface AgentDeleteStepProps extends StepNavigationProps { interface AgentDeleteStepProps extends StepNavigationProps {
selectedAgent: SubagentConfig | null; selectedAgent: SubagentConfig | null;
@@ -41,7 +42,7 @@ export function AgentDeleteStep({
if (!selectedAgent) { if (!selectedAgent) {
return ( return (
<Box> <Box>
<Text color={theme.status.error}>No agent selected</Text> <Text color={theme.status.error}>{t('No agent selected')}</Text>
</Box> </Box>
); );
} }
@@ -49,8 +50,9 @@ export function AgentDeleteStep({
return ( return (
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Text color={theme.status.error}> <Text color={theme.status.error}>
Are you sure you want to delete agent &ldquo;{selectedAgent.name} {t('Are you sure you want to delete agent "{{name}}"?', {
&rdquo;? name: selectedAgent.name,
})}
</Text> </Text>
</Box> </Box>
); );

View File

@@ -11,6 +11,7 @@ import { MANAGEMENT_STEPS } from '../types.js';
import { theme } from '../../../semantic-colors.js'; import { theme } from '../../../semantic-colors.js';
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js'; import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
import { type SubagentConfig } from '@qwen-code/qwen-code-core'; import { type SubagentConfig } from '@qwen-code/qwen-code-core';
import { t } from '../../../../i18n/index.js';
interface EditOption { interface EditOption {
id: string; id: string;
@@ -20,15 +21,21 @@ interface EditOption {
const editOptions: EditOption[] = [ const editOptions: EditOption[] = [
{ {
id: 'editor', id: 'editor',
label: 'Open in editor', get label() {
return t('Open in editor');
},
}, },
{ {
id: 'tools', id: 'tools',
label: 'Edit tools', get label() {
return t('Edit tools');
},
}, },
{ {
id: 'color', id: 'color',
label: 'Edit color', get label() {
return t('Edit color');
},
}, },
]; ];
@@ -65,7 +72,9 @@ export function EditOptionsStep({
await launchEditor(selectedAgent?.filePath); await launchEditor(selectedAgent?.filePath);
} catch (err) { } catch (err) {
setError( setError(
`Failed to launch editor: ${err instanceof Error ? err.message : 'Unknown error'}`, t('Failed to launch editor: {{error}}', {
error: err instanceof Error ? err.message : 'Unknown error',
}),
); );
} }
} else if (selectedValue === 'tools') { } else if (selectedValue === 'tools') {
@@ -98,7 +107,7 @@ export function EditOptionsStep({
{error && ( {error && (
<Box flexDirection="column"> <Box flexDirection="column">
<Text bold color={theme.status.error}> <Text bold color={theme.status.error}>
Error: {t('❌ Error:')}
</Text> </Text>
<Box flexDirection="column" padding={1} paddingBottom={0}> <Box flexDirection="column" padding={1} paddingBottom={0}>
<Text color={theme.status.error} wrap="wrap"> <Text color={theme.status.error} wrap="wrap">

View File

@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
import { theme } from '../../../semantic-colors.js'; import { theme } from '../../../semantic-colors.js';
import { useKeypress } from '../../../hooks/useKeypress.js'; import { useKeypress } from '../../../hooks/useKeypress.js';
import { type SubagentConfig } from '@qwen-code/qwen-code-core'; import { type SubagentConfig } from '@qwen-code/qwen-code-core';
import { t } from '../../../../i18n/index.js';
interface NavigationState { interface NavigationState {
currentBlock: 'project' | 'user' | 'builtin'; currentBlock: 'project' | 'user' | 'builtin';
@@ -205,9 +206,9 @@ export const AgentSelectionStep = ({
if (availableAgents.length === 0) { if (availableAgents.length === 0) {
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
<Text color={theme.text.secondary}>No subagents found.</Text> <Text color={theme.text.secondary}>{t('No subagents found.')}</Text>
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Use &apos;/agents create&apos; to create your first subagent. {t("Use '/agents create' to create your first subagent.")}
</Text> </Text>
</Box> </Box>
); );
@@ -237,7 +238,7 @@ export const AgentSelectionStep = ({
{agent.isBuiltin && ( {agent.isBuiltin && (
<Text color={isSelected ? theme.text.accent : theme.text.secondary}> <Text color={isSelected ? theme.text.accent : theme.text.secondary}>
{' '} {' '}
(built-in) {t('(built-in)')}
</Text> </Text>
)} )}
{agent.level === 'user' && projectNames.has(agent.name) && ( {agent.level === 'user' && projectNames.has(agent.name) && (
@@ -245,7 +246,7 @@ export const AgentSelectionStep = ({
color={isSelected ? theme.status.warning : theme.text.secondary} color={isSelected ? theme.status.warning : theme.text.secondary}
> >
{' '} {' '}
(overridden by project level agent) {t('(overridden by project level agent)')}
</Text> </Text>
)} )}
</Text> </Text>
@@ -265,7 +266,9 @@ export const AgentSelectionStep = ({
{projectAgents.length > 0 && ( {projectAgents.length > 0 && (
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
<Text color={theme.text.primary} bold> <Text color={theme.text.primary} bold>
Project Level ({projectAgents[0].filePath.replace(/\/[^/]+$/, '')}) {t('Project Level ({{path}})', {
path: projectAgents[0].filePath.replace(/\/[^/]+$/, ''),
})}
</Text> </Text>
<Box marginTop={1} flexDirection="column"> <Box marginTop={1} flexDirection="column">
{projectAgents.map((agent, index) => { {projectAgents.map((agent, index) => {
@@ -285,7 +288,9 @@ export const AgentSelectionStep = ({
marginBottom={builtinAgents.length > 0 ? 1 : 0} marginBottom={builtinAgents.length > 0 ? 1 : 0}
> >
<Text color={theme.text.primary} bold> <Text color={theme.text.primary} bold>
User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')}) {t('User Level ({{path}})', {
path: userAgents[0].filePath.replace(/\/[^/]+$/, ''),
})}
</Text> </Text>
<Box marginTop={1} flexDirection="column"> <Box marginTop={1} flexDirection="column">
{userAgents.map((agent, index) => { {userAgents.map((agent, index) => {
@@ -302,7 +307,7 @@ export const AgentSelectionStep = ({
{builtinAgents.length > 0 && ( {builtinAgents.length > 0 && (
<Box flexDirection="column"> <Box flexDirection="column">
<Text color={theme.text.primary} bold> <Text color={theme.text.primary} bold>
Built-in Agents {t('Built-in Agents')}
</Text> </Text>
<Box marginTop={1} flexDirection="column"> <Box marginTop={1} flexDirection="column">
{builtinAgents.map((agent, index) => { {builtinAgents.map((agent, index) => {
@@ -321,7 +326,9 @@ export const AgentSelectionStep = ({
builtinAgents.length > 0) && ( builtinAgents.length > 0) && (
<Box marginTop={1}> <Box marginTop={1}>
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Using: {enabledAgentsCount} agents {t('Using: {{count}} agents', {
count: enabledAgentsCount.toString(),
})}
</Text> </Text>
</Box> </Box>
)} )}

View File

@@ -8,6 +8,7 @@ import { Box, Text } from 'ink';
import { theme } from '../../../semantic-colors.js'; import { theme } from '../../../semantic-colors.js';
import { shouldShowColor, getColorForDisplay } from '../utils.js'; import { shouldShowColor, getColorForDisplay } from '../utils.js';
import { type SubagentConfig } from '@qwen-code/qwen-code-core'; import { type SubagentConfig } from '@qwen-code/qwen-code-core';
import { t } from '../../../../i18n/index.js';
interface AgentViewerStepProps { interface AgentViewerStepProps {
selectedAgent: SubagentConfig | null; selectedAgent: SubagentConfig | null;
@@ -17,7 +18,7 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
if (!selectedAgent) { if (!selectedAgent) {
return ( return (
<Box> <Box>
<Text color={theme.status.error}>No agent selected</Text> <Text color={theme.status.error}>{t('No agent selected')}</Text>
</Box> </Box>
); );
} }
@@ -30,31 +31,31 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Box flexDirection="column"> <Box flexDirection="column">
<Box> <Box>
<Text color={theme.text.primary}>File Path: </Text> <Text color={theme.text.primary}>{t('File Path: ')}</Text>
<Text>{agent.filePath}</Text> <Text>{agent.filePath}</Text>
</Box> </Box>
<Box> <Box>
<Text color={theme.text.primary}>Tools: </Text> <Text color={theme.text.primary}>{t('Tools: ')}</Text>
<Text>{toolsDisplay}</Text> <Text>{toolsDisplay}</Text>
</Box> </Box>
{shouldShowColor(agent.color) && ( {shouldShowColor(agent.color) && (
<Box> <Box>
<Text color={theme.text.primary}>Color: </Text> <Text color={theme.text.primary}>{t('Color: ')}</Text>
<Text color={getColorForDisplay(agent.color)}>{agent.color}</Text> <Text color={getColorForDisplay(agent.color)}>{agent.color}</Text>
</Box> </Box>
)} )}
<Box marginTop={1}> <Box marginTop={1}>
<Text color={theme.text.primary}>Description:</Text> <Text color={theme.text.primary}>{t('Description:')}</Text>
</Box> </Box>
<Box padding={1} paddingBottom={0}> <Box padding={1} paddingBottom={0}>
<Text wrap="wrap">{agent.description}</Text> <Text wrap="wrap">{agent.description}</Text>
</Box> </Box>
<Box marginTop={1}> <Box marginTop={1}>
<Text color={theme.text.primary}>System Prompt:</Text> <Text color={theme.text.primary}>{t('System Prompt:')}</Text>
</Box> </Box>
<Box padding={1} paddingBottom={0}> <Box padding={1} paddingBottom={0}>
<Text wrap="wrap">{agent.systemPrompt}</Text> <Text wrap="wrap">{agent.systemPrompt}</Text>

View File

@@ -18,6 +18,7 @@ import { theme } from '../../../semantic-colors.js';
import { getColorForDisplay, shouldShowColor } from '../utils.js'; import { getColorForDisplay, shouldShowColor } from '../utils.js';
import type { SubagentConfig, Config } from '@qwen-code/qwen-code-core'; import type { SubagentConfig, Config } from '@qwen-code/qwen-code-core';
import { useKeypress } from '../../../hooks/useKeypress.js'; import { useKeypress } from '../../../hooks/useKeypress.js';
import { t } from '../../../../i18n/index.js';
interface AgentsManagerDialogProps { interface AgentsManagerDialogProps {
onClose: () => void; onClose: () => void;
@@ -143,21 +144,21 @@ export function AgentsManagerDialog({
const getStepHeaderText = () => { const getStepHeaderText = () => {
switch (currentStep) { switch (currentStep) {
case MANAGEMENT_STEPS.AGENT_SELECTION: case MANAGEMENT_STEPS.AGENT_SELECTION:
return 'Agents'; return t('Agents');
case MANAGEMENT_STEPS.ACTION_SELECTION: case MANAGEMENT_STEPS.ACTION_SELECTION:
return 'Choose Action'; return t('Choose Action');
case MANAGEMENT_STEPS.AGENT_VIEWER: case MANAGEMENT_STEPS.AGENT_VIEWER:
return selectedAgent?.name; return selectedAgent?.name;
case MANAGEMENT_STEPS.EDIT_OPTIONS: case MANAGEMENT_STEPS.EDIT_OPTIONS:
return `Edit ${selectedAgent?.name}`; return t('Edit {{name}}', { name: selectedAgent?.name || '' });
case MANAGEMENT_STEPS.EDIT_TOOLS: case MANAGEMENT_STEPS.EDIT_TOOLS:
return `Edit Tools: ${selectedAgent?.name}`; return t('Edit Tools: {{name}}', { name: selectedAgent?.name || '' });
case MANAGEMENT_STEPS.EDIT_COLOR: case MANAGEMENT_STEPS.EDIT_COLOR:
return `Edit Color: ${selectedAgent?.name}`; return t('Edit Color: {{name}}', { name: selectedAgent?.name || '' });
case MANAGEMENT_STEPS.DELETE_CONFIRMATION: case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
return `Delete ${selectedAgent?.name}`; return t('Delete {{name}}', { name: selectedAgent?.name || '' });
default: default:
return 'Unknown Step'; return t('Unknown Step');
} }
}; };
@@ -183,20 +184,20 @@ export function AgentsManagerDialog({
const getNavigationInstructions = () => { const getNavigationInstructions = () => {
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) { if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
if (availableAgents.length === 0) { if (availableAgents.length === 0) {
return 'Esc to close'; return t('Esc to close');
} }
return 'Enter to select, ↑↓ to navigate, Esc to close'; return t('Enter to select, ↑↓ to navigate, Esc to close');
} }
if (currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) { if (currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) {
return 'Esc to go back'; return t('Esc to go back');
} }
if (currentStep === MANAGEMENT_STEPS.DELETE_CONFIRMATION) { if (currentStep === MANAGEMENT_STEPS.DELETE_CONFIRMATION) {
return 'Enter to confirm, Esc to cancel'; return t('Enter to confirm, Esc to cancel');
} }
return 'Enter to select, ↑↓ to navigate, Esc to go back'; return t('Enter to select, ↑↓ to navigate, Esc to go back');
}; };
return ( return (
@@ -295,7 +296,9 @@ export function AgentsManagerDialog({
default: default:
return ( return (
<Box> <Box>
<Text color={theme.status.error}>Invalid step: {currentStep}</Text> <Text color={theme.status.error}>
{t('Invalid step: {{step}}', { step: currentStep })}
</Text>
</Box> </Box>
); );
} }

View File

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

View File

@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js'; import { theme } from '../../semantic-colors.js';
import { type ToolDefinition } from '../../types.js'; import { type ToolDefinition } from '../../types.js';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { t } from '../../../i18n/index.js';
interface ToolsListProps { interface ToolsListProps {
tools: readonly ToolDefinition[]; tools: readonly ToolDefinition[];
@@ -23,7 +24,7 @@ export const ToolsList: React.FC<ToolsListProps> = ({
}) => ( }) => (
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Available Qwen Code CLI tools: {t('Available Qwen Code CLI tools:')}
</Text> </Text>
<Box height={1} /> <Box height={1} />
{tools.length > 0 ? ( {tools.length > 0 ? (
@@ -46,7 +47,7 @@ export const ToolsList: React.FC<ToolsListProps> = ({
</Box> </Box>
)) ))
) : ( ) : (
<Text color={theme.text.primary}> No tools available</Text> <Text color={theme.text.primary}> {t('No tools available')}</Text>
)} )}
</Box> </Box>
); );

View File

@@ -4,7 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useMemo } from 'react';
import { t } from '../../i18n/index.js';
export const WITTY_LOADING_PHRASES = [ export const WITTY_LOADING_PHRASES = [
"I'm Feeling Lucky", "I'm Feeling Lucky",
@@ -151,10 +152,14 @@ export const usePhraseCycler = (
isWaiting: boolean, isWaiting: boolean,
customPhrases?: string[], customPhrases?: string[],
) => { ) => {
const loadingPhrases = // Translate all phrases at once if using default phrases
customPhrases && customPhrases.length > 0 const loadingPhrases = useMemo(
? customPhrases () =>
: WITTY_LOADING_PHRASES; customPhrases && customPhrases.length > 0
? customPhrases
: WITTY_LOADING_PHRASES.map((phrase) => t(phrase)),
[customPhrases],
);
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState( const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
loadingPhrases[0], loadingPhrases[0],
@@ -163,7 +168,7 @@ export const usePhraseCycler = (
useEffect(() => { useEffect(() => {
if (isWaiting) { if (isWaiting) {
setCurrentLoadingPhrase('Waiting for user confirmation...'); setCurrentLoadingPhrase(t('Waiting for user confirmation...'));
if (phraseIntervalRef.current) { if (phraseIntervalRef.current) {
clearInterval(phraseIntervalRef.current); clearInterval(phraseIntervalRef.current);
phraseIntervalRef.current = null; phraseIntervalRef.current = null;

View File

@@ -9,6 +9,7 @@ import { themeManager } from '../themes/theme-manager.js';
import type { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting import type { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting
import { type HistoryItem, MessageType } from '../types.js'; import { type HistoryItem, MessageType } from '../types.js';
import process from 'node:process'; import process from 'node:process';
import { t } from '../../i18n/index.js';
interface UseThemeCommandReturn { interface UseThemeCommandReturn {
isThemeDialogOpen: boolean; isThemeDialogOpen: boolean;
@@ -34,7 +35,9 @@ export const useThemeCommand = (
addItem( addItem(
{ {
type: MessageType.INFO, type: MessageType.INFO,
text: 'Theme configuration unavailable due to NO_COLOR env variable.', text: t(
'Theme configuration unavailable due to NO_COLOR env variable.',
),
}, },
Date.now(), Date.now(),
); );
@@ -48,7 +51,11 @@ export const useThemeCommand = (
if (!themeManager.setActiveTheme(themeName)) { if (!themeManager.setActiveTheme(themeName)) {
// If theme is not found, open the theme selection dialog and set error message // If theme is not found, open the theme selection dialog and set error message
setIsThemeDialogOpen(true); setIsThemeDialogOpen(true);
setThemeError(`Theme "${themeName}" not found.`); setThemeError(
t('Theme "{{themeName}}" not found.', {
themeName: themeName ?? '',
}),
);
} else { } else {
setThemeError(null); // Clear any previous theme error on success setThemeError(null); // Clear any previous theme error on success
} }
@@ -75,7 +82,11 @@ export const useThemeCommand = (
const isBuiltIn = themeManager.findThemeByName(themeName); const isBuiltIn = themeManager.findThemeByName(themeName);
const isCustom = themeName && mergedCustomThemes[themeName]; const isCustom = themeName && mergedCustomThemes[themeName];
if (!isBuiltIn && !isCustom) { if (!isBuiltIn && !isCustom) {
setThemeError(`Theme "${themeName}" not found in selected scope.`); setThemeError(
t('Theme "{{themeName}}" not found in selected scope.', {
themeName: themeName ?? '',
}),
);
setIsThemeDialogOpen(true); setIsThemeDialogOpen(true);
return; return;
} }

View File

@@ -5,6 +5,7 @@
*/ */
import { AuthType, DEFAULT_QWEN_MODEL } from '@qwen-code/qwen-code-core'; import { AuthType, DEFAULT_QWEN_MODEL } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';
export type AvailableModel = { export type AvailableModel = {
id: string; id: string;
@@ -20,14 +21,20 @@ export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [
{ {
id: MAINLINE_CODER, id: MAINLINE_CODER,
label: MAINLINE_CODER, label: MAINLINE_CODER,
description: get description() {
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)', return t(
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
);
},
}, },
{ {
id: MAINLINE_VLM, id: MAINLINE_VLM,
label: MAINLINE_VLM, label: MAINLINE_VLM,
description: get description() {
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)', return t(
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
);
},
isVision: true, isVision: true,
}, },
]; ];

View File

@@ -30,6 +30,7 @@ import { exec } from 'node:child_process';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { isKittyProtocolEnabled } from './kittyProtocolDetector.js'; import { isKittyProtocolEnabled } from './kittyProtocolDetector.js';
import { VSCODE_SHIFT_ENTER_SEQUENCE } from './platformConstants.js'; import { VSCODE_SHIFT_ENTER_SEQUENCE } from './platformConstants.js';
import { t } from '../../i18n/index.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -146,7 +147,10 @@ async function configureVSCodeStyle(
if (!configDir) { if (!configDir) {
return { return {
success: false, success: false,
message: `Could not determine ${terminalName} config path on Windows: APPDATA environment variable is not set.`, message: t(
'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.',
{ terminalName },
),
}; };
} }
@@ -166,9 +170,12 @@ async function configureVSCodeStyle(
return { return {
success: false, success: false,
message: message:
`${terminalName} keybindings.json exists but is not a valid JSON array. ` + t(
`Please fix the file manually or delete it to allow automatic configuration.\n` + '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.',
`File: ${keybindingsFile}`, { terminalName },
) +
'\n' +
t('File: {{file}}', { file: keybindingsFile }),
}; };
} }
keybindings = parsedContent; keybindings = parsedContent;
@@ -176,10 +183,14 @@ async function configureVSCodeStyle(
return { return {
success: false, success: false,
message: message:
`Failed to parse ${terminalName} keybindings.json. The file contains invalid JSON.\n` + t(
`Please fix the file manually or delete it to allow automatic configuration.\n` + 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.',
`File: ${keybindingsFile}\n` + { terminalName },
`Error: ${parseError}`, ) +
'\n' +
t('File: {{file}}', { file: keybindingsFile }) +
'\n' +
t('Error: {{error}}', { error: String(parseError) }),
}; };
} }
} catch { } catch {
@@ -214,18 +225,23 @@ async function configureVSCodeStyle(
if (existingShiftEnter || existingCtrlEnter) { if (existingShiftEnter || existingCtrlEnter) {
const messages: string[] = []; const messages: string[] = [];
if (existingShiftEnter) { if (existingShiftEnter) {
messages.push(`- Shift+Enter binding already exists`); messages.push('- ' + t('Shift+Enter binding already exists'));
} }
if (existingCtrlEnter) { if (existingCtrlEnter) {
messages.push(`- Ctrl+Enter binding already exists`); messages.push('- ' + t('Ctrl+Enter binding already exists'));
} }
return { return {
success: false, success: false,
message: message:
`Existing keybindings detected. Will not modify to avoid conflicts.\n` + t(
'Existing keybindings detected. Will not modify to avoid conflicts.',
) +
'\n' +
messages.join('\n') + messages.join('\n') +
'\n' + '\n' +
`Please check and modify manually if needed: ${keybindingsFile}`, t('Please check and modify manually if needed: {{file}}', {
file: keybindingsFile,
}),
}; };
} }
@@ -263,19 +279,34 @@ async function configureVSCodeStyle(
await fs.writeFile(keybindingsFile, JSON.stringify(keybindings, null, 4)); await fs.writeFile(keybindingsFile, JSON.stringify(keybindings, null, 4));
return { return {
success: true, success: true,
message: `Added Shift+Enter and Ctrl+Enter keybindings to ${terminalName}.\nModified: ${keybindingsFile}`, message:
t(
'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.',
{
terminalName,
},
) +
'\n' +
t('Modified: {{file}}', { file: keybindingsFile }),
requiresRestart: true, requiresRestart: true,
}; };
} else { } else {
return { return {
success: true, success: true,
message: `${terminalName} keybindings already configured.`, message: t('{{terminalName}} keybindings already configured.', {
terminalName,
}),
}; };
} }
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
message: `Failed to configure ${terminalName}.\nFile: ${keybindingsFile}\nError: ${error}`, message:
t('Failed to configure {{terminalName}}.', { terminalName }) +
'\n' +
t('File: {{file}}', { file: keybindingsFile }) +
'\n' +
t('Error: {{error}}', { error: String(error) }),
}; };
} }
} }
@@ -322,8 +353,9 @@ export async function terminalSetup(): Promise<TerminalSetupResult> {
if (isKittyProtocolEnabled()) { if (isKittyProtocolEnabled()) {
return { return {
success: true, success: true,
message: message: t(
'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).', 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).',
),
}; };
} }
@@ -332,8 +364,9 @@ export async function terminalSetup(): Promise<TerminalSetupResult> {
if (!terminal) { if (!terminal) {
return { return {
success: false, success: false,
message: message: t(
'Could not detect terminal type. Supported terminals: VS Code, Cursor, and Windsurf.', 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.',
),
}; };
} }
@@ -349,7 +382,9 @@ export async function terminalSetup(): Promise<TerminalSetupResult> {
default: default:
return { return {
success: false, success: false,
message: `Terminal "${terminal}" is not supported yet.`, message: t('Terminal "{{terminal}}" is not supported yet.', {
terminal,
}),
}; };
} }
} }

View File

@@ -16,6 +16,7 @@ import type {
SettingsValue, SettingsValue,
} from '../config/settingsSchema.js'; } from '../config/settingsSchema.js';
import { getSettingsSchema } from '../config/settingsSchema.js'; import { getSettingsSchema } from '../config/settingsSchema.js';
import { t } from '../i18n/index.js';
// The schema is now nested, but many parts of the UI and logic work better // The schema is now nested, but many parts of the UI and logic work better
// with a flattened structure and dot-notation keys. This section flattens the // with a flattened structure and dot-notation keys. This section flattens the
@@ -446,7 +447,11 @@ export function getDisplayValue(
if (definition?.type === 'enum' && definition.options) { if (definition?.type === 'enum' && definition.options) {
const option = definition.options?.find((option) => option.value === value); const option = definition.options?.find((option) => option.value === value);
valueString = option?.label ?? `${value}`; if (option?.label) {
valueString = t(option.label) || option.label;
} else {
valueString = `${value}`;
}
} }
// Check if value is different from default OR if it's in modified settings OR if there are pending changes // Check if value is different from default OR if it's in modified settings OR if there are pending changes

View File

@@ -5,6 +5,7 @@
*/ */
import type { ExtendedSystemInfo } from './systemInfo.js'; import type { ExtendedSystemInfo } from './systemInfo.js';
import { t } from '../i18n/index.js';
/** /**
* Field configuration for system information display * Field configuration for system information display
@@ -23,59 +24,59 @@ export function getSystemInfoFields(
): SystemInfoField[] { ): SystemInfoField[] {
const allFields: SystemInfoField[] = [ const allFields: SystemInfoField[] = [
{ {
label: 'CLI Version', label: t('CLI Version'),
key: 'cliVersion', key: 'cliVersion',
}, },
{ {
label: 'Git Commit', label: t('Git Commit'),
key: 'gitCommit', key: 'gitCommit',
}, },
{ {
label: 'Model', label: t('Model'),
key: 'modelVersion', key: 'modelVersion',
}, },
{ {
label: 'Sandbox', label: t('Sandbox'),
key: 'sandboxEnv', key: 'sandboxEnv',
}, },
{ {
label: 'OS Platform', label: t('OS Platform'),
key: 'osPlatform', key: 'osPlatform',
}, },
{ {
label: 'OS Arch', label: t('OS Arch'),
key: 'osArch', key: 'osArch',
}, },
{ {
label: 'OS Release', label: t('OS Release'),
key: 'osRelease', key: 'osRelease',
}, },
{ {
label: 'Node.js Version', label: t('Node.js Version'),
key: 'nodeVersion', key: 'nodeVersion',
}, },
{ {
label: 'NPM Version', label: t('NPM Version'),
key: 'npmVersion', key: 'npmVersion',
}, },
{ {
label: 'Session ID', label: t('Session ID'),
key: 'sessionId', key: 'sessionId',
}, },
{ {
label: 'Auth Method', label: t('Auth Method'),
key: 'selectedAuthType', key: 'selectedAuthType',
}, },
{ {
label: 'Base URL', label: t('Base URL'),
key: 'baseUrl', key: 'baseUrl',
}, },
{ {
label: 'Memory Usage', label: t('Memory Usage'),
key: 'memoryUsage', key: 'memoryUsage',
}, },
{ {
label: 'IDE Client', label: t('IDE Client'),
key: 'ideClient', key: 'ideClient',
}, },
]; ];

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

@@ -0,0 +1,457 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { glob } from 'glob';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// Get __dirname for ESM modules
// @ts-expect-error - import.meta is supported in NodeNext module system at runtime
const __dirname = dirname(fileURLToPath(import.meta.url));
interface CheckResult {
success: boolean;
errors: string[];
warnings: string[];
stats: {
totalKeys: number;
translatedKeys: number;
unusedKeys: string[];
unusedKeysOnlyInLocales?: string[]; // 新增:只在 locales 中存在的未使用键
};
}
/**
* Load translations from JS file
*/
async function loadTranslationsFile(
filePath: string,
): Promise<Record<string, string>> {
try {
// Dynamic import for ES modules
const module = await import(filePath);
return module.default || module;
} catch (error) {
// Fallback: try reading as JSON if JS import fails
try {
const content = fs.readFileSync(
filePath.replace('.js', '.json'),
'utf-8',
);
return JSON.parse(content);
} catch {
throw error;
}
}
}
/**
* Extract string literal from code, handling escaped quotes
*/
function extractStringLiteral(
content: string,
startPos: number,
quote: string,
): { value: string; endPos: number } | null {
let pos = startPos + 1; // Skip opening quote
let value = '';
let escaped = false;
while (pos < content.length) {
const char = content[pos];
if (escaped) {
if (char === '\\') {
value += '\\';
} else if (char === quote) {
value += quote;
} else if (char === 'n') {
value += '\n';
} else if (char === 't') {
value += '\t';
} else if (char === 'r') {
value += '\r';
} else {
value += char;
}
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote) {
return { value, endPos: pos };
} else {
value += char;
}
pos++;
}
return null; // String not closed
}
/**
* Extract all t() calls from source files
*/
async function extractUsedKeys(sourceDir: string): Promise<Set<string>> {
const usedKeys = new Set<string>();
// Find all TypeScript/TSX files
const files = await glob('**/*.{ts,tsx}', {
cwd: sourceDir,
ignore: [
'**/node_modules/**',
'**/dist/**',
'**/*.test.ts',
'**/*.test.tsx',
],
});
for (const file of files) {
const filePath = path.join(sourceDir, file);
try {
const content = fs.readFileSync(filePath, 'utf-8');
// Find all t( calls
const tCallRegex = /t\s*\(/g;
let match;
while ((match = tCallRegex.exec(content)) !== null) {
const startPos = match.index + match[0].length;
let pos = startPos;
// Skip whitespace
while (pos < content.length && /\s/.test(content[pos])) {
pos++;
}
if (pos >= content.length) continue;
const char = content[pos];
if (char === "'" || char === '"') {
const result = extractStringLiteral(content, pos, char);
if (result) {
usedKeys.add(result.value);
}
}
}
} catch {
// Skip files that can't be read
continue;
}
}
return usedKeys;
}
/**
* Check key-value consistency in en.js
*/
function checkKeyValueConsistency(
enTranslations: Record<string, string>,
): string[] {
const errors: string[] = [];
for (const [key, value] of Object.entries(enTranslations)) {
if (key !== value) {
errors.push(`Key-value mismatch: "${key}" !== "${value}"`);
}
}
return errors;
}
/**
* Check if en.js and zh.js have matching keys
*/
function checkKeyMatching(
enTranslations: Record<string, string>,
zhTranslations: Record<string, string>,
): string[] {
const errors: string[] = [];
const enKeys = new Set(Object.keys(enTranslations));
const zhKeys = new Set(Object.keys(zhTranslations));
// Check for keys in en but not in zh
for (const key of enKeys) {
if (!zhKeys.has(key)) {
errors.push(`Missing translation in zh.js: "${key}"`);
}
}
// Check for keys in zh but not in en
for (const key of zhKeys) {
if (!enKeys.has(key)) {
errors.push(`Extra key in zh.js (not in en.js): "${key}"`);
}
}
return errors;
}
/**
* Find unused translation keys
*/
function findUnusedKeys(allKeys: Set<string>, usedKeys: Set<string>): string[] {
return Array.from(allKeys)
.filter((key) => !usedKeys.has(key))
.sort();
}
/**
* Save keys that exist only in locale files to a JSON file
* @param keysOnlyInLocales Array of keys that exist only in locale files
* @param outputPath Path to save the JSON file
*/
function saveKeysOnlyInLocalesToJson(
keysOnlyInLocales: string[],
outputPath: string,
): void {
try {
const data = {
generatedAt: new Date().toISOString(),
keys: keysOnlyInLocales,
count: keysOnlyInLocales.length,
};
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
console.log(`Keys that exist only in locale files saved to: ${outputPath}`);
} catch (error) {
console.error(`Failed to save keys to JSON file: ${error}`);
}
}
/**
* Check if unused keys exist only in locale files and nowhere else in the codebase
* Optimized to search all keys in a single pass instead of multiple grep calls
* @param unusedKeys The list of unused keys to check
* @param sourceDir The source directory to search in
* @param localesDir The locales directory to exclude from search
* @returns Array of keys that exist only in locale files
*/
async function findKeysOnlyInLocales(
unusedKeys: string[],
sourceDir: string,
localesDir: string,
): Promise<string[]> {
if (unusedKeys.length === 0) {
return [];
}
const keysOnlyInLocales: string[] = [];
const localesDirName = path.basename(localesDir);
// Find all TypeScript/TSX files (excluding locales, node_modules, dist, and test files)
const files = await glob('**/*.{ts,tsx}', {
cwd: sourceDir,
ignore: [
'**/node_modules/**',
'**/dist/**',
'**/*.test.ts',
'**/*.test.tsx',
`**/${localesDirName}/**`,
],
});
// Read all files and check for key usage
const foundKeys = new Set<string>();
for (const file of files) {
const filePath = path.join(sourceDir, file);
try {
const content = fs.readFileSync(filePath, 'utf-8');
// Check each unused key in the file content
for (const key of unusedKeys) {
if (!foundKeys.has(key) && content.includes(key)) {
foundKeys.add(key);
}
}
} catch {
// Skip files that can't be read
continue;
}
}
// Keys that were not found in any source files exist only in locales
for (const key of unusedKeys) {
if (!foundKeys.has(key)) {
keysOnlyInLocales.push(key);
}
}
return keysOnlyInLocales;
}
/**
* Main check function
*/
async function checkI18n(): Promise<CheckResult> {
const errors: string[] = [];
const warnings: string[] = [];
const localesDir = path.join(__dirname, '../packages/cli/src/i18n/locales');
const sourceDir = path.join(__dirname, '../packages/cli/src');
const enPath = path.join(localesDir, 'en.js');
const zhPath = path.join(localesDir, 'zh.js');
// Load translation files
let enTranslations: Record<string, string>;
let zhTranslations: Record<string, string>;
try {
enTranslations = await loadTranslationsFile(enPath);
} catch (error) {
errors.push(
`Failed to load en.js: ${error instanceof Error ? error.message : String(error)}`,
);
return {
success: false,
errors,
warnings,
stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] },
};
}
try {
zhTranslations = await loadTranslationsFile(zhPath);
} catch (error) {
errors.push(
`Failed to load zh.js: ${error instanceof Error ? error.message : String(error)}`,
);
return {
success: false,
errors,
warnings,
stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] },
};
}
// Check key-value consistency in en.js
const consistencyErrors = checkKeyValueConsistency(enTranslations);
errors.push(...consistencyErrors);
// Check key matching between en and zh
const matchingErrors = checkKeyMatching(enTranslations, zhTranslations);
errors.push(...matchingErrors);
// Extract used keys from source code
const usedKeys = await extractUsedKeys(sourceDir);
// Find unused keys
const enKeys = new Set(Object.keys(enTranslations));
const unusedKeys = findUnusedKeys(enKeys, usedKeys);
// Find keys that exist only in locales (and nowhere else in the codebase)
const unusedKeysOnlyInLocales =
unusedKeys.length > 0
? await findKeysOnlyInLocales(unusedKeys, sourceDir, localesDir)
: [];
if (unusedKeys.length > 0) {
warnings.push(`Found ${unusedKeys.length} unused translation keys`);
}
const totalKeys = Object.keys(enTranslations).length;
const translatedKeys = Object.keys(zhTranslations).length;
return {
success: errors.length === 0,
errors,
warnings,
stats: {
totalKeys,
translatedKeys,
unusedKeys,
unusedKeysOnlyInLocales,
},
};
}
// Run checks
async function main() {
const result = await checkI18n();
console.log('\n=== i18n Check Results ===\n');
console.log(`Total keys: ${result.stats.totalKeys}`);
console.log(`Translated keys: ${result.stats.translatedKeys}`);
const coverage =
result.stats.totalKeys > 0
? ((result.stats.translatedKeys / result.stats.totalKeys) * 100).toFixed(
1,
)
: '0.0';
console.log(`Translation coverage: ${coverage}%\n`);
if (result.warnings.length > 0) {
console.log('⚠️ Warnings:');
result.warnings.forEach((warning) => console.log(` - ${warning}`));
// Show unused keys
if (
result.stats.unusedKeys.length > 0 &&
result.stats.unusedKeys.length <= 10
) {
console.log('\nUnused keys:');
result.stats.unusedKeys.forEach((key) => console.log(` - "${key}"`));
} else if (result.stats.unusedKeys.length > 10) {
console.log(
`\nUnused keys (showing first 10 of ${result.stats.unusedKeys.length}):`,
);
result.stats.unusedKeys
.slice(0, 10)
.forEach((key) => console.log(` - "${key}"`));
}
// Show keys that exist only in locales files
if (
result.stats.unusedKeysOnlyInLocales &&
result.stats.unusedKeysOnlyInLocales.length > 0
) {
console.log(
'\n⚠ The following keys exist ONLY in locale files and nowhere else in the codebase:',
);
console.log(
' Please review these keys - they might be safe to remove.',
);
result.stats.unusedKeysOnlyInLocales.forEach((key) =>
console.log(` - "${key}"`),
);
// Save these keys to a JSON file
const outputPath = path.join(
__dirname,
'unused-keys-only-in-locales.json',
);
saveKeysOnlyInLocalesToJson(
result.stats.unusedKeysOnlyInLocales,
outputPath,
);
}
console.log();
}
if (result.errors.length > 0) {
console.log('❌ Errors:');
result.errors.forEach((error) => console.log(` - ${error}`));
console.log();
process.exit(1);
}
if (result.success) {
console.log('✅ All checks passed!\n');
process.exit(0);
}
}
main().catch((error) => {
console.error('❌ Fatal error:', error);
process.exit(1);
});

View File

@@ -28,7 +28,7 @@ const targetDir = path.join('dist', 'src');
const extensionsToCopy = ['.md', '.json', '.sb']; const extensionsToCopy = ['.md', '.json', '.sb'];
function copyFilesRecursive(source, target) { function copyFilesRecursive(source, target, rootSourceDir) {
if (!fs.existsSync(target)) { if (!fs.existsSync(target)) {
fs.mkdirSync(target, { recursive: true }); fs.mkdirSync(target, { recursive: true });
} }
@@ -40,9 +40,18 @@ function copyFilesRecursive(source, target) {
const targetPath = path.join(target, item.name); const targetPath = path.join(target, item.name);
if (item.isDirectory()) { if (item.isDirectory()) {
copyFilesRecursive(sourcePath, targetPath); copyFilesRecursive(sourcePath, targetPath, rootSourceDir);
} else if (extensionsToCopy.includes(path.extname(item.name))) { } else {
fs.copyFileSync(sourcePath, targetPath); const ext = path.extname(item.name);
// Copy standard extensions, or .js files in i18n/locales directory
// Use path.relative for precise matching to avoid false positives
const relativePath = path.relative(rootSourceDir, sourcePath);
const normalizedPath = relativePath.replace(/\\/g, '/');
const isLocaleJs =
ext === '.js' && normalizedPath.startsWith('i18n/locales/');
if (extensionsToCopy.includes(ext) || isLocaleJs) {
fs.copyFileSync(sourcePath, targetPath);
}
} }
} }
} }
@@ -52,7 +61,7 @@ if (!fs.existsSync(sourceDir)) {
process.exit(1); process.exit(1);
} }
copyFilesRecursive(sourceDir, targetDir); copyFilesRecursive(sourceDir, targetDir, sourceDir);
// Copy example extensions into the bundle. // Copy example extensions into the bundle.
const packageName = path.basename(process.cwd()); const packageName = path.basename(process.cwd());

View File

@@ -56,6 +56,43 @@ for (const file of filesToCopy) {
} }
} }
// Copy locales folder
console.log('Copying locales folder...');
const localesSourceDir = path.join(
rootDir,
'packages',
'cli',
'src',
'i18n',
'locales',
);
const localesDestDir = path.join(distDir, 'locales');
if (fs.existsSync(localesSourceDir)) {
// Recursive copy function
function copyRecursiveSync(src, dest) {
const stats = fs.statSync(src);
if (stats.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src);
for (const entry of entries) {
const srcPath = path.join(src, entry);
const destPath = path.join(dest, entry);
copyRecursiveSync(srcPath, destPath);
}
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursiveSync(localesSourceDir, localesDestDir);
console.log('Copied locales folder');
} else {
console.warn(`Warning: locales folder not found at ${localesSourceDir}`);
}
// Copy package.json from root and modify it for publishing // Copy package.json from root and modify it for publishing
console.log('Creating package.json for distribution...'); console.log('Creating package.json for distribution...');
const rootPackageJson = JSON.parse( const rootPackageJson = JSON.parse(
@@ -85,7 +122,7 @@ const distPackageJson = {
bin: { bin: {
qwen: 'cli.js', qwen: 'cli.js',
}, },
files: ['cli.js', 'vendor', '*.sb', 'README.md', 'LICENSE'], files: ['cli.js', 'vendor', '*.sb', 'README.md', 'LICENSE', 'locales'],
config: rootPackageJson.config, config: rootPackageJson.config,
dependencies: runtimeDependencies, dependencies: runtimeDependencies,
optionalDependencies: { optionalDependencies: {