Files
DevFlow/scripts/check-i18n.cjs
T
lxy b37279f3a2 新增: 待发箱/唤醒原语/预算随窗口 + 治理门禁与场景护栏
- 待发箱地基: V51 ai_outbox 表 + 手写 OutboxRepo(claim CAS/崩溃残留复位/瞬时失败保留) + outbox.rs 调度器(KV 默认关/Notify 唤醒/会话忙跳过不丢)
- Wake 异步唤醒原语: 三条件(delay/file-appear/process-exit)+纯判定+注入走正典 spawn_agentic_session,默认关
- 上下文预算随模型窗口: model_context.rs 三级优先(池显式>预设表>回退 128K);上界取 1M 而非 128K——max_tokens 仅裁剪预算,卡 128K 等于不治根因
- 文件租约补纯函数与单测(未另造双轨:LEASE 早随 cb260ab 落地) / 跨会话召回补纯函数单测(主体早已落地)
- 子代理: 完整卡已消化 + 子会话压缩层 + P3 批量
- 隧道/中继: 指令离线不丢 + Hello proto 版本 + 心跳与 runbook
- df-mcp: 写工具后落 KV 变更标记(供 GUI 轮询兜底,补独立 stdio 进程无感知)
- 迁移链顶点改单一真值常量 LATEST_SCHEMA_VERSION(修 2 处因 V51 失效的断言)
- 治理: i18n 特殊字符提交门禁(含反引号覆盖 + 10 自测) / 根目录卫生门禁 / 看板归档器 / 单一 lockfile(bun) / swap-install 落 temp/ 且兼容无 Get-FileHash
- 场景护栏: 流式正文可见性 e2e(playwright 免真机事件回放) + 列表续写/外置句柄 node 测试
2026-10-10 08:09:17 +08:00

112 lines
4.1 KiB
JavaScript

// 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 的编译器);解析不到时只告警不拦截(避免门禁误伤)。
//
// 扫描/编译逻辑抽为可导出函数(scanText / scanDirectory / compileMessage),供
// scripts/check-i18n.test.cjs 自测;CLI 入口行为保持不变。
const fs = require('fs');
const path = require('path');
let baseCompile = null;
try {
({ baseCompile } = require('@intlify/message-compiler'));
} catch {
baseCompile = null;
}
const DIRS = ['src/i18n/zh-CN', 'src/i18n/en'];
// key: 'value' / "value" / `value`(带转义),三种字面量都取——
// 反引号模板字面量同样可能含未转义 @/|/裸{},原先只匹配引号 → 门禁假阴性(2026-10-10 对抗审查发现)。
const KV_RE = /(?:^|[\s,{])([A-Za-z0-9_$]+)\s*:\s*([`'"])((?:\\.|(?!\2).)*)\2/gm;
// 编译单个消息值,返回错误数组(@intlify 风格:{ message })
function compileMessage(val) {
const errs = [];
try {
baseCompile(val, { onError: (e) => errs.push(e), onWarn: () => {} });
} catch (e) {
errs.push(e);
}
return errs;
}
// 扫描一段源码文本,返回 { checked, violations }
// - checked: 含 vue-i18n 特殊字符(@ { |)并已送编译的值个数
// - violations: 编译报错的条目 [{ key, value, message }]
function scanText(txt) {
const violations = [];
let checked = 0;
KV_RE.lastIndex = 0;
let m;
while ((m = KV_RE.exec(txt))) {
const key = m[1];
// 模板字面量里的 ${...} 插值对 vue-i18n 无意义,编译前替换为占位符避免假报错
const val = m[2] === '`' ? m[3].replace(/\$\{[^}]*\}/g, 'X') : m[3];
// 只编译含 vue-i18n 特殊字符的值(@ { |),其余不可能因语法报错
if (!/[@{|]/.test(val)) continue;
checked++;
const errs = compileMessage(val);
if (errs.length) violations.push({ key, value: val, message: errs[0].message });
}
return { checked, violations };
}
// 扫描目录下所有 .ts,返回 { checked, violations }(violations 带 file 字段)
function scanDirectory(dir) {
const violations = [];
let checked = 0;
let files;
try {
files = fs.readdirSync(dir);
} catch {
return { checked, violations };
}
for (const f of files) {
if (!f.endsWith('.ts')) continue;
const res = scanText(fs.readFileSync(path.join(dir, f), 'utf8'));
checked += res.checked;
for (const v of res.violations) violations.push({ file: f, ...v });
}
return { checked, violations };
}
// CLI 主流程:返回进程退出码(0 通过 / 1 失败)
function run() {
if (!baseCompile) {
console.warn('[i18n-check] 未找到 @intlify/message-compiler,跳过消息编译预检(需 npm 依赖)');
return 0;
}
let checked = 0;
let failed = 0;
for (const dir of DIRS) {
const res = scanDirectory(dir);
checked += res.checked;
for (const v of res.violations) {
failed++;
console.error(`✗ ${dir}/${v.file} :: ${v.key} => ${v.message}`);
console.error(` 值: ${JSON.stringify(v.value)}`);
console.error(` 提示: 字面 @ 须转义为 {'@'}(同理 | 与裸 {})`);
}
}
if (failed > 0) {
console.error(`[i18n-check] 失败 ${failed} 处(共编译 ${checked} 个含特殊字符的值)——prod 运行时会抛 Invalid linked format`);
return 1;
}
console.log(`[i18n-check] 通过(编译 ${checked} 个含特殊字符的值,0 失败)`);
return 0;
}
if (require.main === module) {
process.exit(run());
}
module.exports = { compileMessage, scanText, scanDirectory, run, KV_RE, DIRS };