/** * @fileoverview zh-CN coverage for the tab's exited-agent badge (and the tab's * accessible name that carries it). * * Every form `paneExitLabel()` produces (plain, an exit code, a negative code, * a signal) must translate fully through i18n.js's real `t()`, with the * session name in the accessible name passed through untranslated (a name * that is itself a dictionary word included), and read unchanged in English. * * Port: N/A. */ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import vm from 'node:vm'; import { JSDOM } from 'jsdom'; import { afterAll, describe, expect, it } from 'vitest'; const PUBLIC = resolve(import.meta.dirname, '../src/web/public'); const read = (f: string) => readFileSync(resolve(PUBLIC, f), 'utf8'); const I18N = read('i18n.js'); const APP = read('app.js'); type Api = { t(s: string): string; configure(o: Record): void }; function translator(language: string) { const dom = new JSDOM('', { runScripts: 'outside-only', url: 'http://localhost/', }); vm.runInContext(I18N, dom.getInternalVMContext(), { filename: 'i18n.js' }); const api = (dom.window as unknown as { CodemanI18n: Api }).CodemanI18n; api.configure({ language }); return { api, dom }; } const zh = translator('zh-CN'); const en = translator('en'); afterAll(() => { zh.dom.window.close(); en.dom.window.close(); }); /** Latin words left after removing what may stay: key names, the AI acronym, the N placeholder. */ const leftover = (text: string) => text .replace(/\b(Ctrl|Cmd|Shift|Alt|Option)\+\w+/g, '') .replace(/\b(Ctrl|Cmd|Shift|Alt|Option|Enter|Tab|Space|End|Home|Escape|G|AI|N)\b/g, '') .match(/[A-Za-z]+/g) ?? []; const helpers = new Function( `${APP.match(/function paneExitLabel\([\s\S]*?\n\}/)![0]}\n${APP.match(/function paneExitAriaLabel\([\s\S]*?\n\}/)![0]}\n` + 'return { paneExitLabel, paneExitAriaLabel };' )() as { paneExitLabel(p: unknown): string; paneExitAriaLabel(name: string, label: string): string }; const EXITS = [{}, { status: 0 }, { status: 3 }, { status: 137 }, { status: -1 }, { signal: 9 }]; describe('the exited-agent badge in zh-CN', () => { const labels = EXITS.map((p) => helpers.paneExitLabel({ ...p, at: 1 })); it('produces the forms this test covers', () => { expect(labels).toEqual(['exited', 'exited (0)', 'exited (3)', 'exited (137)', 'exited (-1)', 'exited (signal 9)']); }); it('every badge form translates fully, and reads the same in English', () => { const bad = labels.filter((l) => { const text = zh.api.t(l); return text === l || leftover(text).length > 0 || en.api.t(l) !== l; }); expect(bad).toEqual([]); expect(zh.api.t('exited (3)')).toBe('已退出(3)'); expect(zh.api.t('exited (signal 9)')).toBe('已退出(信号 9)'); }); it("the tab's accessible name translates around the session name, which stays as typed", () => { for (const name of ['w1-case', 'Open tiles', 'Tiles']) { for (const label of labels) { const source = helpers.paneExitAriaLabel(name, label); const text = zh.api.t(source); expect(text.startsWith(`${name} 会话,智能体已退出`), `${source} -> ${text}`).toBe(true); expect(leftover(text.slice(name.length))).toEqual([]); expect(en.api.t(source)).toBe(source); } } }); }); describe('the Run button family in zh-CN', () => { const SESSION_UI = read('session-ui.js'); const STOCK = JSON.parse(readFileSync(resolve(import.meta.dirname, '../config/clis.stock.json'), 'utf8')) as Array<{ shortBadge: string; }>; // What _applyRunMode can show: its hard-coded labels, and `Run ` for any registry CLI. const applyRunMode = SESSION_UI.slice( SESSION_UI.indexOf(' _applyRunMode() {'), SESSION_UI.indexOf(' sendEnterKey() {') ); const hardCoded = [...applyRunMode.matchAll(/'(Run(?: [A-Z]+)?)'/g)].map((m) => m[1]); const fromRegistry = STOCK.map((e) => `Run ${e.shortBadge}`); it('covers the hard-coded labels and every stock mode code', () => { expect(hardCoded).toEqual(expect.arrayContaining(['Run SH', 'Run OC', 'Run CX', 'Run OMP', 'Run'])); expect(fromRegistry).toEqual(expect.arrayContaining(['Run CC', 'Run SH', 'Run OM'])); }); it('"Run" becomes 运行, the mode code and product names stay, English unchanged', () => { for (const label of new Set([...hardCoded, ...fromRegistry])) { const text = zh.api.t(label); const code = label.slice(4); // A code that is also a product name has its own entry, matched without // case ("Run PI" -> the "Run Pi" entry, 运行 Pi): the code survives either way. expect(text.startsWith('运行'), label).toBe(true); expect(text.slice(2).trim().toLowerCase(), label).toBe(code.toLowerCase()); expect(en.api.t(label)).toBe(label); } }); it('the toolbar around it: titles and the Shell button', () => { for (const s of [ 'Run Shell', 'Select AI backend', 'Terminal / Shell', 'Send Enter', 'Instance count', 'Stop (Ctrl+C)', ]) { const text = zh.api.t(s); expect(text, s).not.toBe(s); // "Shell" stays, as the table already had it (运行 Shell). expect(leftover(text.replace(/Shell/g, '')), s).toEqual([]); expect(en.api.t(s)).toBe(s); } }); }); describe('the Help modal and the shortcut overlay in zh-CN', () => { const dom = new JSDOM(read('index.html'), { runScripts: 'outside-only', url: 'http://localhost/' }); vm.runInContext(I18N, dom.getInternalVMContext(), { filename: 'i18n.js' }); const doc = dom.window.document; const control = doc.createElement('button'); control.textContent = 'Home'; doc.body.appendChild(control); const api = (dom.window as unknown as { CodemanI18n: Api & { start(): void } }).CodemanI18n; api.start(); api.configure({ language: 'zh-CN' }); it('no English left in the Help modal outside the key names', () => { const left: string[] = []; const walk = (el: Element) => { for (const node of el.childNodes) { if (node.nodeType === 3) { if (node.parentElement?.tagName !== 'KBD' && leftover(node.nodeValue ?? '').length) left.push(node.nodeValue!.trim()); } else if (node.nodeType === 1) walk(node as Element); } }; walk(doc.getElementById('helpModal')!); expect(left).toEqual([]); }); it('the Home KEY stays Home, while the word Home elsewhere still translates', () => { const homeKey = [...doc.querySelectorAll('#helpModal kbd')].find((k) => k.closest('div')?.nextElementSibling); const keys = [...doc.querySelectorAll('#helpModal kbd')].map((k) => k.textContent); expect(keys).toContain('Home'); expect(keys).not.toContain('主页'); // Mouse inputs in the key column do translate (as Click / Right-click do). expect(keys).toContain('滚轮'); expect(homeKey).toBeTruthy(); expect(control.textContent).toBe('主页'); }); it('every shortcut registry group and label translates (the overlay and the App Settings list)', () => { const start = APP.indexOf('const DEFAULT_SHORTCUTS = ['); const registry = APP.slice(start, APP.indexOf('\n];', start)); const pairs = [...registry.matchAll(/group: '([^']+)',\s*label: '([^']+)'/g)]; expect(pairs.length).toBeGreaterThan(20); const bad = pairs.flatMap(([, group, label]) => [group, label].filter((s) => zh.api.t(s) === s || leftover(zh.api.t(s)).length > 0) ); expect([...new Set(bad)]).toEqual([]); }); it("the overlay's key column is never translated", () => { const overlay = APP.slice(APP.indexOf(' renderShortcutOverlay() {'), APP.indexOf(' closeShortcutOverlay() {')); expect(overlay.match(//g)).toHaveLength(2); expect(overlay).not.toMatch(//); }); });