diff --git a/packages/cli/src/i18n/locales/ca.js b/packages/cli/src/i18n/locales/ca.js index 5d062e27d23..18d73219d55 100644 --- a/packages/cli/src/i18n/locales/ca.js +++ b/packages/cli/src/i18n/locales/ca.js @@ -23,6 +23,8 @@ export default { 'Shell mode': 'Mode shell', 'YOLO mode': 'Mode YOLO', 'Auto mode': 'Mode auto', + 'auto_mode.entry_notice': + "Mode auto activat.\n Un classificador LLM avalua cada crida d'eina — les accions segures s'aproven automàticament,\n les arriscades es bloquegen. Sortiu: Shift+Tab o /approval-mode default.", 'plan mode': 'mode de planificació', 'auto-accept edits': 'acceptació automàtica de canvis', 'Accepting edits': 'Acceptant canvis', diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 0fd0c0c4dbd..de5c0e28e41 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -25,6 +25,8 @@ export default { 'Shell mode': 'Shell-Modus', 'YOLO mode': 'YOLO-Modus', 'Auto mode': 'Auto-Modus', + 'auto_mode.entry_notice': + 'Auto-Modus aktiviert.\n Ein LLM-Klassifikator bewertet jeden Tool-Aufruf — sichere Aktionen werden automatisch genehmigt,\n riskante werden blockiert. Beenden: Shift+Tab oder /approval-mode default.', 'plan mode': 'Planungsmodus', 'auto-accept edits': 'Änderungen automatisch akzeptieren', 'Accepting edits': 'Änderungen werden akzeptiert', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index b85c6007a69..97c928ffa1b 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -233,6 +233,8 @@ export default { 'Shell mode': 'Shell mode', 'YOLO mode': 'YOLO mode', 'Auto mode': 'Auto mode', + 'auto_mode.entry_notice': + 'Auto mode enabled.\n An LLM classifier evaluates each tool call — safe actions auto-approve,\n risky ones are blocked. Exit: Shift+Tab or /approval-mode default.', 'plan mode': 'plan mode', 'auto-accept edits': 'auto-accept edits', 'Accepting edits': 'Accepting edits', diff --git a/packages/cli/src/i18n/locales/fr.js b/packages/cli/src/i18n/locales/fr.js index 7b9806dd140..b51a965a24b 100644 --- a/packages/cli/src/i18n/locales/fr.js +++ b/packages/cli/src/i18n/locales/fr.js @@ -23,6 +23,8 @@ export default { 'Shell mode': 'Mode shell', 'YOLO mode': 'Mode YOLO', 'Auto mode': 'Mode auto', + 'auto_mode.entry_notice': + "Mode auto activé.\n Un classificateur LLM évalue chaque appel d'outil — les actions sûres sont approuvées automatiquement,\n les actions risquées sont bloquées. Quitter : Shift+Tab ou /approval-mode default.", 'plan mode': 'mode plan', 'auto-accept edits': 'acceptation automatique des modifications', 'Accepting edits': 'Acceptation des modifications', diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index 3e5bf29c41e..eb6a74ac410 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -18,7 +18,9 @@ export default { '@src/myFile.ts': '@src/myFile.ts', 'Shell mode': 'シェルモード', 'YOLO mode': 'YOLOモード', - 'Auto mode': 'Autoモード', + 'Auto mode': '自動モード', + 'auto_mode.entry_notice': + '自動モードが有効です。\n LLM 分類器が各ツール呼び出しを評価します — 安全な操作は自動承認され、\n 危険な操作はブロックされます。終了: Shift+Tab または /approval-mode default。', 'plan mode': 'プランモード', 'auto-accept edits': '編集を自動承認', 'Accepting edits': '編集を承認中', diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 1b952d8c8cb..998cbdf6a1b 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -19,6 +19,8 @@ export default { 'Shell mode': 'Modo shell', 'YOLO mode': 'Modo YOLO', 'Auto mode': 'Modo auto', + 'auto_mode.entry_notice': + 'Modo auto ativado.\n Um classificador LLM avalia cada chamada de ferramenta — ações seguras são aprovadas automaticamente,\n ações arriscadas são bloqueadas. Sair: Shift+Tab ou /approval-mode default.', 'plan mode': 'modo planejamento', 'auto-accept edits': 'aceitar edições automaticamente', 'Accepting edits': 'Aceitando edições', diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 55f48a196c2..caa64dbaa3a 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -25,6 +25,8 @@ export default { 'Shell mode': 'Режим терминала', 'YOLO mode': 'Режим YOLO', 'Auto mode': 'Автоматический режим', + 'auto_mode.entry_notice': + 'Автоматический режим включен.\n LLM-классификатор оценивает каждый вызов инструмента — безопасные действия утверждаются автоматически,\n рискованные блокируются. Выход: Shift+Tab или /approval-mode default.', 'plan mode': 'Режим планирования', 'auto-accept edits': 'Режим принятия правок', 'Accepting edits': 'Принятие правок', diff --git a/packages/cli/src/i18n/locales/zh-TW.js b/packages/cli/src/i18n/locales/zh-TW.js index 469fd486d2f..18396661f4c 100644 --- a/packages/cli/src/i18n/locales/zh-TW.js +++ b/packages/cli/src/i18n/locales/zh-TW.js @@ -219,7 +219,9 @@ export default { '@src/myFile.ts': '@src/myFile.ts', 'Shell mode': 'Shell 模式', 'YOLO mode': 'YOLO 模式', - 'Auto mode': 'Auto 模式', + 'Auto mode': '自動模式', + 'auto_mode.entry_notice': + '已啟用自動模式。\n LLM 分類器會評估每次工具呼叫 — 安全操作將自動批准,\n 有風險的操作將被阻止。退出:Shift+Tab 或 /approval-mode default。', 'plan mode': '規劃模式', 'auto-accept edits': '自動接受編輯', 'Accepting edits': '接受編輯', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 826a2a6f688..747cfb87ea8 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -223,7 +223,9 @@ export default { '@src/myFile.ts': '@src/myFile.ts', 'Shell mode': 'Shell 模式', 'YOLO mode': 'YOLO 模式', - 'Auto mode': 'Auto 模式', + 'Auto mode': '自动模式', + 'auto_mode.entry_notice': + '已启用自动模式。\n LLM 分类器会评估每次工具调用 — 安全操作将自动批准,\n 有风险的操作将被阻止。退出:Shift+Tab 或 /approval-mode default。', 'plan mode': '规划模式', 'auto-accept edits': '自动接受编辑', 'Accepting edits': '接受编辑', diff --git a/packages/cli/src/i18n/mustTranslateKeys.ts b/packages/cli/src/i18n/mustTranslateKeys.ts index cfb6b7538cf..71fde07627c 100644 --- a/packages/cli/src/i18n/mustTranslateKeys.ts +++ b/packages/cli/src/i18n/mustTranslateKeys.ts @@ -94,6 +94,7 @@ export const MUST_TRANSLATE_KEYS = [ '? for shortcuts', 'Invalid approval mode "{{arg}}". Valid modes: {{modes}}', 'Approval mode set to "{{mode}}"', + 'auto_mode.entry_notice', "Set up Qwen Code's status line UI", 'Cached (included in Input): {{tokens}}', 'By source:', diff --git a/packages/cli/src/ui/commands/approvalModeCommand.test.ts b/packages/cli/src/ui/commands/approvalModeCommand.test.ts index 77fbe2b4d93..ebe78d6f053 100644 --- a/packages/cli/src/ui/commands/approvalModeCommand.test.ts +++ b/packages/cli/src/ui/commands/approvalModeCommand.test.ts @@ -91,7 +91,7 @@ describe('approvalModeCommand', () => { expect(result.type).toBe('message'); expect(result.messageType).toBe('info'); - expect(result.content).toContain('auto-edit'); + expect(result.content).toContain('auto-accept edits'); expect(mockSetApprovalMode).toHaveBeenCalledWith('auto-edit'); }); diff --git a/packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx b/packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx index e9f58f49ad7..e486bb353a7 100644 --- a/packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx +++ b/packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx @@ -8,6 +8,7 @@ import { render } from 'ink-testing-library'; import { describe, it, expect } from 'vitest'; import { AutoAcceptIndicator } from './AutoAcceptIndicator.js'; import { ApprovalMode } from '@qwen-code/qwen-code-core'; +import { setLanguageAsync } from '../../i18n/index.js'; describe('', () => { it('renders DEFAULT mode with pause badge and Ask permissions text', () => { @@ -33,11 +34,25 @@ describe('', () => { expect(lastFrame()).toContain('auto-accept edits'); }); - it('renders AUTO mode indicator', () => { + it('renders AUTO mode indicator with the localized label', () => { const { lastFrame } = render( , ); - expect(lastFrame()).toContain('auto mode (classifier-evaluated)'); + const frame = lastFrame()!; + expect(frame).toContain('Auto mode'); + expect(frame).not.toContain('auto mode (classifier-evaluated)'); + }); + + it('renders AUTO mode indicator with the active Chinese locale', async () => { + await setLanguageAsync('zh'); + try { + const { lastFrame } = render( + , + ); + expect(lastFrame()).toContain('自动模式'); + } finally { + await setLanguageAsync('en'); + } }); it('renders YOLO mode indicator', () => { diff --git a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx index caa9234c177..6b1a851c59d 100644 --- a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx +++ b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx @@ -37,7 +37,7 @@ export const AutoAcceptIndicator: React.FC = ({ subText = cycleText; break; case ApprovalMode.AUTO: - textContent = t('auto mode (classifier-evaluated)'); + textContent = t('Auto mode'); subText = cycleText; break; case ApprovalMode.YOLO: diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts index 30d34677179..56aa6436ffa 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts @@ -14,13 +14,18 @@ import { type Mock, } from 'vitest'; import { renderHook, act } from '@testing-library/react'; -import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js'; +import { + emitAutoModeEntryNotices, + useAutoAcceptIndicator, +} from './useAutoAcceptIndicator.js'; import { Config, ApprovalMode } from '@qwen-code/qwen-code-core'; import type { Config as ActualConfigType } from '@qwen-code/qwen-code-core'; import type { Key } from './useKeypress.js'; import { useKeypress } from './useKeypress.js'; import { MessageType } from '../types.js'; +import { setLanguage, setLanguageAsync } from '../../i18n/index.js'; +import { SettingScope, type LoadedSettings } from '../../config/settings.js'; vi.mock('./useKeypress.js'); @@ -55,6 +60,13 @@ interface MockConfigInstanceShape { type UseKeypressHandler = (key: Key) => void; +function createMockSettings(autoModeAcknowledged: boolean): LoadedSettings { + return { + merged: { ui: { autoModeAcknowledged } }, + setValue: vi.fn(), + } as unknown as LoadedSettings; +} + describe('useAutoAcceptIndicator', () => { let mockConfigInstance: MockConfigInstanceShape; let capturedUseKeypressHandler: UseKeypressHandler; @@ -494,6 +506,126 @@ describe('useAutoAcceptIndicator', () => { ); }); + it('should emit the localizable AUTO mode entry notice', async () => { + await setLanguageAsync('en'); + const mockAddItem = vi.fn(); + + emitAutoModeEntryNotices({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }); + + expect(mockAddItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: + 'Auto mode enabled.\n' + + ' An LLM classifier evaluates each tool call — safe actions auto-approve,\n' + + ' risky ones are blocked. Exit: Shift+Tab or /approval-mode default.', + }, + expect.any(Number), + ); + }); + + it('should fall back to readable English when the entry notice key is not loaded', async () => { + const stderrWrite = vi + .spyOn(process.stderr, 'write') + .mockImplementation(() => true); + setLanguage('ca'); + try { + const mockAddItem = vi.fn(); + + emitAutoModeEntryNotices({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }); + + expect(mockAddItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: + 'Auto mode enabled.\n' + + ' An LLM classifier evaluates each tool call — safe actions auto-approve,\n' + + ' risky ones are blocked. Exit: Shift+Tab or /approval-mode default.', + }, + expect.any(Number), + ); + } finally { + stderrWrite.mockRestore(); + await setLanguageAsync('en'); + } + }); + + it('should persist acknowledgement after emitting the AUTO mode entry notice', async () => { + await setLanguageAsync('en'); + const mockAddItem = vi.fn(); + const mockSettings = createMockSettings(false); + + emitAutoModeEntryNotices({ + config: mockConfigInstance as unknown as ActualConfigType, + settings: mockSettings, + addItem: mockAddItem, + }); + + expect(mockAddItem).toHaveBeenCalledWith( + expect.objectContaining({ + type: MessageType.INFO, + text: expect.stringContaining('Auto mode enabled.'), + }), + expect.any(Number), + ); + expect(mockSettings.setValue).toHaveBeenCalledWith( + SettingScope.User, + 'ui.autoModeAcknowledged', + true, + ); + }); + + it('should skip the AUTO mode entry notice after acknowledgement', async () => { + await setLanguageAsync('en'); + const mockAddItem = vi.fn(); + const mockSettings = createMockSettings(true); + + emitAutoModeEntryNotices({ + config: mockConfigInstance as unknown as ActualConfigType, + settings: mockSettings, + addItem: mockAddItem, + }); + + expect(mockAddItem).not.toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining('Auto mode enabled.'), + }), + expect.any(Number), + ); + expect(mockSettings.setValue).not.toHaveBeenCalled(); + }); + + it('should emit the AUTO mode entry notice with the active locale', async () => { + await setLanguageAsync('zh'); + try { + const mockAddItem = vi.fn(); + + emitAutoModeEntryNotices({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: mockAddItem, + }); + + expect(mockAddItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: + '已启用自动模式。\n' + + ' LLM 分类器会评估每次工具调用 — 安全操作将自动批准,\n' + + ' 有风险的操作将被阻止。退出:Shift+Tab 或 /approval-mode default。', + }, + expect.any(Number), + ); + } finally { + await setLanguageAsync('en'); + } + }); + it('should not cycle approval mode on Windows when shouldBlockTab returns true', () => { const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts index 7e2e5b3bb95..4a681fd7e01 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts @@ -14,12 +14,21 @@ import { useKeypress } from './useKeypress.js'; import type { HistoryItemWithoutId } from '../types.js'; import { MessageType } from '../types.js'; import { type LoadedSettings, SettingScope } from '../../config/settings.js'; +import { t } from '../../i18n/index.js'; -const AUTO_MODE_FIRST_TIME_MESSAGE = +const AUTO_MODE_FIRST_TIME_MESSAGE_KEY = 'auto_mode.entry_notice'; +const AUTO_MODE_FIRST_TIME_MESSAGE_FALLBACK = 'Auto mode enabled.\n' + ' An LLM classifier evaluates each tool call — safe actions auto-approve,\n' + ' risky ones are blocked. Exit: Shift+Tab or /approval-mode default.'; +const getAutoModeFirstTimeMessage = () => { + const message = t(AUTO_MODE_FIRST_TIME_MESSAGE_KEY); + return message === AUTO_MODE_FIRST_TIME_MESSAGE_KEY + ? AUTO_MODE_FIRST_TIME_MESSAGE_FALLBACK + : message; +}; + export interface UseAutoAcceptIndicatorArgs { config: Config; /** Settings handle — used to read/write `ui.autoModeAcknowledged`. */ @@ -145,7 +154,7 @@ export function emitAutoModeEntryNotices(opts: { const acknowledged = settings?.merged.ui?.autoModeAcknowledged === true; if (!acknowledged) { addItem( - { type: MessageType.INFO, text: AUTO_MODE_FIRST_TIME_MESSAGE }, + { type: MessageType.INFO, text: getAutoModeFirstTimeMessage() }, now, ); if (settings) { @@ -165,6 +174,8 @@ export function emitAutoModeEntryNotices(opts: { stripped && (stripped.persistent.length > 0 || stripped.session.length > 0) ) { + // Intentionally untranslated operational notice: rule text is copied from + // user/session allow-rule configuration and may contain command syntax. const lines = [ 'ℹ Auto mode temporarily disabled these allow rules', ' (they would bypass the classifier):', diff --git a/packages/cli/src/ui/utils/approvalModeDisplay.test.ts b/packages/cli/src/ui/utils/approvalModeDisplay.test.ts index 52436927dfd..fd3819f0a00 100644 --- a/packages/cli/src/ui/utils/approvalModeDisplay.test.ts +++ b/packages/cli/src/ui/utils/approvalModeDisplay.test.ts @@ -10,23 +10,35 @@ import { formatApprovalModeDescription, formatApprovalModeName, } from './approvalModeDisplay.js'; +import { setLanguageAsync } from '../../i18n/index.js'; describe('approval mode display', () => { describe('formatApprovalModeName', () => { - it('formats yolo as uppercase', () => { - expect(formatApprovalModeName(ApprovalMode.YOLO)).toBe('YOLO'); - }); - - it('formats default mode as a friendly name', () => { + it('formats all modes as friendly names', () => { + expect(formatApprovalModeName(ApprovalMode.PLAN)).toBe('plan mode'); expect(formatApprovalModeName(ApprovalMode.DEFAULT)).toBe( 'Ask permissions', ); + expect(formatApprovalModeName(ApprovalMode.AUTO_EDIT)).toBe( + 'auto-accept edits', + ); + expect(formatApprovalModeName(ApprovalMode.AUTO)).toBe('Auto mode'); + expect(formatApprovalModeName(ApprovalMode.YOLO)).toBe('YOLO mode'); }); - it('falls back to the raw mode value for modes without a custom name', () => { - expect(formatApprovalModeName(ApprovalMode.PLAN)).toBe('plan'); - expect(formatApprovalModeName(ApprovalMode.AUTO_EDIT)).toBe('auto-edit'); - expect(formatApprovalModeName(ApprovalMode.AUTO)).toBe('auto'); + it('formats mode names with the active locale', async () => { + await setLanguageAsync('zh'); + try { + expect(formatApprovalModeName(ApprovalMode.PLAN)).toBe('规划模式'); + expect(formatApprovalModeName(ApprovalMode.DEFAULT)).toBe('请求授权'); + expect(formatApprovalModeName(ApprovalMode.AUTO_EDIT)).toBe( + '自动接受编辑', + ); + expect(formatApprovalModeName(ApprovalMode.AUTO)).toBe('自动模式'); + expect(formatApprovalModeName(ApprovalMode.YOLO)).toBe('YOLO 模式'); + } finally { + await setLanguageAsync('en'); + } }); }); @@ -51,5 +63,28 @@ describe('approval mode display', () => { 'Automatically approve all tools', ); }); + + it('formats descriptions with the active locale', async () => { + await setLanguageAsync('zh'); + try { + expect(formatApprovalModeDescription(ApprovalMode.PLAN)).toBe( + '仅分析,不修改文件或执行命令', + ); + expect(formatApprovalModeDescription(ApprovalMode.DEFAULT)).toBe( + '需要批准文件编辑或 shell 命令', + ); + expect(formatApprovalModeDescription(ApprovalMode.AUTO_EDIT)).toBe( + '自动批准文件编辑', + ); + expect(formatApprovalModeDescription(ApprovalMode.AUTO)).toBe( + '使用分类器自动批准安全的工具调用', + ); + expect(formatApprovalModeDescription(ApprovalMode.YOLO)).toBe( + '自动批准所有工具', + ); + } finally { + await setLanguageAsync('en'); + } + }); }); }); diff --git a/packages/cli/src/ui/utils/approvalModeDisplay.ts b/packages/cli/src/ui/utils/approvalModeDisplay.ts index 7c61dfe8f7f..08255166d76 100644 --- a/packages/cli/src/ui/utils/approvalModeDisplay.ts +++ b/packages/cli/src/ui/utils/approvalModeDisplay.ts @@ -9,10 +9,16 @@ import { t } from '../../i18n/index.js'; export function formatApprovalModeName(mode: ApprovalMode): string { switch (mode) { + case ApprovalMode.PLAN: + return t('plan mode'); case ApprovalMode.DEFAULT: return t('Ask permissions'); + case ApprovalMode.AUTO_EDIT: + return t('auto-accept edits'); + case ApprovalMode.AUTO: + return t('Auto mode'); case ApprovalMode.YOLO: - return 'YOLO'; + return t('YOLO mode'); default: return mode; } diff --git a/scripts/check-i18n.ts b/scripts/check-i18n.ts index eb9109e6ecb..d597c813be4 100644 --- a/scripts/check-i18n.ts +++ b/scripts/check-i18n.ts @@ -63,6 +63,11 @@ export interface PrintCheckI18nOptions { const __dirname = dirname(fileURLToPath(import.meta.url)); const WRITE_UNUSED_KEYS_FLAG = '--write-unused-locale-keys'; const WRITE_UNUSED_KEYS_ENV = 'QWEN_CHECK_I18N_WRITE_UNUSED_KEYS'; +const EN_SEMANTIC_KEY_EXCEPTIONS: ReadonlySet = new Set([ + // Multi-line copy is intentionally keyed by a stable identifier so wording + // changes do not silently break locale lookup. + 'auto_mode.entry_notice', +]); export function shouldWriteUnusedKeysJson(): boolean { return ( @@ -288,7 +293,7 @@ function checkKeyValueConsistency(enTranslations: TranslationDict): string[] { continue; } - if (key !== value) { + if (key !== value && !EN_SEMANTIC_KEY_EXCEPTIONS.has(key)) { errors.push(`Key-value mismatch in en.js: "${key}" !== "${value}"`); } } @@ -414,6 +419,15 @@ export async function checkI18n( .map((language) => language.code), ); + const builtinMustTranslateKeySet = new Set(MUST_TRANSLATE_KEYS); + for (const key of EN_SEMANTIC_KEY_EXCEPTIONS) { + if (!builtinMustTranslateKeySet.has(key)) { + errors.push( + `English semantic key exception must be listed in MUST_TRANSLATE_KEYS: "${key}"`, + ); + } + } + const localeDefinitions = supportedLanguages.map((language) => ({ code: language.code, id: language.id, diff --git a/scripts/tests/check-i18n.test.ts b/scripts/tests/check-i18n.test.ts index 4069f265ef9..d6e7eda144f 100644 --- a/scripts/tests/check-i18n.test.ts +++ b/scripts/tests/check-i18n.test.ts @@ -274,6 +274,36 @@ describe('checkI18n', () => { ); }); + it('allows the AUTO mode entry notice semantic key in en.js while preserving mismatch errors', async () => { + const { localesDir, sourceDir } = makeFixture(); + writeLocale(localesDir, 'en', { + 'auto_mode.entry_notice': 'Auto mode enabled.', + OrdinaryMismatch: 'Different English copy', + }); + writeLocale(localesDir, 'fr', { + 'auto_mode.entry_notice': 'Mode auto activé.', + OrdinaryMismatch: 'Texte français', + }); + writeSource( + sourceDir, + "t('auto_mode.entry_notice');\nt('OrdinaryMismatch');\n", + ); + + const result = await checkI18n({ + localesDir, + sourceDir, + supportedLanguages: languages('en', 'fr'), + mustTranslateKeys: ['auto_mode.entry_notice'], + }); + + expect(result.errors).not.toContain( + 'Key-value mismatch in en.js: "auto_mode.entry_notice" !== "Auto mode enabled."', + ); + expect(result.errors).toContain( + 'Key-value mismatch in en.js: "OrdinaryMismatch" !== "Different English copy"', + ); + }); + it('writes unused locale-only keys only when requested', async () => { const { root, localesDir, sourceDir } = makeFixture(); writeLocale(localesDir, 'en', {