新增: i18n 消息特殊字符提交门禁

- 字面 @ / | / 裸 {} 未转义只在 prod runtime 抛 Invalid linked format(dev 宽松回退、vue-tsc 捕获不到),已两次致生产回归(白屏/输入框消失)
- scripts/check-i18n.cjs:用 @intlify/message-compiler 编译全部含特殊字符的 i18n 值,npm run check:i18n 可手动跑
- commit-msg 规则 i18n-special-chars.sh:暂存区含 src/i18n/ 即跑预检,失败拒提交;双向测试过(干净放行/坏值 exit1)
This commit is contained in:
lxy
2026-10-10 00:28:40 +08:00
parent a95e205eb6
commit 58834c9443
3 changed files with 80 additions and 0 deletions
+1
View File
@@ -8,6 +8,7 @@
"dev:stop": "taskkill /f /im vite.exe", "dev:stop": "taskkill /f /im vite.exe",
"dev:restart": "npm run dev:stop && npm run dev", "dev:restart": "npm run dev:stop && npm run dev",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"check:i18n": "node scripts/check-i18n.cjs",
"preview": "vite preview", "preview": "vite preview",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"tauri": "tauri", "tauri": "tauri",
+66
View File
@@ -0,0 +1,66 @@
// vue-i18n 消息预检:揪出只在 **prod runtime** 抛错的未转义特殊字符(字面 @ / | / 裸 {})。
//
// 背景:vue-i18n 9 把 message 里的字面 `@` 当 linked-message 语法、`|` 当 plural 分隔符,
// 未转义会编译报 "Invalid linked format"。该错误 dev 宽松回退、`vue-tsc` 捕获不到(message 是
// 字符串值非类型),只在打包装出的应用里炸——已两次踩坑(2026-06-15 白屏 / 2026-10-10 输入框消失),
// 且两次静态检查全绿。故前置到提交门禁 + 可手动跑:`node scripts/check-i18n.cjs`
//
// 修法:字面量用 vue-i18n 的插值转义,如 `{'@'}`(渲染仍显示 @)。
//
// 依赖 @intlify/message-compiler(vue-i18n 的编译器);解析不到时只告警不拦截(避免门禁误伤)。
const fs = require('fs');
const path = require('path');
let baseCompile;
try {
({ baseCompile } = require('@intlify/message-compiler'));
} catch {
console.warn('[i18n-check] 未找到 @intlify/message-compiler,跳过消息编译预检(需 npm 依赖)');
process.exit(0);
}
const DIRS = ['src/i18n/zh-CN', 'src/i18n/en'];
// key: 'value' / key: "value"(带转义),仅取字符串字面量值
const KV_RE = /(?:^|[\s,{])([A-Za-z0-9_$]+)\s*:\s*(['"])((?:\\.|(?!\2).)*)\2/gm;
let checked = 0;
let failed = 0;
for (const dir of DIRS) {
let files;
try {
files = fs.readdirSync(dir);
} catch {
continue;
}
for (const f of files) {
if (!f.endsWith('.ts')) continue;
const txt = fs.readFileSync(path.join(dir, f), 'utf8');
KV_RE.lastIndex = 0;
let m;
while ((m = KV_RE.exec(txt))) {
const key = m[1];
const val = m[3];
// 只编译含 vue-i18n 特殊字符的值(@ { |),其余不可能因语法报错
if (!/[@{|]/.test(val)) continue;
checked++;
const errs = [];
try {
baseCompile(val, { onError: (e) => errs.push(e), onWarn: () => {} });
} catch (e) {
errs.push(e);
}
if (errs.length) {
failed++;
console.error(`✗ ${dir}/${f} :: ${key} => ${errs[0].message}`);
console.error(` 值: ${JSON.stringify(val)}`);
console.error(` 提示: 字面 @ 须转义为 {'@'}(同理 | 与裸 {})`);
}
}
}
}
if (failed > 0) {
console.error(`[i18n-check] 失败 ${failed} 处(共编译 ${checked} 个含特殊字符的值)——prod 运行时会抛 Invalid linked format`);
process.exit(1);
}
console.log(`[i18n-check] 通过(编译 ${checked} 个含特殊字符的值,0 失败)`);
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# i18n 消息特殊字符门禁(机制优先):暂存区含 src/i18n/ 改动时,跑 vue-i18n 消息编译预检。
# 背景:字面 @ / | / 裸 {} 未转义只在 prod runtime 抛 "Invalid linked format",dev 宽松、
# vue-tsc 捕获不到,已两次导致生产回归(2026-06-15 白屏 / 2026-10-10 输入框消失)。
# 修法:字面量转义为 {'@'} 等;详见 scripts/check-i18n.cjs
git diff --cached --name-only | grep -q '^src/i18n/' || exit 0
command -v node >/dev/null 2>&1 || exit 0
node scripts/check-i18n.cjs || {
echo "✗ [i18n] 消息含未转义特殊字符(prod 运行时会报 Invalid linked format)"
echo " 字面 @ 改写为 {'@'}(同理 | 与裸 {});本机可先跑 node scripts/check-i18n.cjs"
exit 1
}
exit 0