Auto-detect LLM output language from system locale on first startup

This commit is contained in:
Alexander Farber 2025-12-13 15:19:55 +01:00
parent a92be72e88
commit 4d9f25e9fe
No known key found for this signature in database
5 changed files with 259 additions and 1 deletions

View file

@ -15,6 +15,7 @@ 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 { initializeLlmOutputLanguage } from '../ui/commands/languageCommand.js';
export interface InitializationResult {
authError: string | null;
@ -41,6 +42,9 @@ export async function initializeApp(
'auto';
await initializeI18n(languageSetting);
// Auto-detect and set LLM output language on first use
initializeLlmOutputLanguage();
const authType = settings.merged.security?.auth?.selectedType;
const authError = await performInitialAuth(config, authType);

View file

@ -46,17 +46,27 @@ const getLocalePath = (
return path.join(baseDir, `${lang}.js`);
};
// Supported locale codes mapped to English language names
const LOCALE_TO_LANGUAGE_NAME: Record<string, string> = {
zh: 'Chinese',
en: 'English',
ru: 'Russian',
de: 'German',
};
// 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';
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';
} catch {
// Fallback to default
}
@ -64,6 +74,14 @@ export function detectSystemLanguage(): SupportedLanguage {
return 'en';
}
/**
* Maps a locale code to its English language name.
* Used for LLM output language instructions.
*/
export function getLanguageNameFromLocale(locale: SupportedLanguage): string {
return LOCALE_TO_LANGUAGE_NAME[locale] || 'English';
}
// Translation loading
async function loadTranslationsAsync(
lang: SupportedLanguage,

View file

@ -13,6 +13,16 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
vi.mock('../../i18n/index.js', () => ({
setLanguageAsync: vi.fn().mockResolvedValue(undefined),
getCurrentLanguage: vi.fn().mockReturnValue('en'),
detectSystemLanguage: vi.fn().mockReturnValue('en'),
getLanguageNameFromLocale: vi.fn((locale: string) => {
const map: Record<string, string> = {
zh: 'Chinese',
en: 'English',
ru: 'Russian',
de: 'German',
};
return map[locale] || 'English';
}),
t: vi.fn((key: string) => key),
}));
@ -61,7 +71,10 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
// Import modules after mocking
import * as i18n from '../../i18n/index.js';
import { languageCommand } from './languageCommand.js';
import {
languageCommand,
initializeLlmOutputLanguage,
} from './languageCommand.js';
describe('languageCommand', () => {
let mockContext: CommandContext;
@ -597,4 +610,74 @@ describe('languageCommand', () => {
});
});
});
describe('initializeLlmOutputLanguage', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);
vi.mocked(fs.writeFileSync).mockImplementation(() => undefined);
});
it('should create file when it does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(i18n.detectSystemLanguage).mockReturnValue('en');
initializeLlmOutputLanguage();
expect(fs.mkdirSync).toHaveBeenCalled();
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('output-language.md'),
expect.stringContaining('English'),
'utf-8',
);
});
it('should NOT overwrite existing file', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
initializeLlmOutputLanguage();
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
it('should detect Chinese locale and create Chinese rule file', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(i18n.detectSystemLanguage).mockReturnValue('zh');
initializeLlmOutputLanguage();
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('output-language.md'),
expect.stringContaining('Chinese'),
'utf-8',
);
});
it('should detect Russian locale and create Russian rule file', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(i18n.detectSystemLanguage).mockReturnValue('ru');
initializeLlmOutputLanguage();
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('output-language.md'),
expect.stringContaining('Russian'),
'utf-8',
);
});
it('should detect German locale and create German rule file', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(i18n.detectSystemLanguage).mockReturnValue('de');
initializeLlmOutputLanguage();
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('output-language.md'),
expect.stringContaining('German'),
'utf-8',
);
});
});
});

View file

@ -15,6 +15,8 @@ import { SettingScope } from '../../config/settings.js';
import {
setLanguageAsync,
getCurrentLanguage,
detectSystemLanguage,
getLanguageNameFromLocale,
type SupportedLanguage,
t,
} from '../../i18n/index.js';
@ -73,6 +75,33 @@ function getLlmOutputLanguageRulePath(): string {
);
}
/**
* Initializes the LLM output language rule file on first startup.
* If the file already exists, it is not overwritten (respects user preference).
*/
export function initializeLlmOutputLanguage(): void {
const filePath = getLlmOutputLanguageRulePath();
// Skip if file already exists (user preference)
if (fs.existsSync(filePath)) {
return;
}
// Detect system language and map to language name
const detectedLocale = detectSystemLanguage();
const languageName = getLanguageNameFromLocale(detectedLocale);
// Generate the rule file
const content = generateLlmOutputLanguageRule(languageName);
// Ensure directory exists
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
// Write file
fs.writeFileSync(filePath, content, 'utf-8');
}
/**
* Gets the current LLM output language from the rule file if it exists.
*/