Files
pangolin/design/codegen/gen_l10n_dart.mjs
T
wangjia 13e0ba6d47 feat(i18n): ④ l10n 漂移闸 + codegen 改纯 node 确定性输出
- 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
2026-07-27 08:55:21 +08:00

79 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}`);