diff --git a/package.json b/package.json index afaa31c..e56231a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev:stop": "taskkill /f /im vite.exe", "dev:restart": "npm run dev:stop && npm run dev", "build": "vue-tsc --noEmit && vite build", + "check:i18n": "node scripts/check-i18n.cjs", "preview": "vite preview", "test:e2e": "playwright test", "tauri": "tauri", diff --git a/scripts/check-i18n.cjs b/scripts/check-i18n.cjs new file mode 100644 index 0000000..dc8aaa2 --- /dev/null +++ b/scripts/check-i18n.cjs @@ -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 失败)`); diff --git a/scripts/hooks/rules/i18n-special-chars.sh b/scripts/hooks/rules/i18n-special-chars.sh new file mode 100644 index 0000000..0637004 --- /dev/null +++ b/scripts/hooks/rules/i18n-special-chars.sh @@ -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