feat: Add Portuguese (pt-BR) Support and Refactored I18n Architecture

This commit is contained in:
pomelo-nwu 2026-01-26 23:28:17 +08:00
parent de3bc5fe3a
commit 109738bf67
12 changed files with 1523 additions and 103 deletions

View file

@ -18,6 +18,7 @@ import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
} from '@qwen-code/qwen-code-core';
import type { CustomTheme } from '../ui/themes/theme.js';
import { getLanguageSettingsOptions } from '../i18n/languages.js';
export type SettingsType =
| 'boolean'
@ -211,14 +212,7 @@ const SETTINGS_SCHEMA = {
'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)' },
{ value: 'ru', label: 'Русский (Russian)' },
{ value: 'de', label: 'Deutsch (German)' },
{ value: 'ja', label: '日本語 (Japanese)' },
],
options: [] as readonly SettingEnumOption[],
},
outputLanguage: {
type: 'string',
@ -228,7 +222,7 @@ const SETTINGS_SCHEMA = {
default: 'auto',
description:
'The language for LLM output. Use "auto" to detect from system settings, ' +
'or set a specific language (e.g., "English", "中文", "日本語").',
'or set a specific language.',
showInDialog: true,
},
terminalBell: {
@ -1190,6 +1184,15 @@ const SETTINGS_SCHEMA = {
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
export function getSettingsSchema(): SettingsSchemaType {
// Inject dynamic language options
const schema = SETTINGS_SCHEMA as unknown as SettingsSchema;
if (schema['general']?.properties?.['language']) {
(
schema['general'].properties['language'] as {
options?: SettingEnumOption[];
}
).options = getLanguageSettingsOptions();
}
return SETTINGS_SCHEMA;
}

View file

@ -14,7 +14,7 @@ import {
import { type LoadedSettings, SettingScope } from '../config/settings.js';
import { performInitialAuth } from './auth.js';
import { validateTheme } from './theme.js';
import { initializeI18n } from '../i18n/index.js';
import { initializeI18n, type SupportedLanguage } from '../i18n/index.js';
import { initializeLlmOutputLanguage } from '../utils/languageUtils.js';
export interface InitializationResult {
@ -38,9 +38,9 @@ export async function initializeApp(
// Initialize i18n system
const languageSetting =
process.env['QWEN_CODE_LANG'] ||
settings.merged.general?.language ||
(settings.merged.general?.language as string) ||
'auto';
await initializeI18n(languageSetting);
await initializeI18n(languageSetting as SupportedLanguage | 'auto');
// Auto-detect and set LLM output language on first use
initializeLlmOutputLanguage(settings.merged.general?.outputLanguage);

View file

@ -10,6 +10,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import { homedir } from 'node:os';
import {
type SupportedLanguage,
SUPPORTED_LANGUAGES,
getLanguageNameFromLocale,
} from './languages.js';
@ -55,18 +56,17 @@ const getLocalePath = (
// 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';
if (envLang?.startsWith('ru')) return 'ru';
if (envLang?.startsWith('de')) return 'de';
if (envLang?.startsWith('ja')) return 'ja';
if (envLang) {
for (const lang of SUPPORTED_LANGUAGES) {
if (envLang.startsWith(lang.code)) return lang.code;
}
}
try {
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
if (locale.startsWith('zh')) return 'zh';
if (locale.startsWith('ru')) return 'ru';
if (locale.startsWith('de')) return 'de';
if (locale.startsWith('ja')) return 'ja';
for (const lang of SUPPORTED_LANGUAGES) {
if (locale.startsWith(lang.code)) return lang.code;
}
} catch {
// Fallback to default
}

View file

@ -4,7 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
export type SupportedLanguage = 'en' | 'zh' | 'ru' | 'de' | 'ja' | string;
export type SupportedLanguage =
| 'en'
| 'zh'
| 'ru'
| 'de'
| 'ja'
| 'pt'
| string;
export interface LanguageDefinition {
/** The internal locale code used by the i18n system (e.g., 'en', 'zh'). */
@ -13,6 +20,8 @@ export interface LanguageDefinition {
id: string;
/** The full English name of the language (e.g., 'English', 'Chinese'). */
fullName: string;
/** The native name of the language (e.g., 'English', '中文'). */
nativeName?: string;
}
export const SUPPORTED_LANGUAGES: readonly LanguageDefinition[] = [
@ -20,26 +29,37 @@ export const SUPPORTED_LANGUAGES: readonly LanguageDefinition[] = [
code: 'en',
id: 'en-US',
fullName: 'English',
nativeName: 'English',
},
{
code: 'zh',
id: 'zh-CN',
fullName: 'Chinese',
nativeName: '中文',
},
{
code: 'ru',
id: 'ru-RU',
fullName: 'Russian',
nativeName: 'Русский',
},
{
code: 'de',
id: 'de-DE',
fullName: 'German',
nativeName: 'Deutsch',
},
{
code: 'ja',
id: 'ja-JP',
fullName: 'Japanese',
nativeName: '日本語',
},
{
code: 'pt',
id: 'pt-BR',
fullName: 'Portuguese',
nativeName: 'Português',
},
];
@ -51,3 +71,28 @@ export function getLanguageNameFromLocale(locale: SupportedLanguage): string {
const lang = SUPPORTED_LANGUAGES.find((l) => l.code === locale);
return lang?.fullName || 'English';
}
/**
* Gets the language options for the settings schema.
*/
export function getLanguageSettingsOptions(): Array<{
value: string;
label: string;
}> {
return [
{ value: 'auto', label: 'Auto (detect from system)' },
...SUPPORTED_LANGUAGES.map((l) => ({
value: l.code,
label: l.nativeName
? `${l.nativeName} (${l.fullName})`
: `${l.fullName} (${l.id})`,
})),
];
}
/**
* Gets a string containing all supported language IDs (e.g., "en-US|zh-CN").
*/
export function getSupportedLanguageIds(separator = '|'): string {
return SUPPORTED_LANGUAGES.map((l) => l.id).join(separator);
}

View file

@ -569,8 +569,8 @@ export default {
// ============================================================================
// Commands - Language
// ============================================================================
'Invalid language. Available: en-US, zh-CN':
'Ungültige Sprache. Verfügbar: en-US, zh-CN',
'Invalid language. Available: {{options}}':
'Ungültige Sprache. Verfügbar: {{options}}',
'Language subcommands do not accept additional arguments.':
'Sprach-Unterbefehle akzeptieren keine zusätzlichen Argumente.',
'Current UI language: {{lang}}': 'Aktuelle UI-Sprache: {{lang}}',
@ -579,12 +579,14 @@ export default {
'LLM output language not set': 'LLM-Ausgabesprache nicht festgelegt',
'Set UI language': 'UI-Sprache festlegen',
'Set LLM output language': 'LLM-Ausgabesprache festlegen',
'Usage: /language ui [zh-CN|en-US]': 'Verwendung: /language ui [zh-CN|en-US]',
'Usage: /language ui [{{options}}]': 'Verwendung: /language ui [{{options}}]',
'Usage: /language output <language>':
'Verwendung: /language output <Sprache>',
'Example: /language output 中文': 'Beispiel: /language output Deutsch',
'Example: /language output English': 'Beispiel: /language output English',
'Example: /language output English': 'Beispiel: /language output Englisch',
'Example: /language output 日本語': 'Beispiel: /language output Japanisch',
'Example: /language output Português':
'Beispiel: /language output Portugiesisch',
'UI language changed to {{lang}}': 'UI-Sprache geändert zu {{lang}}',
'LLM output language set to {{lang}}':
'LLM-Ausgabesprache auf {{lang}} gesetzt',
@ -600,12 +602,7 @@ export default {
'To request additional UI language packs, please open an issue on GitHub.':
'Um zusätzliche UI-Sprachpakete anzufordern, öffnen Sie bitte ein Issue auf GitHub.',
'Available options:': 'Verfügbare Optionen:',
' - zh-CN: Simplified Chinese': ' - zh-CN: Vereinfachtes Chinesisch',
' - en-US: English': ' - en-US: Englisch',
'Set UI language to Simplified Chinese (zh-CN)':
'UI-Sprache auf Vereinfachtes Chinesisch (zh-CN) setzen',
'Set UI language to English (en-US)':
'UI-Sprache auf Englisch (en-US) setzen',
'Set UI language to {{name}}': 'UI-Sprache auf {{name}} setzen',
// ============================================================================
// Commands - Approval Mode

View file

@ -576,8 +576,8 @@ export default {
// ============================================================================
// Commands - Language
// ============================================================================
'Invalid language. Available: en-US, zh-CN':
'Invalid language. Available: en-US, zh-CN, ru-RU, de-DE, ja-JP',
'Invalid language. Available: {{options}}':
'Invalid language. Available: {{options}}',
'Language subcommands do not accept additional arguments.':
'Language subcommands do not accept additional arguments.',
'Current UI language: {{lang}}': 'Current UI language: {{lang}}',
@ -586,12 +586,12 @@ 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: /language ui [zh-CN|en-US]':
'Usage: /language ui [zh-CN|en-US|ru-RU|de-DE|ja-JP]',
'Usage: /language ui [{{options}}]': 'Usage: /language ui [{{options}}]',
'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 日本語',
'Example: /language output Português': 'Example: /language output Português',
'UI language changed to {{lang}}': 'UI language changed to {{lang}}',
'LLM output language set to {{lang}}': 'LLM output language set to {{lang}}',
'LLM output language rule file generated at {{path}}':
@ -606,17 +606,7 @@ export default {
'To request additional UI language packs, please open an issue on GitHub.':
'To request additional UI language packs, please open an issue on GitHub.',
'Available options:': 'Available options:',
' - zh-CN: Simplified Chinese': ' - zh-CN: Simplified Chinese',
' - en-US: English': ' - en-US: English',
' - ru-RU: Russian': ' - ru-RU: Russian',
' - de-DE: German': ' - de-DE: German',
' - ja-JP: Japanese': ' - ja-JP: Japanese',
'Set UI language to Simplified Chinese (zh-CN)':
'Set UI language to Simplified Chinese (zh-CN)',
'Set UI language to English (en-US)': 'Set UI language to English (en-US)',
'Set UI language to Russian (ru-RU)': 'Set UI language to Russian (ru-RU)',
'Set UI language to German (de-DE)': 'Set UI language to German (de-DE)',
'Set UI language to Japanese (ja-JP)': 'Set UI language to Japanese (ja-JP)',
'Set UI language to {{name}}': 'Set UI language to {{name}}',
// ============================================================================
// Commands - Approval Mode

View file

@ -369,8 +369,8 @@ export default {
'Terminal "{{terminal}}" is not supported yet.':
'ターミナル "{{terminal}}" はまだサポートされていません',
// Commands - Language
'Invalid language. Available: en-US, zh-CN':
'無効な言語です。使用可能: en-US, zh-CN, ru-RU, de-DE, ja-JP',
'Invalid language. Available: {{options}}':
'無効な言語です。使用可能: {{options}}',
'Language subcommands do not accept additional arguments.':
'言語サブコマンドは追加の引数を受け付けません',
'Current UI language: {{lang}}': '現在のUI言語: {{lang}}',
@ -378,12 +378,12 @@ export default {
'LLM output language not set': 'LLM出力言語が設定されていません',
'Set UI language': 'UI言語を設定',
'Set LLM output language': 'LLM出力言語を設定',
'Usage: /language ui [zh-CN|en-US]':
'使い方: /language ui [zh-CN|en-US|ru-RU|de-DE|ja-JP]',
'Usage: /language ui [{{options}}]': '使い方: /language ui [{{options}}]',
'Usage: /language output <language>': '使い方: /language output <言語>',
'Example: /language output 中文': '例: /language output 中文',
'Example: /language output English': '例: /language output English',
'Example: /language output 日本語': '例: /language output 日本語',
'Example: /language output Português': '例: /language output Português',
'UI language changed to {{lang}}': 'UI言語を {{lang}} に変更しました',
'LLM output language rule file generated at {{path}}':
'LLM出力言語ルールファイルを {{path}} に生成しました',
@ -397,17 +397,7 @@ export default {
'To request additional UI language packs, please open an issue on GitHub.':
'追加のUI言語パックをリクエストするには、GitHub で Issue を作成してください',
'Available options:': '使用可能なオプション:',
' - zh-CN: Simplified Chinese': ' - zh-CN: 簡体字中国語',
' - en-US: English': ' - en-US: 英語',
' - ru-RU: Russian': ' - ru-RU: ロシア語',
' - de-DE: German': ' - de-DE: ドイツ語',
' - ja-JP: Japanese': ' - ja-JP: 日本語',
'Set UI language to Simplified Chinese (zh-CN)':
'UI言語を簡体字中国語(zh-CN)に設定',
'Set UI language to English (en-US)': 'UI言語を英語(en-US)に設定',
'Set UI language to Russian (ru-RU)': 'UI言語をロシア語(ru-RU)に設定',
'Set UI language to German (de-DE)': 'UI言語をドイツ語(de-DE)に設定',
'Set UI language to Japanese (ja-JP)': 'UI言語を日本語(ja-JP)に設定',
'Set UI language to {{name}}': 'UI言語を {{name}} に設定',
// Approval Mode
'Approval Mode': '承認モード',
'Current approval mode: {{mode}}': '現在の承認モード: {{mode}}',

File diff suppressed because it is too large Load diff

View file

@ -580,8 +580,8 @@ export default {
// ============================================================================
// Команды - Язык
// ============================================================================
'Invalid language. Available: en-US, zh-CN':
'Неверный язык. Доступны: en-US, zh-CN, ru-RU, de-DE, ja-JP',
'Invalid language. Available: {{options}}':
'Недопустимый язык. Доступны: {{options}}',
'Language subcommands do not accept additional arguments.':
'Подкоманды языка не принимают дополнительных аргументов.',
'Current UI language: {{lang}}': 'Текущий язык интерфейса: {{lang}}',
@ -589,13 +589,14 @@ export default {
'LLM output language not set': 'Язык вывода LLM не установлен',
'Set UI language': 'Установка языка интерфейса',
'Set LLM output language': 'Установка языка вывода LLM',
'Usage: /language ui [zh-CN|en-US]':
'Использование: /language ui [zh-CN|en-US|ru-RU|de-DE|ja-JP]',
'Usage: /language ui [{{options}}]':
'Использование: /language ui [{{options}}]',
'Usage: /language output <language>':
'Использование: /language output <language>',
'Example: /language output 中文': 'Пример: /language output 中文',
'Example: /language output English': 'Пример: /language output English',
'Example: /language output 日本語': 'Пример: /language output 日本語',
'Example: /language output Português': 'Пример: /language output Português',
'UI language changed to {{lang}}': 'Язык интерфейса изменен на {{lang}}',
'LLM output language set to {{lang}}':
'Язык вывода LLM установлен на {{lang}}',
@ -611,21 +612,7 @@ export default {
'To request additional UI language packs, please open an issue on GitHub.':
'Для запроса дополнительных языковых пакетов интерфейса, пожалуйста, создайте обращение на GitHub.',
'Available options:': 'Доступные варианты:',
' - zh-CN: Simplified Chinese': ' - zh-CN: Упрощенный китайский',
' - en-US: English': ' - en-US: Английский',
' - ru-RU: Russian': ' - ru-RU: Русский',
' - de-DE: German': ' - de-DE: Немецкий',
' - ja-JP: Japanese': ' - ja-JP: Японский',
'Set UI language to Simplified Chinese (zh-CN)':
'Установить язык интерфейса на упрощенный китайский (zh-CN)',
'Set UI language to English (en-US)':
'Установить язык интерфейса на английский (en-US)',
'Set UI language to Russian (ru-RU)':
'Установить язык интерфейса на русский (ru-RU)',
'Set UI language to German (de-DE)':
'Установить язык интерфейса на немецкий (de-DE)',
'Set UI language to Japanese (ja-JP)':
'Установить язык интерфейса на японский (ja-JP)',
'Set UI language to {{name}}': 'Установить язык интерфейса на {{name}}',
// ============================================================================
// Команды - Режим подтверждения

View file

@ -548,8 +548,8 @@ export default {
// ============================================================================
// Commands - Language
// ============================================================================
'Invalid language. Available: en-US, zh-CN':
'无效的语言。可用选项:en-US, zh-CN, ru-RU, de-DE, ja-JP',
'Invalid language. Available: {{options}}':
'无效的语言。可用选项:{{options}}',
'Language subcommands do not accept additional arguments.':
'语言子命令不接受额外参数',
'Current UI language: {{lang}}': '当前 UI 语言:{{lang}}',
@ -557,11 +557,12 @@ export default {
'LLM output language not set': '未设置 LLM 输出语言',
'Set UI language': '设置 UI 语言',
'Set LLM output language': '设置 LLM 输出语言',
'Usage: /language ui [zh-CN|en-US]': '用法:/language ui [zh-CN|en-US]',
'Usage: /language ui [{{options}}]': '用法:/language ui [{{options}}]',
'Usage: /language output <language>': '用法:/language output <语言>',
'Example: /language output 中文': '示例:/language output 中文',
'Example: /language output English': '示例:/language output English',
'Example: /language output 日本語': '示例:/language output 日本語',
'Example: /language output Português': '示例:/language output Português',
'UI language changed to {{lang}}': 'UI 语言已更改为 {{lang}}',
'LLM output language set to {{lang}}': 'LLM 输出语言已设置为 {{lang}}',
'LLM output language rule file generated at {{path}}':
@ -575,11 +576,7 @@ export default {
'To request additional UI language packs, please open an issue on GitHub.':
'如需请求其他 UI 语言包,请在 GitHub 上提交 issue',
'Available options:': '可用选项:',
' - zh-CN: Simplified Chinese': ' - zh-CN: 简体中文',
' - en-US: English': ' - en-US: English',
'Set UI language to Simplified Chinese (zh-CN)':
'将 UI 语言设置为简体中文 (zh-CN)',
'Set UI language to English (en-US)': '将 UI 语言设置为英语 (en-US)',
'Set UI language to {{name}}': '将 UI 语言设置为 {{name}}',
// ============================================================================
// Commands - Approval Mode

View file

@ -22,6 +22,7 @@ vi.mock('../../i18n/index.js', () => ({
ru: 'Russian',
de: 'German',
ja: 'Japanese',
pt: 'Portuguese',
};
return map[locale] || 'English';
}),
@ -73,6 +74,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
// Import modules after mocking
import * as i18n from '../../i18n/index.js';
import { SUPPORTED_LANGUAGES } from '../../i18n/languages.js';
import { languageCommand } from './languageCommand.js';
import { initializeLlmOutputLanguage } from '../../utils/languageUtils.js';
@ -566,11 +568,9 @@ describe('languageCommand', () => {
it('should have nested language subcommands', () => {
const nestedNames = uiSubcommand?.subCommands?.map((c) => c.name);
expect(nestedNames).toContain('zh-CN');
expect(nestedNames).toContain('en-US');
expect(nestedNames).toContain('ru-RU');
expect(nestedNames).toContain('de-DE');
expect(nestedNames).toContain('ja-JP');
for (const lang of SUPPORTED_LANGUAGES) {
expect(nestedNames).toContain(lang.id);
}
});
it('should have action that sets language', async () => {
@ -831,5 +831,18 @@ describe('languageCommand', () => {
'utf-8',
);
});
it('should detect Portuguese locale and create Portuguese rule file', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(i18n.detectSystemLanguage).mockReturnValue('pt');
initializeLlmOutputLanguage();
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('output-language.md'),
expect.stringContaining('Portuguese'),
'utf-8',
);
});
});
});

View file

@ -18,7 +18,10 @@ import {
type SupportedLanguage,
t,
} from '../../i18n/index.js';
import { SUPPORTED_LANGUAGES } from '../../i18n/languages.js';
import {
SUPPORTED_LANGUAGES,
getSupportedLanguageIds,
} from '../../i18n/languages.js';
import {
OUTPUT_LANGUAGE_AUTO,
isAutoLanguage,
@ -62,11 +65,14 @@ function parseUiLanguageArg(input: string): SupportedLanguage | null {
}
/**
* Formats a UI language code for display (e.g., "zh" -> "Chinesezh-CN").
* Formats a UI language code for display (e.g., "zh" -> "中文 (Chinese) [zh-CN]").
*/
function formatUiLanguageDisplay(lang: SupportedLanguage): string {
const option = SUPPORTED_LANGUAGES.find((o) => o.code === lang);
return option ? `${option.fullName}${option.id}` : lang;
if (!option) return lang;
return option.nativeName && option.nativeName !== option.fullName
? `${option.nativeName} (${option.fullName}) [${option.id}]`
: `${option.fullName} [${option.id}]`;
}
/**
@ -219,7 +225,7 @@ export const languageCommand: SlashCommand = {
messageType: 'error',
content: [
t('Invalid command. Available subcommands:'),
` - /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`,
` - /language ui [${getSupportedLanguageIds()}] - ${t('Set UI language')}`,
` - /language output <language> - ${t('Set LLM output language')}`,
].join('\n'),
};
@ -245,7 +251,7 @@ export const languageCommand: SlashCommand = {
t('Current LLM output language: {{lang}}', { lang: outputLangDisplay }),
'',
t('Available subcommands:'),
` /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`,
` /language ui [${getSupportedLanguageIds()}] - ${t('Set UI language')}`,
` /language output <language> - ${t('Set LLM output language')}`,
].join('\n'),
};
@ -274,12 +280,12 @@ export const languageCommand: SlashCommand = {
t('Set UI language'),
'',
t('Usage: /language ui [{{options}}]', {
options: SUPPORTED_LANGUAGES.map((o) => o.id).join('|'),
options: getSupportedLanguageIds(),
}),
'',
t('Available options:'),
...SUPPORTED_LANGUAGES.map(
(o) => ` - ${o.id}: ${t(o.fullName)}`,
(o) => ` - ${o.id}: ${o.nativeName || o.fullName}`,
),
'',
t(
@ -295,7 +301,7 @@ export const languageCommand: SlashCommand = {
type: 'message',
messageType: 'error',
content: t('Invalid language. Available: {{options}}', {
options: SUPPORTED_LANGUAGES.map((o) => o.id).join(','),
options: getSupportedLanguageIds(','),
}),
};
}
@ -308,7 +314,9 @@ export const languageCommand: SlashCommand = {
(lang): SlashCommand => ({
name: lang.id,
get description() {
return t('Set UI language to {{name}}', { name: lang.fullName });
return t('Set UI language to {{name}}', {
name: lang.nativeName || lang.fullName,
});
},
kind: CommandKind.BUILT_IN,
action: async (context, args) => {