f76e3cdffd
② 单源 codegen 落地:
- strings.json 补全 lists 段(days7/featsFree/featsPro/featsTeam),
之前 extraction 漏了 4 个 List<String> getter;现 246 getter+4 list+5 method×6 语。
- design/codegen/gen_l10n_dart.mjs:strings.json → strings_{zh,en,ja,ko,ru,es}.dart,
getter 转义 \$、method 保留 $var 插值,dart format 定型(保证漂移闸零 diff)。
- app_text.dart 保持手写(enum/abstract 声明/AppLangMisc 逻辑),编译器强制 concrete⊆abstract 兜底。
- 往返无损:生成的 Dart 重新抽取 == strings.json 逐字节一致;flutter analyze 0 issue、flutter test 244 全绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
84 lines
3.4 KiB
JavaScript
84 lines
3.4 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`,保证 CI「重生成零 diff」闸稳定(漂移闸 = 重生成 + git diff)。
|
||
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import url from 'node:url';
|
||
import { execFileSync } from 'node:child_process';
|
||
|
||
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);
|
||
}
|
||
|
||
// dart format 定型,保证零 diff 闸稳定
|
||
try {
|
||
execFileSync('dart', ['format', ...written], { cwd: ROOT, stdio: 'inherit' });
|
||
} catch (e) {
|
||
console.warn('⚠ dart format 失败(dart 不在 PATH?),已生成未格式化文件:', e.message);
|
||
}
|
||
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} 语`);
|