Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
28e1600
feat(cli): add Traditional Chinese (zh-TW) as a UI language option
MikeWang0316tw Apr 24, 2026
d849354
fix: use upstream unused-keys-only-in-locales.json to resolve conflict
MikeWang0316tw Apr 24, 2026
3d542a0
revert: remove check-i18n.ts changes to avoid pre-existing zh.js issues
MikeWang0316tw Apr 24, 2026
bcc92ea
feat(cli): add Traditional Chinese (zh-TW) as a UI language option
MikeWang0316tw Apr 24, 2026
cb51281
fix(cli): add WITTY_LOADING_PHRASES to zh-TW locale
MikeWang0316tw Apr 24, 2026
89c4955
fix(cli): sync zh-TW.js with en.js keys, fix double-escape, fix check…
MikeWang0316tw Apr 24, 2026
ccbde3d
fix: resolve conflict in unused-keys-only-in-locales.json
MikeWang0316tw Apr 24, 2026
e3db54c
fix(cli): add missing Performance translation to zh-TW
MikeWang0316tw Apr 24, 2026
3374d47
fix(cli): add quotes to Performance key in zh-TW
MikeWang0316tw Apr 24, 2026
3555bd5
fix(cli): regenerate zh-TW.js with correct multi-line value parsing
MikeWang0316tw Apr 24, 2026
1382c3a
fix: resolve conflict in unused-keys-only-in-locales.json
MikeWang0316tw Apr 24, 2026
0bd40bc
fix(cli): regenerate zh-TW.js with correct multi-line value parsing
MikeWang0316tw Apr 24, 2026
fc23c1e
fix(cli): standardize zh-TW.js key quoting and sync zh.js keys
MikeWang0316tw Apr 24, 2026
7eedd5f
chore: merge origin/main and resolve zh.js conflicts
MikeWang0316tw Apr 24, 2026
522aad4
fix(cli): update loading phrases when UI language changes
MikeWang0316tw Apr 24, 2026
c8d4fae
fix(i18n): normalize locale separators and fix case-insensitive langu…
MikeWang0316tw Apr 24, 2026
a5c3ed9
fix(test): update getLanguageNameFromLocale mock to include zh-TW
MikeWang0316tw Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions packages/cli/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,18 @@ const getLocalePath = (
export function detectSystemLanguage(): SupportedLanguage {
const envLang = process.env['QWEN_CODE_LANG'] || process.env['LANG'];
if (envLang) {
// Normalize POSIX locales (e.g. zh_TW.UTF-8 → zh-tw) before matching
const normalized = envLang.replace(/_/g, '-').toLowerCase();
for (const lang of SUPPORTED_LANGUAGES) {
if (envLang.startsWith(lang.code)) return lang.code;
if (normalized.startsWith(lang.code.toLowerCase())) return lang.code;
}
}

try {
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
const normalized = locale.replace(/_/g, '-').toLowerCase();
for (const lang of SUPPORTED_LANGUAGES) {
if (locale.startsWith(lang.code)) return lang.code;
if (normalized.startsWith(lang.code.toLowerCase())) return lang.code;
}
} catch {
// Fallback to default
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/i18n/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export type SupportedLanguage =
| 'en'
| 'zh'
| 'zh-TW'
| 'ru'
| 'de'
| 'ja'
Expand All @@ -32,6 +33,12 @@ export const SUPPORTED_LANGUAGES: readonly LanguageDefinition[] = [
fullName: 'English',
nativeName: 'English',
},
{
code: 'zh-TW',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Adding zh-TW after the generic zh entry breaks the new locale for users who keep general.language on auto. detectSystemLanguage() walks SUPPORTED_LANGUAGES in order and uses startsWith(), so zh-TW/zh-TW.UTF-8 matches zh first and still resolves to Simplified Chinese.

Suggested change
code: 'zh-TW',
{
code: 'zh-TW',
id: 'zh-TW',
fullName: 'Traditional Chinese',
nativeName: '繁體中文',
},
{
code: 'zh',
id: 'zh-CN',
fullName: 'Chinese',
nativeName: '中文',
},

— gpt-5.4 via Qwen Code /review

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Adding zh-TW as a mixed-case language code exposes an existing case-sensitive lookup in getLanguageNameFromLocale(): normalizeOutputLanguage() lowercases user input before calling it, so /language output zh-TW becomes zh-tw and is preserved as the literal string instead of resolving to Traditional Chinese.

Suggested change
code: 'zh-TW',
code: 'zh-tw',

Alternatively, keep the canonical code and make getLanguageNameFromLocale() compare l.code.toLowerCase() against the normalized input.

— gpt-5.5 via Qwen Code /review

id: 'zh-TW',
fullName: 'Traditional Chinese',
nativeName: '繁體中文',
},
{
code: 'zh',
id: 'zh-CN',
Expand Down Expand Up @@ -75,7 +82,8 @@ export const SUPPORTED_LANGUAGES: readonly LanguageDefinition[] = [
* Used for LLM output language instructions.
*/
export function getLanguageNameFromLocale(locale: SupportedLanguage): string {
const lang = SUPPORTED_LANGUAGES.find((l) => l.code === locale);
const lower = locale.toLowerCase();
const lang = SUPPORTED_LANGUAGES.find((l) => l.code.toLowerCase() === lower);
return lang?.fullName || 'English';
}

Expand Down
1,676 changes: 1,676 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions packages/cli/src/ui/hooks/usePhraseCycler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { useState, useEffect, useRef, useMemo } from 'react';
import { t, ta } from '../../i18n/index.js';
import { t, ta, getCurrentLanguage } from '../../i18n/index.js';

export const WITTY_LOADING_PHRASES: string[] = ["I'm Feeling Lucky"];

Expand All @@ -23,6 +23,7 @@ export const usePhraseCycler = (
customPhrases?: string[],
) => {
// Get phrases from translations if available
const currentLanguage = getCurrentLanguage();
const loadingPhrases = useMemo(() => {
if (customPhrases && customPhrases.length > 0) {
return customPhrases;
Expand All @@ -31,7 +32,8 @@ export const usePhraseCycler = (
return translatedPhrases.length > 0
? translatedPhrases
: WITTY_LOADING_PHRASES;
}, [customPhrases]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [customPhrases, currentLanguage]);

const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
loadingPhrases[0],
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/utils/languageUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ vi.mock('../i18n/index.js', () => ({
getLanguageNameFromLocale: vi.fn((locale: string) => {
const map: Record<string, string> = {
en: 'English',
'zh-tw': 'Traditional Chinese',
zh: 'Chinese',
ru: 'Russian',
de: 'German',
Expand All @@ -30,7 +31,7 @@ vi.mock('../i18n/index.js', () => ({
fr: 'French',
es: 'Spanish',
};
return map[locale] || 'English';
return map[locale.toLowerCase()] || 'English';
}),
}));

Expand Down Expand Up @@ -123,6 +124,12 @@ describe('languageUtils', () => {
expect(normalizeOutputLanguage('Ru')).toBe('Russian');
});

it('should convert "zh-TW" (mixed case) to "Traditional Chinese"', () => {
expect(normalizeOutputLanguage('zh-TW')).toBe('Traditional Chinese');
expect(normalizeOutputLanguage('zh-tw')).toBe('Traditional Chinese');
expect(normalizeOutputLanguage('ZH-TW')).toBe('Traditional Chinese');
});

it('should preserve explicit language names as-is', () => {
expect(normalizeOutputLanguage('Japanese')).toBe('Japanese');
expect(normalizeOutputLanguage('French')).toBe('French');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,11 +405,8 @@ describe('LoggingContentGenerator', () => {

it('uses generator modalities when converting logged OpenAI requests', async () => {
convertGeminiRequestToOpenAISpy.mockImplementationOnce(
(request, requestContext, options) => realConvertGeminiRequestToOpenAI(
request,
requestContext,
options,
),
(request, requestContext, options) =>
realConvertGeminiRequestToOpenAI(request, requestContext, options),
);

const wrapped = createWrappedGenerator(
Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/qwen/qwenOAuth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,10 @@ export enum QwenOAuth2Event {
export type AuthResult =
| { success: true }
| {
success: false;
reason: 'timeout' | 'cancelled' | 'error' | 'rate_limit';
message?: string; // Detailed error message for better error reporting
};
success: false;
reason: 'timeout' | 'cancelled' | 'error' | 'rate_limit';
message?: string; // Detailed error message for better error reporting
};

/**
* Global event emitter instance for QwenOAuth2 authentication events
Expand Down Expand Up @@ -731,13 +731,14 @@ async function authWithQwenDeviceFlow(
debugLogger.info(`Please open this URL manually: ${url}`);
});

// Optional: Also listen for 'close' or 'exit' if needed for cleanup,
// Optional: Also listen for 'close' or 'exit' if needed for cleanup,
// but 'error' is the main crasher.
} else {
// Fallback: If open() didn't return a valid process object, log a warning
debugLogger.debug('open() did not return a valid child process object.');
debugLogger.debug(
'open() did not return a valid child process object.',
);
}

} catch (err) {
// Handle synchronous errors or promise rejections from open()
const errorMessage = err instanceof Error ? err.message : String(err);
Expand Down
3 changes: 2 additions & 1 deletion packages/vscode-ide-companion/schemas/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@
"default": false
},
"language": {
"description": "The language for the user interface. Use \"auto\" to detect from system settings. You can also use custom language codes (e.g., \"es\", \"fr\") by placing JS language files in ~/.qwen/locales/ (e.g., ~/.qwen/locales/es.js). Options: auto, en, zh, ru, de, ja, pt, fr",
"description": "The language for the user interface. Use \"auto\" to detect from system settings. You can also use custom language codes (e.g., \"es\", \"fr\") by placing JS language files in ~/.qwen/locales/ (e.g., ~/.qwen/locales/es.js). Options: auto, en, zh-TW, zh, ru, de, ja, pt, fr",
"enum": [
"auto",
"en",
"zh-TW",
"zh",
"ru",
"de",
Expand Down
79 changes: 64 additions & 15 deletions scripts/check-i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { fileURLToPath } from 'url';
import { dirname } from 'path';

// Get __dirname for ESM modules
// @ts-expect-error - import.meta is supported in NodeNext module system at runtime
const __dirname = dirname(fileURLToPath(import.meta.url));

interface CheckResult {
Expand All @@ -23,6 +22,7 @@ interface CheckResult {
stats: {
totalKeys: number;
translatedKeys: number;
zhTWTranslatedKeys: number;
unusedKeys: string[];
unusedKeysOnlyInLocales?: string[]; // 新增:只在 locales 中存在的未使用键
};
Expand Down Expand Up @@ -172,27 +172,31 @@ function checkKeyValueConsistency(
}

/**
* Check if en.js and zh.js have matching keys
* Check if locale files have matching keys with en.js
* @param enTranslations The en.js translations
* @param localeTranslations The target locale translations (zh.js or zh-TW.js)
* @param localeLabel Label for diagnostics (e.g., "zh.js" or "zh-TW.js")
*/
function checkKeyMatching(
enTranslations: Record<string, string | string[]>,
zhTranslations: Record<string, string | string[]>,
localeTranslations: Record<string, string | string[]>,
localeLabel: string,
): string[] {
const errors: string[] = [];
const enKeys = new Set(Object.keys(enTranslations));
const zhKeys = new Set(Object.keys(zhTranslations));
const localeKeys = new Set(Object.keys(localeTranslations));

// Check for keys in en but not in zh
// Check for keys in en but not in locale
for (const key of enKeys) {
if (!zhKeys.has(key)) {
errors.push(`Missing translation in zh.js: "${key}"`);
if (!localeKeys.has(key)) {
errors.push(`Missing translation in ${localeLabel}: "${key}"`);
}
}

// Check for keys in zh but not in en
for (const key of zhKeys) {
// Check for keys in locale but not in en
for (const key of localeKeys) {
if (!enKeys.has(key)) {
errors.push(`Extra key in zh.js (not in en.js): "${key}"`);
errors.push(`Extra key in ${localeLabel} (not in en.js): "${key}"`);
}
}

Expand Down Expand Up @@ -304,10 +308,12 @@ async function checkI18n(): Promise<CheckResult> {

const enPath = path.join(localesDir, 'en.js');
const zhPath = path.join(localesDir, 'zh.js');
const zhTWPath = path.join(localesDir, 'zh-TW.js');

// Load translation files
let enTranslations: Record<string, string | string[]>;
let zhTranslations: Record<string, string | string[]>;
let zhTWTranslations: Record<string, string | string[]>;

try {
enTranslations = await loadTranslationsFile(enPath);
Expand All @@ -319,7 +325,12 @@ async function checkI18n(): Promise<CheckResult> {
success: false,
errors,
warnings,
stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] },
stats: {
totalKeys: 0,
translatedKeys: 0,
zhTWTranslatedKeys: 0,
unusedKeys: [],
},
};
}

Expand All @@ -333,7 +344,31 @@ async function checkI18n(): Promise<CheckResult> {
success: false,
errors,
warnings,
stats: { totalKeys: 0, translatedKeys: 0, unusedKeys: [] },
stats: {
totalKeys: 0,
translatedKeys: 0,
zhTWTranslatedKeys: 0,
unusedKeys: [],
},
};
}

try {
zhTWTranslations = await loadTranslationsFile(zhTWPath);
} catch (error) {
errors.push(
`Failed to load zh-TW.js: ${error instanceof Error ? error.message : String(error)}`,
);
return {
success: false,
errors,
warnings,
stats: {
totalKeys: 0,
translatedKeys: 0,
zhTWTranslatedKeys: 0,
unusedKeys: [],
},
};
}

Expand All @@ -342,9 +377,21 @@ async function checkI18n(): Promise<CheckResult> {
errors.push(...consistencyErrors);

// Check key matching between en and zh
const matchingErrors = checkKeyMatching(enTranslations, zhTranslations);
const matchingErrors = checkKeyMatching(
enTranslations,
zhTranslations,
'zh.js',
);
errors.push(...matchingErrors);

// Check key matching between en and zh-TW
const matchingTWErrors = checkKeyMatching(
enTranslations,
zhTWTranslations,
'zh-TW.js',
);
errors.push(...matchingTWErrors);

// Extract used keys from source code
const usedKeys = await extractUsedKeys(sourceDir);

Expand All @@ -363,15 +410,17 @@ async function checkI18n(): Promise<CheckResult> {
}

const totalKeys = Object.keys(enTranslations).length;
const translatedKeys = Object.keys(zhTranslations).length;
const zhTranslatedKeys = Object.keys(zhTranslations).length;
const zhTWTranslatedKeys = Object.keys(zhTWTranslations).length;

return {
success: errors.length === 0,
errors,
warnings,
stats: {
totalKeys,
translatedKeys,
translatedKeys: zhTranslatedKeys,
zhTWTranslatedKeys,
unusedKeys,
unusedKeysOnlyInLocales,
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/unused-keys-only-in-locales.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-04-22T15:40:32.318Z",
"generatedAt": "2026-04-24T07:06:13.173Z",
"keys": [
" Models: Qwen latest models\n",
" qwen auth qwen-oauth - Authenticate with Qwen OAuth (discontinued)",
Expand Down
Loading