An English key in a lookup table is an identifier, not a label
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.
This commit is contained in:
parent
e5bc7a0d5b
commit
0c005cf7dc
1 changed files with 43 additions and 0 deletions
|
|
@ -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 = `<template><p>{{ x }}</p></template>
|
||||
<script setup>
|
||||
const RES_KEY = { 'JESUS Film': 'gospel.res.jesusFilm', 'Bible video': 'gospel.res.bibleVideo' }
|
||||
const BROKEN = { 'Gospel audio': 'gospel.res.no_such_key_at_all' }
|
||||
const KIND = { 'Bible Stories': 'scripture' }
|
||||
</script>`
|
||||
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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue