From 0c005cf7dc7f358a989dc6a65a74735e4147ef59 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 04:45:31 +0000 Subject: [PATCH] An English key in a lookup table is an identifier, not a label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gospel's resources-list maps the resource titles that arrive in the data to the i18n keys that render them: const RES_KEY = { 'Bible video': 'gospel.res.bibleVideo', ... } title: RES_KEY[r.title] ? tr(RES_KEY[r.title]) : r.title The English on the left is matched against a value from the data; what the reader sees is the key on the right. Wrapping the left side in t() would make every lookup miss and fall through to the raw data value, so the count would drop while the display broke — the worst shape a gate finding can have. Found by gospel, who triaged it rather than acting on it. Exempt only when the mapped key really resolves in every locale, the same way a fallback has to earn its exemption. A table pointing at a key nobody wrote still counts: the lookup succeeds and hands t() a key the catalogue has never heard of. A value that is a plain category rather than a key counts too — the gate can only vouch for what it can resolve. Self-test covers all three arms, using the real RES_KEY case. Poisoning the exemption to skip the resolution check trips it. gospel 153 -> 146; fleet 159 -> 152. --- internal/check-hardcoded-english.mjs | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/internal/check-hardcoded-english.mjs b/internal/check-hardcoded-english.mjs index 6ba093f61..b5026eef3 100644 --- a/internal/check-hardcoded-english.mjs +++ b/internal/check-hardcoded-english.mjs @@ -256,6 +256,28 @@ function fallbackFullyResolved(key, root = '.') { return true } +// ★ THE KEY OF A LOOKUP TABLE IS AN IDENTIFIER, NOT A LABEL. gospel's resources-list has +// const RES_KEY = { 'Bible video': 'gospel.res.bibleVideo', ... } +// title: RES_KEY[r.title] ? tr(RES_KEY[r.title]) : r.title +// so the English on the LEFT is matched against a value that arrives in the DATA, and the string a +// reader sees comes from the key on the right. Translating the left side would make every lookup +// miss and fall through to the raw data value — the count would drop while the display broke, which +// is the worst shape a gate finding can have. +// +// Earned, not assumed, exactly as the fallback exemption is: the property's VALUE must be an i18n +// key that really resolves. A table pointing at a key nobody wrote is a real defect — the lookup +// succeeds and hands t() a key the catalogue has never heard of — so that still counts. +function isResolvedLookupKey(node, parent, grand) { + // the literal must BE the property's key, not its value + if (!parent || parent.type !== 'ObjectProperty' || parent.key !== node) return false + if (!grand || grand.type !== 'ObjectExpression') return false + const v = parent.value + if (v?.type !== 'StringLiteral') return false + // a dotted key shape, and it has to resolve — a bare category like 'scripture' is not a key + if (!/^[a-z][\w-]*(\.[\w-]+)+$/i.test(v.value)) return false + return fallbackFullyResolved(v.value) +} + // The `T('a.key', 'English')` / `L('key', 'English')` / `tStatic('a.key') || 'English'` shapes, seen // from the English literal looking outward. `names` is the set of translator identifiers already // resolved for this file, so an alias is recognised without being named here. @@ -455,6 +477,10 @@ function scanScript(code, file, out, lineBase = 0) { const full = (prefixes.get(callee) || '') + fbKey if (fallbackFullyResolved(full)) return } + // An English key in a table whose value is a real i18n key: an identifier the data is matched + // against, never a string the reader sees. + // stack's last entry IS the parent, so the enclosing object is one further back. + if (isResolvedLookupKey(node, parent, stack[stack.length - 2])) return if (isStr) { const text = node.value.trim() @@ -790,6 +816,23 @@ const c = cfg.z || 'A plain config fallback with no translator at all must count 'QUIET on config || t(key) || English when the key resolves') ok(ch.some((f) => /no resolving key/.test(f.text)) && ch.some((f) => /no translator at all/.test(f.text)), 'COUNTS the same chain when the key is missing, and when there is no translator in it') + // ★ LOOKUP-TABLE KEYS. gospel's RES_KEY matches a value that arrives in the DATA and returns the + // i18n key to render. Wrapping the left side in t() would make every lookup miss and fall through + // to the raw data value: the count drops while the display breaks. Exempt only when the target + // really resolves — a table pointing at a key nobody wrote is a genuine defect. + const LK = ` +` + const lk = scanSfc(LK, 'lookup.vue') + ok(!lk.some((f) => /JESUS Film|Bible video/.test(f.text)), + 'QUIET on a lookup-table key whose mapped i18n key resolves in every locale') + ok(lk.some((f) => f.text === 'Gospel audio'), + 'COUNTS a lookup-table key whose mapped i18n key does not exist') + ok(lk.some((f) => f.text === 'Bible Stories'), + "and COUNTS one whose value is a category, not a key — the gate can only vouch for what it can resolve") // the catalogue-side rule, tested directly: the two facts that make a fallback safe ok(fallbackFullyResolved('dashboard.map_loading'), 'a key present and really translated in all 11 locales counts as resolved')