Hardcoded-English gate reads interpolations and two more fallback idioms

Interpolation expressions carry the same defect as bound attributes: a literal
the reader sees, assembled in an expression a template walker skips. Added
before the gate lands, so the count each owner triages is the real one. It rose
by twenty-nine.

Two more shapes are fallbacks rather than leaks, both found by triaging real
hits. A proxy translator whose member read misses, and the idiom that asks
whether the translator handed the key back, which is what a missing key looks
like in vue-i18n. The second is matched on the key's shape -- dotted, no spaces
-- so an ordinary comparison against a word is not mistaken for one.

Both directions are planted in the self-test, which now makes seventeen
assertions: every leak shape must be seen, and every correct form must be
silent.
This commit is contained in:
Doxa Maps 2026-09-28 11:54:52 +00:00
parent ca1dc11565
commit 11e2d7eaaf

View file

@ -105,6 +105,15 @@ function guardedFallback(stack, names) {
if (x.type === 'CallExpression' && /^te$/.test(calleeName(x))) found = true
// a member read on a localisation object or alias: t.search, i18n.t.search
if (x.type === 'MemberExpression' && x.object?.type === 'Identifier' && names.has(x.object.name)) found = true
// THE "DID t() RETURN THE KEY?" IDIOM. vue-i18n returns the key itself when it is missing, so
// `msg !== 'messages.engineMissing' ? msg : 'English'` is a miss check and its English arm is a
// fallback. Matched on the KEY SHAPE (dotted, no spaces) rather than on any string comparison,
// so an ordinary `x !== 'yes'` is not mistaken for one. (cities-internet, triaged 2026-09-28.)
if (x.type === 'BinaryExpression' && /^[!=]==?$/.test(x.operator)) {
for (const side of [x.left, x.right]) {
if (side?.type === 'StringLiteral' && /^[a-z][\w-]*(\.[\w-]+)+$/.test(side.value)) found = true
}
}
})
return found
})
@ -157,6 +166,7 @@ function walkTemplate(node, file, out) {
const v = p.value && p.value.content
if (v && isProse(v) && !allowedBy(v)) out.push({ file, kind: `attr:${p.name}`, text: v })
}
// (interpolations are handled below, on the node itself)
// type 7 = DIRECTIVE. v-bind carries the attribute name in `arg`.
if (p.type === 7 && p.name === 'bind' && p.exp?.content) {
const attrName = p.arg?.content || 'bind'
@ -167,6 +177,12 @@ function walkTemplate(node, file, out) {
if (attrName !== 'style' && attrName !== 'class') scanBoundExpr(p.exp.content, file, attrName, out)
}
}
// type 5 = INTERPOLATION. `{{ cond ? 'Yes' : 'No' }}` is the same defect as a bound attribute —
// a literal the reader sees, assembled in an expression the template walker would otherwise skip.
// `{{ t('Foo') }}` stays quiet because the expression is parsed with the same translator rules.
if (node.type === 5 && node.content?.content) {
scanBoundExpr(node.content.content, file, 'interpolation', out)
}
for (const c of node.children || []) walkTemplate(c, file, out)
}
@ -206,6 +222,7 @@ const PLANTED_BAD = `<template>
<button title="Reset the map">x</button>
<th :title="\`Sort this table by \${col}\`">h</th>
<button :title="'Reset the view'">r</button>
<span>{{ ok ? 'Everything loaded' : 'Nothing loaded' }}</span>
</div>
</template>
<script setup>
@ -218,6 +235,8 @@ const PLANTED_GOOD = `<template>
<div class="rd-card is-open">
<h2>{{ t('Prayer Coverage') }}</h2>
<th :title="t('Sort this table')" :class="\`rd-card \${x}\`" :id="\`country-\${code}\`">h</th>
<span>{{ t('Prayer Coverage') }}</span>
<span>{{ n }} {{ t('Countries') }}</span>
</div>
</template>
<script setup>
@ -232,6 +251,7 @@ const lang = \`\${l}-u-nu-latn\`
const err = \`HTTP \${res.status}\`
const lone = 'Other'
const proxyMiss = t.search || 'Search names, country, religion…'
const keyMiss = msg !== 'messages.engineMissing' ? msg : 'The map engine did not load.'
</script>`
function selfTest() {
@ -252,10 +272,13 @@ function selfTest() {
ok(bad.some((f) => f.kind === 'template-literal' && f.text === 'countries'), 'SEES English inside a TEMPLATE LITERAL (the class that held a real defect)')
ok(bad.some((f) => f.kind === 'bind:title' && /Sort this table by/.test(f.text)), 'SEES a TEMPLATE LITERAL inside a bound attribute (sr: every scanner missed this)')
ok(bad.some((f) => f.kind === 'bind:title' && f.text === 'Reset the view'), "SEES a quoted '…' literal inside a bound attribute (both quote styles)")
ok(bad.some((f) => f.kind === 'bind:interpolation' && /Everything loaded|Nothing loaded/.test(f.text)), 'SEES a literal inside an INTERPOLATION expression')
ok(!good.some((f) => f.text === 'All Regions'), 'QUIET on a call through an ALIAS of t()')
ok(!good.some((f) => f.text === 'Regions List'), 'QUIET on a te()-guarded fallback')
ok(!good.some((f) => String(f.kind).startsWith('bind:')), 'QUIET on a bound attribute that IS a translator call')
ok(!good.some((f) => /Search names/.test(f.text)), 'QUIET on a proxy-t member miss (t.search || …) — a fallback, not a leak')
ok(!good.some((f) => f.kind === 'bind:interpolation'), 'QUIET on interpolations that are translator calls or bare values')
ok(!good.some((f) => /map engine did not load/.test(f.text)), 'QUIET on the "did t() return the key?" miss idiom (msg !== \'a.dotted.key\')')
ok(!good.some((f) => /rd-card|example\.test|nu-latn|HTTP/.test(f.text)), 'QUIET on css, urls, locale extensions and developer error text')
ok(good.length === 0, `QUIET overall on a correct file (got ${good.length}${good.length ? ': ' + JSON.stringify(good[0]) : ''})`)
// The blind spot is PINNED, not merely mentioned: if single-word detection is ever added, this