13e0ba6d47
- gen_l10n_dart.mjs 去掉 dart format:其输出依赖包解析状态、跨环境不稳(首次 4 空格、 重跑 8 空格),会让漂移闸误报。改为确定性终态格式(list 单行 const [])。 analyze 不校验格式,不影响;gen 现零 dart 依赖、纯 node 确定。 - ci/check-codegen-drift.sh 增 l10n 段:重生成 strings_*.dart → git diff 非空即 fail (与 token 段同法,纯 node)。 - 验证:往返无损、重生成零 diff(闸稳定)、flutter analyze 0 issue、244 测试全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
79 lines
3.3 KiB
JavaScript
79 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
||
// gen_l10n_dart.mjs — l10n 单源 codegen
|
||
//
|
||
// 真源:design/i18n/strings.json(getters + lists + methods × 6 语)。
|
||
// 生成:client/lib/l10n/strings_{zh,en,ja,ko,ru,es}.dart(concrete AppText 实现)。
|
||
// 手写保留:client/lib/l10n/app_text.dart(enum AppLang / abstract AppText 声明 /
|
||
// AppLangLabel / AppLangMisc 逻辑)。Dart 编译器强制 concrete⊆abstract 兜底漂移。
|
||
//
|
||
// 用法:node design/codegen/gen_l10n_dart.mjs
|
||
// 输出为确定性终态格式,不跑 dart format(其结果依赖包解析状态、跨环境不稳,
|
||
// 会让漂移闸误报)。漂移闸 = 纯 node 重生成 + git diff,无 dart 依赖。
|
||
// analyze 不校验格式,故手控格式即可;若单独启用 `dart format --set-exit-if-changed`,
|
||
// 需让本文件输出与之对齐(当前未启用)。
|
||
|
||
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 SRC = path.join(ROOT, 'design/i18n/strings.json');
|
||
const OUT_DIR = path.join(ROOT, 'client/lib/l10n');
|
||
|
||
const LANGS = ['zh', 'en', 'ja', 'ko', 'ru', 'es'];
|
||
const CLASS = { zh: 'StringsZh', en: 'StringsEn', ja: 'StringsJa', ko: 'StringsKo', ru: 'StringsRu', es: 'StringsEs' };
|
||
const HEADER_NOTE = { zh: '中文', en: 'English', ja: '日本語', ko: '한국어', ru: 'Русский', es: 'Español' };
|
||
|
||
// 字面量转义(getter / list 元素):无插值,$ 一律转义为字面量。
|
||
function litStr(s) {
|
||
return s.replace(/\\/g, '\\\\').replace(/\$/g, '\\$').replace(/'/g, "\\'").replace(/\n/g, '\\n');
|
||
}
|
||
// 模板转义(method):保留 $var 插值,仅转义反斜杠/引号/换行。
|
||
function tplStr(s) {
|
||
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
|
||
}
|
||
|
||
const data = JSON.parse(fs.readFileSync(SRC, 'utf8'));
|
||
const { getters, lists, methods } = data;
|
||
|
||
function genFile(lang) {
|
||
const L = [];
|
||
L.push(`// strings_${lang}.dart — ${HEADER_NOTE[lang]}文案(自动生成 · 勿手改)`);
|
||
L.push('//');
|
||
L.push('// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json.');
|
||
L.push('// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。');
|
||
L.push(`import 'app_text.dart';`);
|
||
L.push('');
|
||
L.push(`class ${CLASS[lang]} extends AppText {`);
|
||
L.push(` const ${CLASS[lang]}();`);
|
||
L.push('');
|
||
L.push(' @override');
|
||
L.push(` AppLang get lang => AppLang.${lang};`);
|
||
L.push('');
|
||
for (const [k, m] of Object.entries(getters)) {
|
||
L.push(' @override');
|
||
L.push(` String get ${k} => '${litStr(m[lang])}';`);
|
||
}
|
||
for (const [k, m] of Object.entries(lists)) {
|
||
const items = m[lang].map((x) => `'${litStr(x)}'`).join(', ');
|
||
L.push(' @override');
|
||
L.push(` List<String> get ${k} => const [${items}];`);
|
||
}
|
||
for (const [k, m] of Object.entries(methods)) {
|
||
L.push(' @override');
|
||
L.push(` String ${k}(${m.args}) => '${tplStr(m.tpl[lang])}';`);
|
||
}
|
||
L.push('}');
|
||
L.push('');
|
||
return L.join('\n');
|
||
}
|
||
|
||
const written = [];
|
||
for (const lang of LANGS) {
|
||
const out = path.join(OUT_DIR, `strings_${lang}.dart`);
|
||
fs.writeFileSync(out, genFile(lang));
|
||
written.push(out);
|
||
}
|
||
const g = Object.keys(getters).length, l = Object.keys(lists).length, me = Object.keys(methods).length;
|
||
console.log(`✓ 生成 ${written.length} 份:getters=${g} lists=${l} methods=${me} × ${LANGS.length} 语`);
|