// check-i18n.cjs 的门禁自测:用 node 内置测试跑,不引第三方依赖。 // node --test scripts/check-i18n.test.cjs // // 覆盖(对应 2026-10-10 对抗审查发现的假阴性 + 常规门禁行为): // ① 单引号值含裸 @ → 报错 // ② 双引号值含裸 @ → 报错 // ③ 反引号模板字面量含裸 @ → 报错(原实现只匹配引号,漏报) // ④ 含 ${} 插值不误报(模板字面量里的插值需先占位再编译) // ⑤ 转义写法 {'@'} → 通过 // ⑥ 含 | 与裸 {} 的用例 // 另附 scanDirectory 对临时目录样本的端到端断言。 const test = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { scanText, scanDirectory } = require('./check-i18n.cjs'); let compilerAvailable = true; try { require('@intlify/message-compiler'); } catch { compilerAvailable = false; } const opts = compilerAvailable ? {} : { skip: '@intlify/message-compiler 不可用,跳过编译相关用例' }; // 构造一份 TS 源样本:每行一个 `key: '',` const src = (...lines) => `export default {\n${lines.map((l) => ` ${l},`).join('\n')}\n}\n`; test('①单引号值含裸 @ → 报错', opts, () => { const r = scanText(src("a: 'hello @ world'")); assert.equal(r.checked, 1); assert.equal(r.violations.length, 1); assert.equal(r.violations[0].key, 'a'); }); test('②双引号值含裸 @ → 报错', opts, () => { const r = scanText(src('b: "hello @ world"')); assert.equal(r.checked, 1); assert.equal(r.violations.length, 1); assert.equal(r.violations[0].key, 'b'); }); test('③反引号模板字面量含裸 @ → 报错(假阴性回归)', opts, () => { const r = scanText(src('c: `hello @ world`')); assert.equal(r.checked, 1); assert.equal(r.violations.length, 1); assert.equal(r.violations[0].key, 'c'); }); test('④-1 模板字面量含 ${} 插值(无特殊字符)不误报', opts, () => { const r = scanText(src('d: `hello ${name}`')); // 插值被占位后不含 @ { |,不计入 checked,也无违规 assert.equal(r.violations.length, 0); }); test('④-2 插值位于花括号内不误报(占位替代的关键场景)', opts, () => { // 原值 `total {${count}}` 直接编译会报 Invalid token in placeholder: '$', // 必须先替换 ${...} 为占位符成为 `total {X}`(合法具名插值)才不误报。 const r = scanText(src('e: `total {${count}}`')); assert.equal(r.violations.length, 0); assert.equal(r.checked, 1); }); test('⑤转义写法 {\'@\'} → 通过', opts, () => { const r = scanText(src("f: \"{'@'}\"")); assert.equal(r.checked, 1); assert.equal(r.violations.length, 0); }); test('⑥-1 值含 | 会进入编译且正常通过', opts, () => { const r = scanText(src("g: 'one | two'")); assert.equal(r.checked, 1); assert.equal(r.violations.length, 0); }); test('⑥-2 值以 | 结尾(plural 缺省) → 报错', opts, () => { const r = scanText(src("h: 'one |'")); assert.equal(r.checked, 1); assert.equal(r.violations.length, 1); }); test('⑥-3 裸 { 未闭合 → 报错', opts, () => { const r = scanText(src("i: 'x { y'")); assert.equal(r.checked, 1); assert.equal(r.violations.length, 1); }); test('scanDirectory 扫临时目录样本:定位到违规文件', opts, () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-check-')); try { fs.writeFileSync(path.join(dir, 'ok.ts'), src("ok: 'plain text'")); fs.writeFileSync(path.join(dir, 'bad.ts'), src('bad: `oops @ here`')); const r = scanDirectory(dir); assert.equal(r.violations.length, 1); assert.equal(r.violations[0].file, 'bad.ts'); assert.equal(r.violations[0].key, 'bad'); } finally { fs.rmSync(dir, { recursive: true, force: true }); } });