Add correction layer to i18n that allows for corrections where the official translation is wrong
This commit is contained in:
1 parent
bc50cac01f
commit
6573431e72
5 files changed
+54
-12
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
{"version":1,"lang":"en","meta":{"generator":"tools/build-i18n.mjs","source":{"id":"assets","label":"ArknightsAssets/ArknightsGamedata (en)","home":"https://github.com/ArknightsAssets/ArknightsGamedata","season":"act2autochess"},"fallback":"tools/i18n/fallback-remake.json (the remake), tools/i18n/fallback-pr70.json (PR #70 by @YuriRestia), public/i18n/en.json","coverage":{"bands":{"texts":186,"translated":186,"official":186,"pct":100},"bonds":{"texts":138,"translated":138,"official":138,"pct":100},"bosses":{"texts":66,"translated":66,"official":66,"pct":100},"choices":{"texts":679,"translated":679,"official":679,"pct":100},"config":{"texts":110,"translated":110,"official":103,"pct":100},"effects":{"texts":1083,"translated":1083,"official":1083,"pct":100},"emotes":{"texts":41,"translated":41,"official":3,"pct":100},"enemies":{"texts":1734,"translated":1734,"official":1734,"pct":100},"factions":{"texts":14,"translated":14,"official":14,"pct":100},"garrisons":{"texts":747,"translated":747,"official":747,"pct":100},"items":{"texts":582,"translated":582,"official":570,"pct":100},"modules":{"texts":2498,"translated":2449,"official":2431,"pct":98},"operators":{"texts":876,"translated":854,"official":846,"pct":97.5},"skills":{"texts":5673,"translated":5439,"official":5439,"pct":95.9},"stages":{"texts":1124,"translated":1124,"official":1122,"pct":100},"talents":{"texts":5996,"translated":5688,"official":5680,"pct":94.9},"tokens":{"texts":458,"translated":458,"official":457,"pct":100},"traits":{"texts":1876,"translated":1860,"official":1860,"pct":99.1}},"totals":{"texts":23881,"translated":23252,"official":23158,"pct":97.4}},"names":{"\"炎佑\"":"Yan's Protection","Dor-1号失败品":"Dor-1 Failure","Dor-α号失败品":"Dor-α Failure","Dor-β号复制体":"Dor-β Replica","M3茧甲":"M3 Cocoon Shell","W·悬赏":"W/Bounty","“变形者集群”":"'Damazti Cluster'","“墓碑”":"'Gravestone'","“墓碑”·悬赏":"'Gravestone'/Bounty","“复仇者”":"'Hateful Avenger'","“复仇者”·悬赏":"'Hateful Avenger'/Bounty","“小自在”":"'Freeling'","“巨大的丑东西”":"'The Big Ugly Thing'","“巨大的丑东西”·悬赏":"'The Big Ugly Thing'/Bounty","“帝国的甲胄”":"'Armor of the Empire'","“庞贝”":"'Pompeii'","“庞贝”·悬赏":"'Pompeii'/Bounty","“斩胄之剑”":"'Armor Cleaver'","“灵幛”":"Sudaram","“独轮车玩具”":"'Unicycle Toy'","“破胄之锤”":"'Armor Crusher'","“碎铳之簧”":"'Gunshatter'","“神秘顾客”":"'Mystery Customer'","“耀阳”":"'Blazing Sun'","“自在”":"'Free'","“自在”·悬赏":"'Free'/Bounty","“萨科塔之眼”":"'Eyes of the Sankta'","“萨科塔之眼”·悬赏":"'Eyes of the Sankta'/Bounty","“萨科塔之翼”":"'Wings of the Sankta'","“萨科塔昂首”":"'Pride of the Sankta'","“萨米的意志”":"'Samivilinn,' the Resolution of the Very North","“裂管之音”/“断弦之音”":"'Echoes of Cracked Pipe'/'Echoes of Broken Strings'","“越长尘”":"'Sand Voyager'","“遗弃者”":"'Forsaken One'","“遗弃者”·悬赏":"'Forsaken One'/Bounty","“邪魔的利刃”":"'Demon's Blade'","“邪魔的利刃”·悬赏":"'Demon's Blade'/Bounty","“鼠王”·悬赏":"'Rat King'/Bounty","不屈":"Resilient","不屈弹射器":"Tough Launcher","不祥幻影":"Ominous Apparition","不稳定要素":"Unstable Element","业余竞演者":"Amateur Performer","业务指标":"Work Quota","临时收音师":"Audio Technician","主角阵营角色":"Hero's Sidekick","乌尔比安":"Ulpianus","乌顶巨角卢鲁":"Blackhorn Rhuul","九流之缘":"Streetwise Ties","人事部文档":"HR File","人才盲盒":"Talent Blind Box","仇白":"Qiubai","从不混淆的方向":"Undeviating Course","令":"Ling","以己之长":"Personal Power","伊内丝":"Ines","伊利昂的木驮兽":"Wooden Burdenbeast of Ilion","伊奥莱塔·罗素":"Ioleta Russell","伊芙利特":"Ifrit","休眠子裔":"Dormant Heir","休谟斯":"Humus","休露丝":"Sciurus","众志合一":"Wills As One","优等生":"Top Student","传奇重犯":"Infamous Recidivist","伪装服":"Disguise Outfit","伺夜":"Vigil","余":"Yu","佩佩":"Pepe","俗心":"Vernacularism","保护目标(冻结状态)":"Protection Objective (Frozen)","信仰搅拌机":"Sankta Miksaparato","信标":"Transmitter","假想敌:再生":"OpFor: Rebirth","假想敌:弦":"OpFor: String","假想敌:淤困":"OpFor: Quagmire","假想敌:管":"OpFor: Pipe","假想敌:胄":"OpFor: Armor","假想敌:蚀裂":"OpFor: Fissure","假想敌:铳":"OpFor: Gun","假想敌:镜膜":"OpFor: Mirror","假想敌:骨刺":"OpFor: Bonespur","假想敌:黑云":"OpFor: BlackCloud","假日威龙陈":"Ch'en the Holungday","傀影":"Phantom","催泪瓦斯":"Teargas Can","元核孽生者":"Nucleic Maleficent","元核孽生者·悬赏":"Nucleic Maleficient/Bounty","克莱门莎":"Clementia","冒失的小弟":"Careless Underling","军用猎狗":"Military Hound","冰爆源石虫":"ILine truncated
|
||||
{"version":1,"lang":"en","meta":{"generator":"tools/build-i18n.mjs","source":{"id":"assets","label":"ArknightsAssets/ArknightsGamedata (en)","home":"https://github.com/ArknightsAssets/ArknightsGamedata","season":"act2autochess"},"fallback":"tools/i18n/corrections-en.json (before the official texts), tools/i18n/fallback-remake.json (the remake), tools/i18n/fallback-pr70.json (PR #70 by @YuriRestia), public/i18n/en.json","coverage":{"bands":{"texts":186,"translated":186,"official":186,"pct":100},"bonds":{"texts":138,"translated":138,"official":136,"pct":100},"bosses":{"texts":66,"translated":66,"official":66,"pct":100},"choices":{"texts":679,"translated":679,"official":679,"pct":100},"config":{"texts":110,"translated":110,"official":103,"pct":100},"effects":{"texts":1083,"translated":1083,"official":1081,"pct":100},"emotes":{"texts":41,"translated":41,"official":3,"pct":100},"enemies":{"texts":1734,"translated":1734,"official":1734,"pct":100},"factions":{"texts":14,"translated":14,"official":14,"pct":100},"garrisons":{"texts":747,"translated":747,"official":747,"pct":100},"items":{"texts":582,"translated":582,"official":570,"pct":100},"modules":{"texts":2498,"translated":2449,"official":2431,"pct":98},"operators":{"texts":876,"translated":854,"official":846,"pct":97.5},"skills":{"texts":5673,"translated":5439,"official":5439,"pct":95.9},"stages":{"texts":1124,"translated":1124,"official":1122,"pct":100},"talents":{"texts":5996,"translated":5688,"official":5680,"pct":94.9},"tokens":{"texts":458,"translated":458,"official":457,"pct":100},"traits":{"texts":1876,"translated":1860,"official":1860,"pct":99.1}},"totals":{"texts":23881,"translated":23252,"official":23154,"pct":97.4}},"names":{"\"炎佑\"":"Yan's Protection","Dor-1号失败品":"Dor-1 Failure","Dor-α号失败品":"Dor-α Failure","Dor-β号复制体":"Dor-β Replica","M3茧甲":"M3 Cocoon Shell","W·悬赏":"W/Bounty","“变形者集群”":"'Damazti Cluster'","“墓碑”":"'Gravestone'","“墓碑”·悬赏":"'Gravestone'/Bounty","“复仇者”":"'Hateful Avenger'","“复仇者”·悬赏":"'Hateful Avenger'/Bounty","“小自在”":"'Freeling'","“巨大的丑东西”":"'The Big Ugly Thing'","“巨大的丑东西”·悬赏":"'The Big Ugly Thing'/Bounty","“帝国的甲胄”":"'Armor of the Empire'","“庞贝”":"'Pompeii'","“庞贝”·悬赏":"'Pompeii'/Bounty","“斩胄之剑”":"'Armor Cleaver'","“灵幛”":"Sudaram","“独轮车玩具”":"'Unicycle Toy'","“破胄之锤”":"'Armor Crusher'","“碎铳之簧”":"'Gunshatter'","“神秘顾客”":"'Mystery Customer'","“耀阳”":"'Blazing Sun'","“自在”":"'Free'","“自在”·悬赏":"'Free'/Bounty","“萨科塔之眼”":"'Eyes of the Sankta'","“萨科塔之眼”·悬赏":"'Eyes of the Sankta'/Bounty","“萨科塔之翼”":"'Wings of the Sankta'","“萨科塔昂首”":"'Pride of the Sankta'","“萨米的意志”":"'Samivilinn,' the Resolution of the Very North","“裂管之音”/“断弦之音”":"'Echoes of Cracked Pipe'/'Echoes of Broken Strings'","“越长尘”":"'Sand Voyager'","“遗弃者”":"'Forsaken One'","“遗弃者”·悬赏":"'Forsaken One'/Bounty","“邪魔的利刃”":"'Demon's Blade'","“邪魔的利刃”·悬赏":"'Demon's Blade'/Bounty","“鼠王”·悬赏":"'Rat King'/Bounty","不屈":"Resilient","不屈弹射器":"Tough Launcher","不祥幻影":"Ominous Apparition","不稳定要素":"Unstable Element","业余竞演者":"Amateur Performer","业务指标":"Work Quota","临时收音师":"Audio Technician","主角阵营角色":"Hero's Sidekick","乌尔比安":"Ulpianus","乌顶巨角卢鲁":"Blackhorn Rhuul","九流之缘":"Streetwise Ties","人事部文档":"HR File","人才盲盒":"Talent Blind Box","仇白":"Qiubai","从不混淆的方向":"Undeviating Course","令":"Ling","以己之长":"Personal Power","伊内丝":"Ines","伊利昂的木驮兽":"Wooden Burdenbeast of Ilion","伊奥莱塔·罗素":"Ioleta Russell","伊芙利特":"Ifrit","休眠子裔":"Dormant Heir","休谟斯":"Humus","休露丝":"Sciurus","众志合一":"Wills As One","优等生":"Top Student","传奇重犯":"Infamous Recidivist","伪装服":"Disguise Outfit","伺夜":"Vigil","余":"Yu","佩佩":"Pepe","俗心":"Vernacularism","保护目标(冻结状态)":"Protection Objective (Frozen)","信仰搅拌机":"Sankta Miksaparato","信标":"Transmitter","假想敌:再生":"OpFor: Rebirth","假想敌:弦":"OpFor: String","假想敌:淤困":"OpFor: Quagmire","假想敌:管":"OpFor: Pipe","假想敌:胄":"OpFor: Armor","假想敌:蚀裂":"OpFor: Fissure","假想敌:铳":"OpFor: Gun","假想敌:镜膜":"OpFor: Mirror","假想敌:骨刺":"OpFor: Bonespur","假想敌:黑云":"OpFor: BlackCloud","假日威龙陈":"Ch'en the Holungday","傀影":"Phantom","催泪瓦斯":"Teargas Can","元核孽生者":"Nucleic Maleficent","元核孽生者·悬赏":"Nucleic Maleficient/Bounty","克莱门莎":"Clementia","冒失的小弟":"Careless UndLine truncated
|
||||
@@ -156,6 +156,11 @@ rebuilt.
|
||||
the source on the record's own ids, then the 下半 season (`act2autochess`). Rich-text tags and placeholders are the
|
||||
same as in Chinese, so `ui/richText.js` renders the English unchanged (a few official EN texts leave a tag unclosed;
|
||||
the parser runs it to the end of the text, as the game does).
|
||||
- **Corrections**: `tools/i18n/corrections-<lang>.json` (the pack's code: `en`, `ja`, `ko`, `zh-TW`; optional) is
|
||||
tried before the official data of that language, for an official translation that contradicts the Chinese text and
|
||||
the engine (exact match on the raw Chinese text; the plain twin is stripped from it; every entry carries a `_why:`
|
||||
note; `_`-keys are never written to the overlay). Only `corrections-en.json` exists. Counted as `correction` in the
|
||||
coverage report.
|
||||
- **Fallbacks**, in order: `tools/i18n/fallback-remake.json` (the remake's own strings: stage-name parts, item notes,
|
||||
the escaped levels' map names, the season name), `tools/i18n/fallback-pr70.json` (PR #70's translations of texts the EN client
|
||||
lacks: modules of CN-only operators, the 甄选 slots, 结算称号 criteria, emote labels), then `public/i18n/en.json`;
|
||||
|
||||
@@ -91,6 +91,29 @@ test('build-i18n matching: exact, blackboard templates, plain from raw, composit
|
||||
assert.equal(shapeKey('攻击力<@ba.vup>+{atk:0%}</>'), shapeKey('攻击力+80%'));
|
||||
});
|
||||
|
||||
test('build-i18n corrections: tried before the official text, the plain twin is stripped from the corrected raw one', () => {
|
||||
const zh = [{ t: { a: '攻击力<@ba.vup>+50%</>' } }];
|
||||
const en = [{ t: { a: 'ATK <@ba.vup>+50%</> flat' } }];
|
||||
const data = { effects: { e1: { descRaw: '攻击力<@ba.vup>+50%</>', desc: '攻击力+50%' } } };
|
||||
const { overlay, report } = buildOverlay({ zh, en, data, corrections: { _about: 'x', '攻击力<@ba.vup>+50%</>': 'ATK <@ba.vup>+50%</> per item' } });
|
||||
const e = applyRecordOverlay(data.effects.e1, overlay.files.effects.e1).value;
|
||||
assert.equal(e.descRaw, 'ATK <@ba.vup>+50%</> per item');
|
||||
assert.equal(e.desc, 'ATK +50% per item');
|
||||
assert.equal(report.coverage.effects.correction, 2);
|
||||
assert.equal(report.coverage.effects.translated, 2);
|
||||
// another language reads its own corrections the same way (build-i18n loads tools/i18n/corrections-<lang>.json)
|
||||
const ko = buildOverlay({ zh, en: [{ t: { a: '공격력 <@ba.vup>+50%</>' } }], data, lang: 'ko', corrections: { '攻击力<@ba.vup>+50%</>': '장비 1개당 공격력 <@ba.vup>+50%</>' } });
|
||||
assert.equal(applyRecordOverlay(data.effects.e1, ko.overlay.files.effects.e1).value.descRaw, '장비 1개당 공격력 <@ba.vup>+50%</>');
|
||||
assert.equal(ko.report.coverage.effects.correction, 2);
|
||||
});
|
||||
|
||||
test('data/i18n/en.json: the 维多利亚 6-member ATK bonus reads per item, as installVictoria applies it (corrections-en.json)', () => {
|
||||
const en = JSON.parse(readFileSync(path.join(ROOT, 'data', 'i18n', 'en.json'), 'utf8'));
|
||||
for (const t of [en.files.bonds.victoriaShip.effectDesc, en.files.bonds.victoriaShip.effectDescRaw, en.files.effects.bondeffect_victoria.desc]) {
|
||||
assert.match(t, /\+50%(?:<\/>)? ATK per equipment carried, or (?:<@ba\.vup>)?\+80%(?:<\/>)? per upgraded equipment/);
|
||||
}
|
||||
});
|
||||
|
||||
test('data/i18n/en.json: shape, the 盟约 season of the EN build, coverage and the names map', () => {
|
||||
assert.equal(OVERLAY.version, 1);
|
||||
assert.equal(OVERLAY.lang, 'en');
|
||||
|
||||
+20
-11
@@ -10,6 +10,8 @@
|
||||
// How a text is matched (no id table to keep up to date — the official tables are walked side by side):
|
||||
// 1. the zh_CN and the EN tables are walked in parallel (same keys; arrays of keyed records by their key), giving
|
||||
// pairs Chinese source → English source with the ids on their path as "scope";
|
||||
// 0. tools/i18n/corrections-<lang>.json first, when the language has one: an official translation that contradicts
|
||||
// the Chinese text and the engine (exact match on the data text, each entry with a `_why:` note) (correction);
|
||||
// 2. a data text equal to a Chinese source takes its English source (exact);
|
||||
// 3. a data text whose blackboard placeholders were resolved by build-data ({atk:0%} → 80%) is matched against the
|
||||
// Chinese templates of the same shape: the numbers are read back into a blackboard, the Chinese template must
|
||||
@@ -463,11 +465,14 @@ class Translator {
|
||||
/**
|
||||
* @param {PairIndex} index
|
||||
* @param {{ name: string, map: Map<string, string> }[]} fallbacks in order of preference
|
||||
* @param {{ composites?: boolean, corrections?: Map<string, string> }} [opts] corrections: tried before the official
|
||||
* texts (an official translation that contradicts the Chinese and the engine, tools/i18n/corrections-<lang>.json)
|
||||
*/
|
||||
constructor(index, fallbacks, { composites = true } = {}) {
|
||||
constructor(index, fallbacks, { composites = true, corrections = new Map() } = {}) {
|
||||
this.index = index;
|
||||
this.fallbacks = fallbacks;
|
||||
this.composites = composites;
|
||||
this.corrections = corrections;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -476,6 +481,8 @@ class Translator {
|
||||
* @returns {{ en: string, how: string } | null}
|
||||
*/
|
||||
translate(zh, ctx) {
|
||||
const fix = this.corrections.get(zh);
|
||||
if (fix) return { en: fix, how: 'correction' };
|
||||
const ex = this.index.exact.get(zh);
|
||||
if (ex) return { en: bestVariant(ex, ctx).en, how: 'exact' };
|
||||
// a plain text (build-data strips the markup of `desc`, `text`, card / device descriptions) against stripped sources
|
||||
@@ -626,7 +633,7 @@ function translateScalar(file, id, text, tr, stats, samples) {
|
||||
|
||||
class Stats {
|
||||
constructor() { this.kinds = {}; this.missing = new Map(); }
|
||||
row(kind) { return this.kinds[kind] || (this.kinds[kind] = { texts: 0, exact: 0, template: 0, composite: 0, remake: 0, pr70: 0, ui: 0, dict: 0, missing: 0, notes: 0 }); }
|
||||
row(kind) { return this.kinds[kind] || (this.kinds[kind] = { texts: 0, exact: 0, template: 0, composite: 0, correction: 0, remake: 0, pr70: 0, ui: 0, dict: 0, missing: 0, notes: 0 }); }
|
||||
hit(kind, how) { const r = this.row(kind); r.texts++; r[how]++; }
|
||||
miss(kind, text) {
|
||||
const r = this.row(kind);
|
||||
@@ -645,10 +652,10 @@ const dictMap = (d, untranslated = (zh, t) => hasCjk(t)) => new Map(Object.entri
|
||||
|
||||
/**
|
||||
* Build the overlay. Exported for tests (they pass in-memory tables).
|
||||
* @param {{ zh: object[], en: object[], data: Record<string, any>, fallback?: { remake?: object, pr70?: object, ui?: object }, source?: object }} input
|
||||
* zh / en: the picked sub-trees of TABLES, in order
|
||||
* @param {{ zh: object[], en: object[], data: Record<string, any>, fallback?: { remake?: object, pr70?: object, ui?: object }, corrections?: object, source?: object }} input
|
||||
* zh / en: the picked sub-trees of TABLES, in order; corrections: { zh: en } tried before the official texts
|
||||
*/
|
||||
export function buildOverlay({ zh, en, data, fallback = {}, source = null, lang = 'en' }) {
|
||||
export function buildOverlay({ zh, en, data, fallback = {}, corrections = {}, source = null, lang = 'en' }) {
|
||||
const untranslated = untranslatedTest(lang, LANG_SOURCES[lang]?.untranslated === 'native' ? nativeCharset(zh, en) : null);
|
||||
const index = new PairIndex(untranslated);
|
||||
for (let i = 0; i < zh.length; i++) index.walk(zh[i], en[i], []);
|
||||
@@ -657,7 +664,7 @@ export function buildOverlay({ zh, en, data, fallback = {}, source = null, lang
|
||||
{ name: 'remake', map: dictMap(fallback.remake, untranslated) },
|
||||
{ name: 'pr70', map: dictMap(fallback.pr70, untranslated) },
|
||||
{ name: 'ui', map: dictMap(fallback.ui, untranslated) },
|
||||
], { composites: LANG_SOURCES[lang]?.composite ?? false });
|
||||
], { composites: LANG_SOURCES[lang]?.composite ?? false, corrections: dictMap(corrections, untranslated) });
|
||||
const stats = new Stats();
|
||||
const samples = {};
|
||||
const files = {};
|
||||
@@ -695,7 +702,7 @@ export function buildOverlay({ zh, en, data, fallback = {}, source = null, lang
|
||||
const coverage = {};
|
||||
let total = 0, translated = 0, official = 0;
|
||||
for (const [kind, r] of Object.entries(stats.kinds).sort()) {
|
||||
const ok = r.exact + r.template + r.composite + r.remake + r.pr70 + r.ui + r.dict;
|
||||
const ok = r.exact + r.template + r.composite + r.correction + r.remake + r.pr70 + r.ui + r.dict;
|
||||
coverage[kind] = { ...r, translated: ok, pct: r.texts ? Math.round((ok / r.texts) * 1000) / 10 : 100 };
|
||||
total += r.texts; translated += ok; official += r.exact + r.template + r.composite;
|
||||
}
|
||||
@@ -706,8 +713,8 @@ export function buildOverlay({ zh, en, data, fallback = {}, source = null, lang
|
||||
generator: 'tools/build-i18n.mjs',
|
||||
source: source || null,
|
||||
fallback: lang === 'en'
|
||||
? 'tools/i18n/fallback-remake.json (the remake), tools/i18n/fallback-pr70.json (PR #70 by @YuriRestia), public/i18n/en.json'
|
||||
: `public/i18n/${lang}.json (the pack's UI strings)`,
|
||||
? 'tools/i18n/corrections-en.json (before the official texts), tools/i18n/fallback-remake.json (the remake), tools/i18n/fallback-pr70.json (PR #70 by @YuriRestia), public/i18n/en.json'
|
||||
: `${Object.keys(corrections).some((k) => !k.startsWith('_')) ? `tools/i18n/corrections-${lang}.json (before the official texts), ` : ''}public/i18n/${lang}.json (the pack's UI strings)`,
|
||||
coverage: Object.fromEntries(Object.entries(coverage).map(([k, v]) => [k, { texts: v.texts, translated: v.translated, official: v.exact + v.template + v.composite, pct: v.pct }])),
|
||||
totals: { texts: total, translated, official, pct: total ? Math.round((translated / total) * 1000) / 10 : 100 },
|
||||
},
|
||||
@@ -758,7 +765,9 @@ async function main() {
|
||||
ui: await readDict(join(ROOT, 'public', 'i18n', 'en.json')),
|
||||
} : { ui: await readDict(join(ROOT, 'public', 'i18n', `${opts.lang}.json`)) };
|
||||
if (opts.dict) fallback.dict = JSON.parse(await readFile(opts.dict, 'utf8'));
|
||||
const { overlay, report } = buildOverlay({ zh, en, data, fallback, lang: opts.lang, source: client ? { id: opts.source, label: src.label, home: src.home, season: seasonEn } : { id: 'dict', label: src.label } });
|
||||
// corrections of the language's own official texts (none for a language without a file)
|
||||
const corrections = await readDict(join(ROOT, 'tools', 'i18n', `corrections-${opts.lang}.json`));
|
||||
const { overlay, report } = buildOverlay({ zh, en, data, fallback, corrections, lang: opts.lang, source: client ? { id: opts.source, label: src.label, home: src.home, season: seasonEn } : { id: 'dict', label: src.label } });
|
||||
|
||||
// self-check: applying the overlay keeps every record's shape (strings replace strings only)
|
||||
let applied = 0;
|
||||
@@ -774,7 +783,7 @@ async function main() {
|
||||
if (client) log(` season in the ${opts.lang} build: ${seasonEn}${version ? ` · ${version.replace(/\s+/g, ' ')}` : ''}`);
|
||||
log(` ${report.pairs} zh→${opts.lang} source pairs; coverage per kind (translated / texts):`);
|
||||
for (const [kind, c] of Object.entries(report.coverage)) {
|
||||
const fb = [c.remake && `remake ${c.remake}`, c.pr70 && `PR #70 ${c.pr70}`, c.ui && `UI ${c.ui}`, c.dict && `dictionary ${c.dict}`].filter(Boolean).join(' · ');
|
||||
const fb = [c.correction && `corrections ${c.correction}`, c.remake && `remake ${c.remake}`, c.pr70 && `PR #70 ${c.pr70}`, c.ui && `UI ${c.ui}`, c.dict && `dictionary ${c.dict}`].filter(Boolean).join(' · ');
|
||||
log(` ${kind.padEnd(10)} ${String(c.translated).padStart(5)} / ${String(c.texts).padEnd(5)} ${String(c.pct).padStart(5)} % official ${c.exact + c.template + c.composite} (exact ${c.exact} · template ${c.template} · composite ${c.composite})${fb ? ` · ${fb}` : ''}${c.notes ? ` · notes skipped ${c.notes}` : ''}`);
|
||||
}
|
||||
log(` total ${report.totals.translated} / ${report.totals.texts} (${report.totals.pct} %), official ${report.totals.official}; ${Object.keys(overlay.names).length} names (${report.nameConflicts.length} with several ${opts.lang} forms)`);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"_about": "Corrections of the official EN client text: { \"<Chinese data text>\": \"<English>\" }, tried BEFORE the official EN data (tools/build-i18n.mjs; another language has its own corrections-<code>.json). Only for an official translation that contradicts the Chinese text and the engine; each entry has a `_why:<key start>` note. Raw keys only: the plain twin (desc / effectDesc) is derived by stripping the markup.",
|
||||
"_why:<@ba.vup>携带装备</>的【维多利亚】": "Victoria Alliance, 6 operators: the Chinese says +50% ATK per equipment carried (每携带1件装备), +80% per advanced one; server/sim/content/bonds/core.js installVictoria adds atk_normal_equip / +atk_golden_equip per item. The official EN ('gain +50% ATK, or +80% ATK if equipment is upgraded') reads as a flat bonus.",
|
||||
"<@ba.vup>携带装备</>的【维多利亚】干员伤害提升至<@ba.vup>{0:0%}</><@ba.acrem>(受层数影响)</>\n<每叠加<@ba.vup>25</>层>获得一件带有随机特殊效果的维式重锤\n<在场<@ba.vup>6</>名不同【维多利亚】干员>【维多利亚】干员每携带1件装备攻击力<@ba.vup>+50%</>,携带进阶装备改为<@ba.vup>+80%</>": "[Victoria] Operators <@ba.vup>carrying equipment</> deal <@ba.vup>{0:0%}</> damage <@ba.acrem>(affected by no. of stacks)</>\n<Every <@ba.vup>25</> stacks> Obtain a random special Victorian Hammer\n<With <@ba.vup>6</> different [Victoria] Operators on field> [Victoria] Operators gain <@ba.vup>+50%</> ATK per equipment carried, or <@ba.vup>+80%</> per upgraded equipment"
|
||||
}
|
||||
Reference in new issue
Block a user