feat: add language slash command

This commit is contained in:
pomelo-nwu
2025-11-17 20:45:19 +08:00
parent da2be8045f
commit 88b5717f83
5 changed files with 462 additions and 10 deletions

View File

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

View File

@@ -126,11 +126,11 @@ export default {
'LLM output language not set': 'LLM output language not set',
'Set UI language': 'Set UI language',
'Set LLM output language': 'Set LLM output language',
'Usage: /lang ui [zh-CN|en-US]': 'Usage: /lang ui [zh-CN|en-US]',
'Usage: /lang output <language>': 'Usage: /lang output <language>',
'Example: /lang output 中文': 'Example: /lang output 中文',
'Example: /lang output English': 'Example: /lang output English',
'Example: /lang output 日本語': 'Example: /lang output 日本語',
'Usage: /language ui [zh-CN|en-US]': 'Usage: /language ui [zh-CN|en-US]',
'Usage: /language output <language>': 'Usage: /language output <language>',
'Example: /language output 中文': 'Example: /language output 中文',
'Example: /language output English': 'Example: /language output English',
'Example: /language output 日本語': 'Example: /language output 日本語',
'UI language changed to {{lang}}': 'UI language changed to {{lang}}',
'LLM output language rule file generated at {{path}}':
'LLM output language rule file generated at {{path}}',

View File

@@ -119,11 +119,11 @@ export default {
'LLM output language not set': '未设置 LLM 输出语言',
'Set UI language': '设置 UI 语言',
'Set LLM output language': '设置 LLM 输出语言',
'Usage: /lang ui [zh-CN|en-US]': '用法:/lang ui [zh-CN|en-US]',
'Usage: /lang output <language>': '用法:/lang output <语言>',
'Example: /lang output 中文': '示例:/lang output 中文',
'Example: /lang output English': '示例:/lang output English',
'Example: /lang output 日本語': '示例:/lang output 日本語',
'Usage: /language ui [zh-CN|en-US]': '用法:/language ui [zh-CN|en-US]',
'Usage: /language output <language>': '用法:/language output <语言>',
'Example: /language output 中文': '示例:/language output 中文',
'Example: /language output English': '示例:/language output English',
'Example: /language output 日本語': '示例:/language output 日本語',
'UI language changed to {{lang}}': 'UI 语言已更改为 {{lang}}',
'LLM output language rule file generated at {{path}}':
'LLM 输出语言规则文件已生成于 {{path}}',

View File

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

View File

@@ -0,0 +1,435 @@
/**
* @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 `# ${language} Response Rules
## Core Rules
**ALL OUTPUTS MUST USE ${language.toUpperCase()}, WITHOUT EXCEPTION.** This includes: conversation replies, tool call results, generated files, documentation, comments, and error messages. Even if the user asks in English, you MUST respond in ${language}.
## Tool Call Outputs
All tool execution result descriptions, success/failure messages, and summary explanations must use ${language}:
- File operations: \`read_file\`, \`write_file\`, \`edit_file\`, etc.
- Code search: \`codebase_search\`, \`grep\`, etc.
- Terminal commands: \`run_terminal_cmd\` execution result descriptions
- Other tools: \`todo_write\`, \`web_search\`, etc.
**Examples:**
- ✅ "Successfully read file config.json, contains 15 lines" (translated to ${language})
- ❌ "Successfully read file config.json, contains 15 lines" (in English when ${language} is required)
**Note:** Variable names and function names in code can remain in English, but comments, documentation, and all explanatory text must be in ${language}.
`;
}
/**
* 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();
return {
type: 'message',
messageType: 'info',
content: t('UI language changed to {{lang}}', { 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,
}),
});
} 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);
},
},
],
};