feat: Add i18n to commands description

This commit is contained in:
pomelo-nwu
2025-11-17 21:06:01 +08:00
parent 88b5717f83
commit 6cccf9cc59
36 changed files with 338 additions and 150 deletions

View File

@@ -91,6 +91,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 { type VisionSwitchOutcome } from './components/ModelSwitchDialog.js'; import { type VisionSwitchOutcome } from './components/ModelSwitchDialog.js';
@@ -372,14 +373,14 @@ export const AppContainer = (props: AppContainerProps) => {
// Handle Qwen OAuth timeout // Handle Qwen OAuth timeout
const handleQwenAuthTimeout = useCallback(() => { const handleQwenAuthTimeout = useCallback(() => {
onAuthError('Qwen OAuth authentication timed out. Please try again.'); onAuthError(t('Qwen OAuth authentication timed out. Please try again.'));
cancelQwenAuth(); cancelQwenAuth();
setAuthState(AuthState.Updating); setAuthState(AuthState.Updating);
}, [onAuthError, cancelQwenAuth, setAuthState]); }, [onAuthError, cancelQwenAuth, setAuthState]);
// Handle Qwen OAuth cancel // Handle Qwen OAuth cancel
const handleQwenAuthCancel = useCallback(() => { const handleQwenAuthCancel = useCallback(() => {
onAuthError('Qwen OAuth authentication cancelled.'); onAuthError(t('Qwen OAuth authentication cancelled.'));
cancelQwenAuth(); cancelQwenAuth();
setAuthState(AuthState.Updating); setAuthState(AuthState.Updating);
}, [onAuthError, cancelQwenAuth, setAuthState]); }, [onAuthError, cancelQwenAuth, setAuthState]);
@@ -401,7 +402,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

@@ -14,6 +14,7 @@ import { Colors } from '../colors.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { OpenAIKeyPrompt } from '../components/OpenAIKeyPrompt.js'; import { OpenAIKeyPrompt } from '../components/OpenAIKeyPrompt.js';
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
import { t } from '../../i18n/index.js';
interface AuthDialogProps { interface AuthDialogProps {
onSelect: ( onSelect: (
@@ -53,10 +54,14 @@ export function AuthDialog({
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(
@@ -107,7 +112,9 @@ export function AuthDialog({
const handleOpenAIKeyCancel = () => { const handleOpenAIKeyCancel = () => {
setShowOpenAIKeyPrompt(false); setShowOpenAIKeyPrompt(false);
setErrorMessage('OpenAI API key is required to use OpenAI authentication.'); setErrorMessage(
t('OpenAI API key is required to use OpenAI authentication.'),
);
}; };
useKeypress( useKeypress(
@@ -125,7 +132,9 @@ export function AuthDialog({
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;
} }
@@ -165,9 +174,9 @@ export function AuthDialog({
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
@@ -182,10 +191,10 @@ export function AuthDialog({
</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>
<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

@@ -13,6 +13,7 @@ import {
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { AuthState } from '../types.js'; import { AuthState } from '../types.js';
import { validateAuthMethod } from '../../config/auth.js'; import { validateAuthMethod } from '../../config/auth.js';
import { t } from '../../i18n/index.js';
export function validateAuthMethodWithSettings( export function validateAuthMethodWithSettings(
authType: AuthType, authType: AuthType,
@@ -20,7 +21,13 @@ export function validateAuthMethodWithSettings(
): string | null { ): string | null {
const enforcedType = settings.merged.security?.auth?.enforcedType; const enforcedType = settings.merged.security?.auth?.enforcedType;
if (enforcedType && enforcedType !== authType) { if (enforcedType && enforcedType !== authType) {
return `Authentication is enforced to be ${enforcedType}, but you are currently using ${authType}.`; return t(
'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.',
{
enforcedType,
currentType: authType,
},
);
} }
if (settings.merged.security?.auth?.useExternal) { if (settings.merged.security?.auth?.useExternal) {
return null; return null;
@@ -76,7 +83,11 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
setAuthError(null); setAuthError(null);
setAuthState(AuthState.Authenticated); setAuthState(AuthState.Authenticated);
} catch (e) { } catch (e) {
onAuthError(`Failed to login. Message: ${getErrorMessage(e)}`); onAuthError(
t('Failed to login. Message: {{message}}', {
message: getErrorMessage(e),
}),
);
} finally { } finally {
setIsAuthenticating(false); setIsAuthenticating(false);
} }

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

@@ -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,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 {
@@ -150,7 +156,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 {

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

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

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

@@ -115,10 +115,18 @@ async function setUiLanguage(
// Reload commands to update their descriptions with the new language // Reload commands to update their descriptions with the new language
context.ui.reloadCommands(); context.ui.reloadCommands();
// Map language codes to friendly display names
const langDisplayNames: Record<SupportedLanguage, string> = {
zh: '中文zh-CN',
en: 'Englishen-US',
};
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: t('UI language changed to {{lang}}', { lang }), content: t('UI language changed to {{lang}}', {
lang: langDisplayNames[lang],
}),
}; };
} }

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 }),
}; };
} }
@@ -111,7 +114,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 +130,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 +150,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 +186,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 +199,7 @@ const listCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Config not loaded.', content: t('Config not loaded.'),
}; };
} }
@@ -276,7 +299,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 +311,7 @@ const refreshCommand: SlashCommand = {
return { return {
type: 'message', type: 'message',
messageType: 'error', messageType: 'error',
content: 'Config not loaded.', content: t('Config not loaded.'),
}; };
} }
@@ -324,8 +349,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

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

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

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

@@ -89,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,
inputWidth, inputWidth,
suggestionsWidth, suggestionsWidth,

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 { DeviceAuthorizationInfo } from '../hooks/useQwenAuth.js'; import type { DeviceAuthorizationInfo } from '../hooks/useQwenAuth.js';
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>
@@ -246,16 +255,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

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

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

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