diff --git a/internal/check-hardcoded-english.mjs b/internal/check-hardcoded-english.mjs index daf887e6e..0a1a6b7c5 100644 --- a/internal/check-hardcoded-english.mjs +++ b/internal/check-hardcoded-english.mjs @@ -351,6 +351,36 @@ function isDataComparison(node, parent) { // resource not found means NOT FOUND in the pools checked" is a sentence that happens to be used as // a key, and that is a real leak — gospel found twelve defects hiding in exactly that kind of // already-classified bucket. Anything with sentence punctuation stays loud whatever its value is. +// ★ REGION IDENTITIES, EXEMPT ONLY WHILE THEY ARE TRANSLATABLE (studio's judgement, R26). +// legend-tree's REGION_OF maps a zone slug to a region NAME, and that name is an identity: node ids +// derive from it, and every surface that shows one translates it at render through REGION_KEY — +// upg-100-list.vue:440 `REGION_KEY[english] ? t(\`poster.${…}\`) : english`, poster-wall.vue:129 the +// same. So the English never reaches a reader. +// +// ⚠ THE EXEMPTION IS CONDITIONAL, AND THAT IS THE WHOLE POINT. It holds only for a name that IS a +// key of REGION_KEY. A new region added to REGION_OF without a REGION_KEY entry falls through that +// `|| english` and renders English in all eleven locales — which is precisely the defect upg100 +// already shipped once with Americas and Europe. So the rule flags it, and the gate keeps working as +// the table grows instead of blessing the file. +// +// Deliberately NOT "object values" and NOT "legend-tree.js": either would exempt the next table +// nobody has checked. The membership test is what makes this safe to have at all. +function regionKeyNames(code) { + // ⚠ NO NEWLINE REQUIREMENT. The first version anchored on `\n}` and so never matched a REGION_KEY + // written on ONE LINE — regionKeyNames returned null, the rule exited early, and BOTH two-sided + // cases passed without the exemption ever running. Found by poisoning the membership test and + // seeing nothing fail. + const m = /\bREGION_KEY\s*=\s*\{([\s\S]*?)\}/.exec(code) + if (!m) return null + return new Set([...m[1].matchAll(/'([^']+)'\s*:/g)].map((x) => x[1])) +} +function isTranslatedRegionIdentity(text, code) { + const names = regionKeyNames(code) + if (!names || !names.has(text)) return false + // it must actually be used as a region identity in this file, not merely share a spelling + return /const REGION_OF\s*=/.test(code) || /regionFor\s*=/.test(code) +} + const DATA_MAP_MIN_ENTRIES = 6 function isPureDataMapKey(node, parent, grand) { if (!parent || parent.type !== 'ObjectProperty' || parent.key !== node) return false @@ -610,6 +640,8 @@ function scanScript(code, file, out, lineBase = 0) { if (!text.length || !isProse(text)) return // gospel: empty matches were being reported if (!/\s/.test(text)) return // ⚠ the single-word blind spot, documented above if (allowedBy(text)) return + // A region identity translated at render — exempt only while REGION_KEY names it. + if (isTranslatedRegionIdentity(text, code)) return out.push({ file, kind: 'script-string', text, line: lineOf(node, lineBase) }) return } @@ -977,6 +1009,17 @@ const RENDERED = { africa: 'A sentence the reader actually sees here', asia: 'An // names, values that are prose a reader sees: the keys must STILL count, because this is a // label table, not a join table. Without this case the allData guard can be removed and every // test still passes — measured by poisoning it. + // ★ REGION IDENTITIES (studio, R26): exempt ONLY while REGION_KEY names them. A region added + // without a key falls through `|| english` and renders English in eleven locales — the defect + // upg100 already shipped once with Americas and Europe. The conditional IS the rule. + { + const KEYED = "const REGION_KEY={'Africa':'region_africa','Middle East':'region_middle_east','India':'region_india','Other':'region_other','China + Mongolia':'region_china_mongolia','Central Asia':'region_central_asia'}" + const mk = (of) => `\n` + ok(!scanSfc(mk("{ 'west-africa': 'Africa', 'middle-east-core': 'Middle East' }"), 'rg.vue').some((f) => f.text === 'Africa'), + 'QUIET on a region identity that REGION_KEY names') + ok(scanSfc(mk("{ 'west-africa': 'Africa', 'new-place': 'Antarctica Region' }"), 'rb.vue').some((f) => f.text === 'Antarctica Region'), + 'COUNTS a region with NO REGION_KEY entry — it would render English through the fallback') + } ok(scanSfc(`