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', {