From a3e65f42306cb0e3d16cced73585fb94cdae3f39 Mon Sep 17 00:00:00 2001 From: Savas Neto Date: Mon, 14 Sep 2026 19:17:50 -0300 Subject: [PATCH] fix(language): anchor responses to the user's language by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models occasionally drifted to the wrong language (e.g. Spanish replies in pt-BR sessions) because no language directive existed unless the user set settings.language. Now every system prompt path — including CLAUDE_CODE_SIMPLE mode — injects a locale-agnostic 'respond in the language of the user's most recent message' section, while the explicit settings.language override is preserved for users who want a fixed language. Bumps version to 0.15.25. Co-Authored-By: Verboo Code --- package.json | 2 +- src/components/LanguagePicker.tsx | 2 +- src/constants/languageAnchor.test.ts | 123 +++++++++++++++++++++++++++ src/constants/prompts.ts | 12 ++- 4 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 src/constants/languageAnchor.test.ts diff --git a/package.json b/package.json index 5a6ca82da2..c8772204c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@verboo/code", - "version": "0.15.24", + "version": "0.15.25", "description": "Verboo Code — coding agent for the Verboo platform", "type": "module", "bin": { diff --git a/src/components/LanguagePicker.tsx b/src/components/LanguagePicker.tsx index bf61433fa2..2b07012a12 100644 --- a/src/components/LanguagePicker.tsx +++ b/src/components/LanguagePicker.tsx @@ -68,7 +68,7 @@ export function LanguagePicker(t0) { } let t7; if ($[10] === Symbol.for("react.memo_cache_sentinel")) { - t7 = Leave empty for default (English); + t7 = Leave empty to respond in the user's language; $[10] = t7; } else { t7 = $[10]; diff --git a/src/constants/languageAnchor.test.ts b/src/constants/languageAnchor.test.ts new file mode 100644 index 0000000000..e610f612a9 --- /dev/null +++ b/src/constants/languageAnchor.test.ts @@ -0,0 +1,123 @@ +import { afterEach, beforeEach, expect, test } from 'bun:test' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +// MACRO is replaced at build time by Bun.define but not in test mode. +// Define it globally so tests that import modules using MACRO don't crash. +;(globalThis as Record).MACRO = { + VERSION: '99.0.0', + DISPLAY_VERSION: '0.0.0-test', + BUILD_TIME: new Date().toISOString(), + ISSUES_EXPLAINER: 'report the issue at https://github.com/verbeux-ai/code/issues', + PACKAGE_URL: '@verboo/code', + NATIVE_PACKAGE_URL: undefined, +} + +import { setClaudeConfigHomeDirForTesting } from '../utils/envUtils.js' +import { resetSettingsCache } from '../utils/settings/settingsCache.js' +import { clearSystemPromptSections } from './systemPromptSections.js' +import { getSystemPrompt } from './prompts.js' + +const MIRROR_SNIPPET = + "Always respond in the same language as the user's most recent message" +const SECTION_HEADER = '# Language' + +let dir: string + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'lang-anchor-')) + setClaudeConfigHomeDirForTesting(dir) + resetSettingsCache() + clearSystemPromptSections() +}) + +afterEach(() => { + setClaudeConfigHomeDirForTesting(undefined) + resetSettingsCache() + clearSystemPromptSections() + rmSync(dir, { recursive: true, force: true }) + delete process.env.CLAUDE_CODE_SIMPLE +}) + +test('default (unset language) injects the mirror directive', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain(SECTION_HEADER) + expect(text).toContain(MIRROR_SNIPPET) +}) + +test('default (unset language) never leaks an undefined preference', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).not.toContain('Always respond in undefined') + expect(text).not.toContain('Use undefined') +}) + +test('the default directive is locale-agnostic — pins no language', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).not.toContain('Always respond in English') + expect(text).not.toContain('Always respond in Portuguese') + expect(text).not.toContain('Always respond in Spanish') +}) + +test('explicit language override replaces the mirror directive', async () => { + writeFileSync( + join(dir, 'settings.json'), + JSON.stringify({ language: 'Brazilian Portuguese' }), + ) + resetSettingsCache() + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain('Always respond in Brazilian Portuguese.') + expect(text).not.toContain(MIRROR_SNIPPET) +}) + +test('any configured language value passes through verbatim', async () => { + writeFileSync( + join(dir, 'settings.json'), + JSON.stringify({ language: 'japanese' }), + ) + resetSettingsCache() + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain('Always respond in japanese.') + expect(text).not.toContain(MIRROR_SNIPPET) +}) + +test('simple mode system prompt also includes the language anchor', async () => { + process.env.CLAUDE_CODE_SIMPLE = '1' + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain(MIRROR_SNIPPET) +}) + +test('simple mode respects an explicit language override', async () => { + writeFileSync( + join(dir, 'settings.json'), + JSON.stringify({ language: 'english' }), + ) + resetSettingsCache() + process.env.CLAUDE_CODE_SIMPLE = '1' + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain('Always respond in english.') + expect(text).not.toContain(MIRROR_SNIPPET) +}) + +test('mirror section keeps technical terms exempt (no forced translation of code)', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain( + 'Technical terms and code identifiers should remain in their original form', + ) +}) diff --git a/src/constants/prompts.ts b/src/constants/prompts.ts index 006977f082..eaf1d7739a 100644 --- a/src/constants/prompts.ts +++ b/src/constants/prompts.ts @@ -139,10 +139,17 @@ function getAntModelOverrideSection(): string | null { return getAntModelOverrideConfig()?.defaultSystemPromptSuffix || null } +// Locale-agnostic default: mirror the user's language instead of pinning a +// fixed locale — Verboo Code is an international product. +const MIRROR_USER_LANGUAGE_SECTION = `Always respond in the same language as the user's most recent message. Use that language for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.` + function getLanguageSection( languagePreference: string | undefined, ): string | null { - if (!languagePreference) return null + if (!languagePreference) { + return `# Language +${MIRROR_USER_LANGUAGE_SECTION}` + } return `# Language Always respond in ${languagePreference}. Use ${languagePreference} for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.` @@ -450,7 +457,8 @@ export async function getSystemPrompt( if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { return [ `You are Verboo Code, an open-source coding agent and CLI.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`, - ] + getLanguageSection(getInitialSettings().language), + ].filter(s => s !== null) } const cwd = getCwd()