feat(i18n): ③ 原型接单源 — I18N 块由 strings.json 生成,消除重复翻译
原型 design/prototype/screens/{ui-desktop,ui-mobile}.html 的 I18N 数据块不再手写,
改由 strings.json 生成(marker 包裹、生成器唯一写):
- alias.json:protoKey → flutterKey(产品串取单源 zh/en,单源赢);desktop 130 / mobile 143。
- overlay.json:原型专属(开发挡板 + app 没有的串,zh/en);desktop 98 / mobile 104。
- new-strings.json:4 个新产品串(分流规则/自定义/局域网直连/强制),6 语,已并入
strings.json + app_text.dart 抽象声明 + 重生成 6 份 strings_*.dart。
- gen_proto_i18n.mjs:strings.json+alias+overlay → 两份 HTML 的 I18N 块;--check 只比对
marker 块(unrelated HTML 编辑不误报)。
- check-proto-i18n.mjs:两份 HTML 所有 data-i18n key 的 zh/en 必须 resolve。
- ci/check-codegen-drift.sh:加原型 i18n 段(--check 漂移 + resolution 验收)。
同带入之前路由原型 WIP 的 i18n 收口:私有服务内置行→局域网/私网直连行、rr.* 接 data-i18n
(rr.lan→routingLanDirect 等)。路由页其余 UI(段选/添加规则页)仍为独立后续。
验收(自复核全绿):生成器跑通、data-i18n 0 unresolved、两生成器重生成零 diff、
flutter analyze lib/l10n 0 issue、flutter test 244 全绿、new-strings 无红线词。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
// check-proto-i18n.mjs — 原型 i18n 验收闸
|
||||
//
|
||||
// 解析 design/prototype/screens/{ui-desktop,ui-mobile}.html 里所有 data-i18n /
|
||||
// data-i18n-ph 用到的 key,断言每个 key 在生成的 I18N 块里 **zh 与 en 都非空**
|
||||
// (产品 key 经 alias 来自 strings.json,挡板 key 来自 overlay)。任一 unresolved
|
||||
// → 非零退出并列出。
|
||||
//
|
||||
// 用法: node design/prototype/tools/check-proto-i18n.mjs
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..', '..', '..');
|
||||
const SCREENS = path.join(ROOT, 'design/prototype/screens');
|
||||
|
||||
// 允许 en 为空的装饰性 key(如 latin 品牌副标,en 态刻意留空以隐藏)
|
||||
const EMPTY_EN_OK = { 'ui-mobile.html': new Set(['brandLatin']) };
|
||||
|
||||
// 从 marker 之间 eval 出 I18N 对象
|
||||
function loadI18N(src) {
|
||||
const s = src.indexOf('// <<<gen-i18n>>>');
|
||||
const e = src.indexOf('// <<<end-gen-i18n>>>');
|
||||
const block = src.slice(s, e);
|
||||
// 锚定到大写 `I18N` 声明的 `{`(跳过 marker 注释里出现的小写 i18n/{...})
|
||||
const braceStart = block.indexOf('{', block.indexOf('I18N'));
|
||||
let depth = 0, i = braceStart, inStr = null;
|
||||
for (; i < block.length; i++) {
|
||||
const c = block[i], p = block[i - 1];
|
||||
if (inStr) { if (c === inStr && p !== '\\') inStr = null; continue; }
|
||||
if (c === "'" || c === '"' || c === '`') { inStr = c; continue; }
|
||||
if (c === '{') depth++;
|
||||
else if (c === '}') { depth--; if (depth === 0) { i++; break; } }
|
||||
}
|
||||
const obj = new Function('return (' + block.slice(braceStart, i) + ')')();
|
||||
// desktop: {k:{zh,en}} → 归一; mobile: {zh:{k},en:{k}} → 归一
|
||||
if (obj.zh && obj.en && typeof obj.zh === 'object') {
|
||||
const flat = {};
|
||||
for (const k of Object.keys(obj.zh)) flat[k] = { zh: obj.zh[k], en: obj.en[k] };
|
||||
return flat;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 抽取 HTML 里所有 data-i18n / data-i18n-ph 引用的 key
|
||||
function usedKeys(src) {
|
||||
const set = new Set();
|
||||
for (const m of src.matchAll(/data-i18n(?:-ph)?="([^"]+)"/g)) set.add(m[1]);
|
||||
return set;
|
||||
}
|
||||
|
||||
let failed = 0;
|
||||
for (const file of ['ui-desktop.html', 'ui-mobile.html']) {
|
||||
const src = fs.readFileSync(path.join(SCREENS, file), 'utf8');
|
||||
const dict = loadI18N(src);
|
||||
const used = usedKeys(src);
|
||||
const emptyOk = EMPTY_EN_OK[file] || new Set();
|
||||
const unresolved = [];
|
||||
for (const k of [...used].sort()) {
|
||||
const v = dict[k];
|
||||
if (!v) { unresolved.push(`${k}: 缺失(不在生成的 I18N)`); continue; }
|
||||
if (v.zh == null || v.zh === '') unresolved.push(`${k}: zh 为空`);
|
||||
if ((v.en == null || v.en === '') && !emptyOk.has(k)) unresolved.push(`${k}: en 为空`);
|
||||
}
|
||||
if (unresolved.length) {
|
||||
failed += unresolved.length;
|
||||
console.error(`✗ ${file}: ${unresolved.length} unresolved`);
|
||||
unresolved.forEach((u) => console.error(` ${u}`));
|
||||
} else {
|
||||
console.log(`✓ ${file}: ${used.size} data-i18n keys 全部 resolved(zh/en 非空)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) { console.error(`\n✗ 共 ${failed} 项 unresolved`); process.exit(1); }
|
||||
console.log('✓ 原型 i18n 验收通过');
|
||||
Reference in New Issue
Block a user