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:
parent
ca1dc11565
commit
11e2d7eaaf
1 changed files with 23 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue