The deliberate bucket was one flat number, and it is the one place where
over-claiming costs nothing and shows nothing: a marker covering seventy-odd
lines and a one-line span read identically in a total. gospel went looking for
an over-broad island on their own card, suspected it, and found twenty-seven
strings behind a single attribute -- including the prose that says the figures
mean "the pools searched" rather than "nothing exists".
Each suppressed string now names the island that suppressed it, and the report
groups by island, largest first, in strings and in lines. A large island is not
automatically wrong, but it is the shape an over-broad one has, and nobody
should have to suspect it first.
It finds the one gospel measured by hand, and a larger one they had not: eighty
-four lines and twenty strings behind another single attribute in the same file.
Inheritance down the subtree is unchanged; it is correct. What changes is that
the output can now be read.
skills/plugin-designer/references/i18n.md, beside backend/glossary/targets/workflow. Every example is
real code with a file and line, and every claim was checked against main rather than written from
memory — a reference that is wrong costs more than no reference.
★ ONE CORRECTION TO THE BRIEF, and it matters for whoever reads this next. The ask named poster-wall.vue
as the fleet's `<i18n-t>` example. It is the opposite: that file's comment explains why `<i18n-t>` could
NOT be used there — the string is a prop rendered by a library component with no slots — and there is no
`<i18n-t>` usage anywhere in the tree. So the rule is written as "compose the parts, do not reach for
i18n-t", which is what the code actually does and what a plugin author can actually follow. Telling them
to use a component the codebase has never used would have sent them down a path with no example to copy.
The rules: one key per SENTENCE with named placeholders (the Hindi tail case, where "." in English is
" प्रार्थना करते हुए।" in Hindi and a wrapper would have dropped it); label form for counts while there is
no pluralRules layer; compose markup from escaped parts; dir="ltr" for deliberate English so the gate can
tell it from a leak; te()/proxy-t fallbacks are NOT leaks and must not be "fixed"; no resolved labels at
module scope (getters, or the first locale freezes); a namespaced translator hides the prefix from every
scanner; and the two gates that cover your code rather than the catalogue.
Rule 8 carries the precision dx-shepherd gave me today: check:hardcoded-english is ON DEMAND, and
gates:pre runs only its self-test — so an author who assumes the suite covers it will land leaks.
skills/ is shared, so this is a source sha for studio rather than something I land.
Written by the UPG-100 plugin owner. It parses templates and scripts and flags a translator
result stored at module or setup scope, or tStatic inside a computed. A production profile or
library/ fails; an experimental profile is reported. It found the search header's eight labels,
fixed in the commit before this, so it arrives green.
The eight labels came from tStatic inside a computed. tStatic reads a plain window global the
computed cannot track, so its only dependency was props.labels and the row stayed in the boot
language after the host switched locale. They now go through useOptionalT, which calls the
host's vue-i18n (a reactive locale) when installed and falls back to tStatic when not; English
stays the fallback and a host's own labels still win.
poster.region_india / region_other cover the two region headings the poster wall also
returns; messages.bandInRange / bandOutOfRange are the two states of the HUD band label,
two keys rather than a ternary over one. Eleven locales.
A literal string passed to a child that renders it is invisible to every scan
we have: in the parent it is an attribute value, in the child an interpolation
of a variable. The English and the render are in different files, so no
single-file rule can join them. One plugin hands a component a unit and a noun
that it prints as visible text, and a shared component derives the plural by
appending an s -- an English rule in library code, with no English string at
the site to find.
Reported as suspect, never counted. Whether the child renders the prop is not
knowable from here; it may be a key, an id, or an event name. Calling a suspect
a defect would make the count untrustworthy in the other direction, so this is
a question for a human rather than a verdict.
Only component tags, and only prop names that suggest rendered text, so an
event name is left alone. Plain elements keep going through the existing
visible-attribute rule.
Canary mode now triages an unplantable file too. "No text surface" and "hands
literal English to a child" are opposite risks, and reporting them together
buried the second inside the first -- which is exactly how this class stayed
hidden inside a bland count.
Both serious bugs in this gate made it quieter, and quiet reads as good news.
A zero is only worth what the detector is worth on that file, so canary mode
plants a known leak in each production SFC and requires it to be caught. A
plugin that cannot catch a planted leak cannot be trusted to report zero.
In memory rather than a temp directory: there is no cleanup to get wrong and no
chance of scanning a stale copy.
The canary goes inside the root element, located by the parser. Two earlier
attempts were wrong the same way. Injecting after the template tag made it a
sibling of the root, inheriting none of the root's attributes -- and a
root-level attribute is exactly what blinded a plugin, so that canary would
have passed the plugin it could not see. Finding the root tag by pattern then
broke on an attribute containing an arrow function, whose greater-than ends the
match mid-attribute. A pattern cannot tokenise markup any more than it can
tokenise JavaScript.
A skip is reported, not counted as a pass: a file with no plantable first child
has an unproven zero, and hiding that would repeat the mistake the mode exists
to prevent. Two such files are named in the output.
The island rule also accepted a bound :dir whose expression merely contained
the text 'ltr'. That matches the locale-aware switch every RTL-ready component
puts on its root, so such a component's entire template was treated as
deliberate English and every leak inside it was silenced.
regions-dashboard read zero for exactly that reason. Not clean: blind. Three
production plugins use the same pattern, so their counts were understated too,
and the failure is the dangerous direction -- a scan that under-reports looks
identical to code that is fine.
It surfaced from a one-count disagreement with gospel that we both expected to
be a stale tree. It was not: their run counted a mis-marked label as deliberate
where mine had already fixed it, and chasing the single-digit gap found the
bug.
A dynamic direction switch is the opposite of "this stays English" -- it is the
component promising to follow the reader. The fixture root now carries one, so
every assertion runs inside such a component and the blindness cannot return
without the whole self-test failing.
TWO PREPARATIONS FOR THE GENERATED PROFILE DESCRIPTION (Driver brief A1.5 col 2).
1. THE FETCH. Both packet fetch sites (:1363, :1588) requested
slug,name,country_code,population,religion,image_url. describeGroup needs the
language and a reachedness level, so primary_language and imb_gsec are added.
Both are existing API fields — poster-wall:171 already fetches primary_language
and sources.json:43 lists imb_gsec — so this adds no new API surface. Verified
they survive normalizeRecords, which is non-destructive ({...raw}).
GSEC and not EvangelicalLevel because GSEC is a NUMBER. EvangelicalLevel is
English prose from the API ("2% or Greater but Less than 5%"), so interpolating
it would leave every non-English packet half-English in its widest column. sr
measured both across 12,379 rows; I re-derived the GSEC distribution
independently (266/6573/269/199/1839/1428/1805) and confirmed that no GSEC
description mentions 0.1%, which is why "frontier" cannot be said from this
field and the wording stops at the 2% line the funnel page already states.
2. REGION IDS FOLD DIACRITICS. legend-tree.js built an id with
.toLowerCase().replace(/[^a-z0-9]+/g,'-'), which DELETES a combining mark rather
than folding it: Gujarāt became 'region-gujar-t', a different id from
'region-gujarat'. Nothing is broken today — the labels are regionFor()'s fixed
ASCII set and nothing joins on the id — but the brief's whole point is that a
fork brings its own set and names its own regions, and the first accented label
would split one region in two with no error and no empty state.
Same class gospel measured joining admin-to-list.json: a naive fold matched 514
of 1009 rows and lost THREE whole zones while every surviving zone still looked
populated. A join that returns half is more dangerous than one that returns
nothing. Fixed at the fold, not by enumerating the spellings we know.
Verified behaviour-preserving: Africa -> region-africa unchanged, and Gujarāt
and Gujarat now produce the same id.
The stamp sat on the header. gospel pointed out they only investigated our
mismatched counts because the arithmetic would not close -- an eight-hit gap
was too large to rationalise -- and that a gap of one or two would have been
shrugged off as drift.
So the failure that still gets through is a discrepancy small enough to explain
away, and provenance on a line the quoter skips does nothing for it. The
verdict line is what gets pasted into a report, so the tree travels with the
number instead of three lines above it.
It was added for the writer who forgets to name a sha. Within the hour it
earned itself on a different case: gospel and I compared counts, disagreed,
and spent the exchange assuming one set was stale. Neither was. Same script,
different source trees, twelve scanned files apart -- two honest measurements
of different bytes.
"19 SFC(s) scanned" describes the shape of a run and says nothing about which
bytes, so two correct runs are indistinguishable in a report. The stamp is what
lets a reader tell them apart, which is the better argument for the line and is
now written beside it.
L56 published the catalogue keys these three sites were waiting for, so the template now asks for
them instead of carrying the English:
the gap list's "related:" label -> gospel.gapList.relatedLabel
the legend's "Show all N" clear button -> gospel.filter.showAll, with the count as a placeholder
the detail panel's "Nothing in X itself" -> gospel.related.nothingInItself
Each was chosen because the swap costs no markup: the <em> and <b> wrap the whole phrase, so they
stay outside the call and the emphasis survives. The sites where a <b> sits INSIDE the sentence --
"Showing only <b>…</b>." above all -- are deliberately not in this commit, because the key is one
whole sentence and using it would silently drop the emphasis that distinguishes the filter's value
from its label. That needs a markup-preserving form, not a quiet visual regression on a production
map.
Measured, not assumed: hardcoded-english for this plugin 147 -> 144, and every one of the 72 keys
the leaf now references resolves in the catalogue. That last check caught a mistake in this very
change -- a fallback I wrote as t('gospel.state.thisVariety'), which does not exist and would have
rendered the key name to a reader. It is back to its literal until a key exists for it.
i18n parity and never-translated both pass; gospel's own render gate passes in ar, zh, ru and hi.
Rebuilt this bundle only, then fix:sri; check:sri is green at 64 leaves.
The new hardcoded-English gate found this in its first run over the production
plugins: an aria-label built by concatenation, so a screen-reader user on a
non-English locale heard "Open the" and "dashboard" in English around a region
name in their own language.
It uses the key the regions list already uses for the same sentence, which is
translated in all eleven locales, so the label now reads as one sentence rather
than two languages spliced together.
Found by the gate in the plugin that wrote it, which is the argument for making
it shared rather than private.
Ten strings that rendered to a reader in English whatever locale they chose.
Found by check:hardcoded-english; keys landed by studio L56.
STATUS OVERLAY (renders in .upg-status): noMapboxToken, loadingBoundaries (one
key, both call sites), fetchingUupgs {n}, mapStatusCounts {polys}{zones}{total}
{pins}. The "⚠ " prefix stays in the template, per the key spec. :2084 right
beside these was ALREADY t('messages.engineNotLoaded') — this makes the
neighbours consistent with it rather than introducing a new pattern.
MAP ERROR: noMapboxTokenDetail. It renders immediately after a translated
<strong>, so English sat literally adjacent to localized text.
LEGEND TABS: bound, not added — tabs.upg100Region / upg100Zone and
legend.tabInfo.* already existed in all 11 with exactly this text. ★ LEGEND_TABS
becomes a COMPUTED: t() is destructured at :391, BELOW the old const, so a plain
array could not call it; and even hoisted it would freeze the labels at the
locale active when the module evaluated. The host switches language at runtime,
so the labels have to re-evaluate on access. Both consumers are in the template,
where a ref auto-unwraps, so the call sites are unchanged.
THE PLURAL, deliberately a LABEL form: territor{{ n === 1 ? 'y' : 'ies' }} ->
t('messages.territoriesSelected', { count }). I had proposed a vue-i18n plural
key; studio was right that it would be worse, and I tested it — with no
pluralRules registered, v9 applies an English-shaped choice (count===1 ? 0 : 1)
to EVERY locale, so ru with 3 forms supplied picks wrong at 1, 2 and 21.
Confidently wrong grammar that LOOKS translated is worse than honest English.
POSTER-WALL headings translate at render; REGION_OF keeps its English values as
grouping identity (studio's split). ⚠ TWO OF THE SEVEN HEADINGS HAVE NO KEY:
'India' and 'Other'. The gate reported five because its header states it cannot
see single-word strings — this is that documented blind spot materialising, not
an oversight in the spec. They fall through to English text (never a raw key)
until poster.region_india / region_other exist; raised with studio.
Bundle rebuilt + fix:sri. The manifest churn is the same old->new hash for this
bundle; doxa-maps-spa's manifest changes only because it references it.
The engine-missing message read the shared messages.engineMissing through this file's
connectivity-prefixed t(), so it always missed and showed English in every locale.
Shared keys are now read qualified with tStatic: engineMissing, engineNotLoaded, and
couldNotLoadNamed for a data table that fails to load (naming the tab in the reader's
language, not 'table not found (404)'). Four unused English tab labels are removed.
studio landed data-control on the map controls (L54 3ed7d8d0c), so the gate can
name a control by untranslatable identity instead of its displayed text. Each
wrapper writes data-control="zoom-in|zoom-out|fullscreen|theme|…" onto
MapControlButton; it is not a declared prop, so it falls through onto the
rendered <button>.
REMOVES A CROSS-OWNERSHIP DEPENDENCY. Check (4) resolved its expected strings out
of library/i18n/locales/en/buttons.json — locale-correct, but it made a check in
MY gate depend on a key name in STUDIO's catalogue, so a rename there would have
failed a test here. data-control removes that coupling entirely.
THE THEME BUTTON GAINS THE MOST, and it had two more sites I had not fixed:
:488 (check 5's toggle) and :1502 (17b cold-load) both matched
[title^="Switch to"]. That title is LOCALIZED and DYNAMIC ("Switch to dark" /
"Switch to light"), so no i18n key ever resolved it and a prefix match on English
prose was the only option. All three sites now use data-control="theme", and the
`theme === 1` assertion finally means what it says.
Zero English-literal attribute selectors remain in the gate; every control query
is now a class or data-control. The one surviving `[title="…"]` string is inside
a comment recording why the pattern was abandoned.
fc93847bb ("Batch selection builds a packet, instead of recolouring regions")
replaced copy-to-clipboard with packet-building and left four pieces behind: the
button's handler, its execCommand fallback, the window global they wrote, and the
gate check that read it. This removes all four.
copySelection had exactly ONE reference in the file — its own definition — and
legacyCopy was called only from it, so both were unreachable. The minifier was
already dropping them: __UPG_LAST_CLIPBOARD__, which copySelection set
unconditionally, appears 0 times in the built bundle.
THE GATE HALF MATTERED MORE. visual-gate check (15) asserted the Copy payload,
guarding on .upg-hud-btn.copy existing — a button that no longer exists. But the
guard skipped only the MEASUREMENT: copyOk was initialised false and check() ran
unconditionally, so it reported FAIL ("no .upg-hud-btn.copy in HUD") on every run
since the button went. A permanently red assertion for a deliberately deleted
feature teaches readers to ignore red, which is worse than no check at all.
formatClipboard SURVIVES — 3 references, and one is live: __UPG_SELECT__
.clipboardText, the headless probe surface the gate still uses. Deleting the
"clipboard code" wholesale would have broken it.
Also corrects the D-8 comment, which still named "Copy + Clear" as the HUD
buttons; they are Prayer List / Poster Map / Clear.
VERIFIED BY THE BUILD NOT MOVING: rebuilt after deleting 43 lines of source and
the bundle is byte-identical to its parent (sha 3dda630d5cf5e647, 992356 bytes).
That is the test of the claim — if the output had changed, the code was not dead.
Two stale claims in one comment. It said `defineAsyncComponent` while the code
below it calls `retryableAsyncComponent` (:56), and it pointed at
research-map.vue:152 for the pattern — which is now a tStatic import; the real
lazy-load moved to :193.
Not cosmetic. Auditing card BUG-LAZY-PLUGIN-DEATH-ON-A-SINGLE-FAILED-IMPORT, the
census of components bypassing the retry wrapper is a grep for raw
`defineAsyncComponent`, and this comment made my own file read as a bypass. The
one real bypass (pplr-data-maps.vue:46, six lazy views) is exp's and routed. A
comment that names the wrong API costs the next reader the same detour.
Says why the wrapper is used rather than only naming it: Vue caches a REJECTED
loader promise, so one dropped chunk request kills the panel for the life of the
page. That is the defect the card exists for, and the reason not to "simplify"
this back to the raw API.
Comment-only, so the build is byte-identical — the strip-shadow-css-comments
pass drops it. Rebuilt and re-ran fix:sri anyway to prove it: 124 hashes already
agreed, 0 rewritten, the committed .js unchanged.
gospel's diff of a hand triage against the checker found this: a selector
listing two custom elements was reported as user-visible English, because it
reads as English only where the tag names happen to be English words. The
single-tag rule matched neither, since it is a comma-separated list.
Exempt by call site rather than by content, as with developer strings. No
content rule can reliably tell a tag list from a sentence, and the site is a
fact about what the string is for. Covers querySelector, querySelectorAll,
closest, matches and the getElementBy* family.
The self-test plants gospel's own case and a second list of custom elements
from another plugin, since the pattern is not theirs alone.
gospel: 15 whole-sentence keys from its card (search, filter, gapList, province) with
placeholders each locale may move, 9 standalone strings, and 8 sentences that the template
assembles from fragments (prayer-page link, macrolanguage note, video titles, the largest-N
line), each now one key. UPG-100: map status and token messages, the territory hint, a
label-form territory count that needs no plural rules, and the poster wall's five region
headings. The templates switch to these keys in the owners' own commits.
check:composite-freshness found stale on main: all-people-groups
big-picture
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-internet
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
A namespaced translator qualifies every key with its own area, so a key from
another area can never resolve and the English fallback becomes the normal path
in every locale. The reader sees English exactly as if it had been hardcoded,
which is why it belongs here rather than in a separate check.
Nothing caught it before. The existing key check reads the key as written,
which does exist; it is the key as qualified that misses. exp-shepherd hit it
twice in one day, in two different plugins.
Detected by shape -- qualified key absent while the unqualified one is present
-- so it cannot fire on an ordinary missing key, which is the other check's job.
The first version found only one of the two real cases, because the other
declares its translator through a wrapper that takes no argument and carries
the namespace one indirection away. Wrappers are now resolved from source, and
both historical defects are reproduced from the code as it stood before each
fix, with the qualified key named in the message.
Self-test now makes twenty-eight assertions, including a planted wrapper.
gospel, upg100 and exp-shepherd triaged 300-odd hits between them, and most of
what they found was the gate being wrong rather than the code.
False positives removed, each from a real hit: class and id suffixes built by
interpolation, relative URLs, custom-element tag names, and empty matches.
Developer strings are now exempt by CALL SITE rather than by content, because
the site is the fact and the wording is not. A split template literal is one
finding reported as its whole string with its holes marked, instead of one hit
per fragment -- that alone was turning single defects into three.
Text inside a dir="ltr" island is DELIBERATE English, the marker these plugins
already use for content ruled to stay English. It is listed but not counted, so
nobody is asked to undo a decision.
The header gains a second blind spot, found the same day by exp-shepherd: a
guarded fallback is judged by its shape, not by whether the lookup it guards
works. A key read with the wrong prefix misses every time, making the English
arm the normal path in every locale while this stays quiet. I had triaged
exactly that line as "not a leak" because the key existed -- existing and being
found are different things.
Self-test is twenty-three assertions: every leak shape seen, every correct form
silent, both blind spots pinned.
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.
sr, upg100, gospel and regions each wrote a scanner for user-visible English
that never reaches a translator, and each found real defects. Four copies means
four sets of blind spots and four allow-lists drifting apart, so this merges
them. Every rule earns its place from a defect one of those scanners caught.
Templates and script blocks are parsed, never matched with a regex: a regex for
string literals matches from the closing quote of one to the opening quote of
the next, which produced 137 phantom findings before this was parsed properly.
Translator aliases are read out of each file, because `const t2 = t` defeats a
check that knows only the base names. Guarded fallbacks are not leaks, in both
shapes that exist here: an existence check around the call, and a proxy whose
member read misses. Bound attributes are parsed too, in either quote style,
which is the blind spot every one of the four scanners shared.
The self-test plants a leak of each kind and requires the gate to see it, then
plants the correct forms and requires silence. It also asserts the fixtures are
valid Vue first, because a malformed fixture and a blind scanner produce the
same silence.
The header records what this cannot see: single-word strings. A bare 'Other'
used as a fallback label is user-visible English and will not be reported. That
trade keeps the signal readable, and the self-test pins it so that closing the
gap forces the header to be corrected rather than left stale.
Five bound English strings no locale could reach, now catalogue keys in all eleven locales:
the column sort tooltip (dashboard.sort_by_column_hint, {column}), the full-screen button's
title and label (fs_*), the export tooltip (export_*_hint, a label form with {n} so no plural
rule is needed), and the donut's accessible name (aria.donutChart, {title}), which read a
translated title followed by the English word "donut". English stays the fallback argument.
Accessibility audit of my production surfaces, headless from source, scan negative-tested before I
trusted its zeros. Four hits, two real.
· THE PROJECTION PILL (DashboardMapPanel) carries role="button" and tabindex="0" and had no key
handler. A keyboard reader tabs to it, hears "button", presses Enter, and nothing happens — the
role and the tabindex promise an activation that was never wired (WCAG 2.1.1). It is a span rather
than a button because the pill sits inside the map chrome, so the promise has to be kept by hand.
I touched this element earlier today to give it a translated accessible name and did not notice it
could not be operated; naming a control and making it usable are separate jobs.
· THE SORTABLE COLUMN HEADERS (DashboardDataTable) carry role="columnheader" and aria-sort and were
click-only. Screen-reader users were told the sort state of a control they could not reach. Now
focusable with Enter and Space, and shift+Enter still adds a second column, because onSort reads
shiftKey off the event it is handed and a KeyboardEvent carries it the same way.
.prevent on space so the page does not scroll under the press.
TWO HITS WERE FALSE POSITIVES and are recorded so the next sweep does not re-raise them: the facet
rail's .fr-body and the chip bar's .rd-chips both take @click on a container, which looks like a
click-handler on a non-focusable div. They are DELEGATION: the facet rows carry their own
:tabindex/:role and the container has @keydown for parity, and every chip is a real <button> with an
aria-label. The control is the row and the chip, not the box around them.
Verified: check:sfc-parse PASS, and the scan reports 0 on both files afterwards. Not landed — the quiet
window is on; this waits for studio.
Found by cross-reading exp-shepherd's country-posters port, not by anything failing here. Their
73438b43b routes all eighteen of that map's region clears through a regionIdle() wrap. Mine still wrote
NINE raw constants — fill-opacity 0.20 and 0.7, line-color rgba(60,60,80,0.35), line-width 0.6 — across
clearCountryHighlight, the deselect path and the dim path.
A raw value makes the property CONSTANT again. So the first time a reader cleared a highlight, or ran a
country search and dismissed it, the three region paints stopped being data-driven and the next
highlight recompiled the shader variant that b5894ec3 exists to have compiled at layer-add. rega
measured 0 ms on the row click and that measurement was honest — it was taken on a fresh tab, where no
clear had run yet. The path nobody exercised is the one that pays.
Every region paint write now goes through regionIdle(v) — ['case', ['has','iso_3166_1_alpha_3'], v, v],
both branches the value it already had, so nothing looks different and the property stays data-driven.
Nine sites, no behaviour change, and the pre-warm now survives a clear.
★ DEFINED ABOVE ITS FIRST USE, deliberately. The first caller sits four hundred lines above where the
other idle constants live, and a const read before its declaration line has executed is the TDZ this
file has already shipped twice this week (_lastFitKey, then _lastPinOpacityKey). Its callers only run
after mount, so it would probably have been fine — "probably fine" is what the gate exists to stop me
saying. check:setup-order PASS.
Verified: check:sfc-parse and check:setup-order PASS. Needs eyes: rega — clear a region highlight, then
click another region row, and the compile should still be 0 ms. That is the case no run has covered.
retryableAsyncComponent: after the three retries the pane rendered blank. It now shows
"This view couldn't load." with a Retry button (messages keys, 11 locales). Retry swaps in a
fresh async component, since Vue keeps a rejected load for the life of the page.
Each map control wrapper passes data-control (zoom-in, zoom-out, fullscreen, theme, location,
help, share) to its button: an untranslated hook for probes, where title and aria-label are
localised.
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-internet
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
Regions: the country and region counts under the dashboard title were English
template literals; they are now label keys ('Countries: %count%') in all eleven
locales, which also sidesteps plural forms.
Gospel: the Zume chip joined 'in' + a language name, which reads backwards in
Hindi; it is now one phrase key with a {language} placeholder.
UPG-100: the Batch Select button title comes from legend.title.batchSelect.
The six lazy views were built with raw defineAsyncComponent, which caches a rejected
loader, so one network blip left that tab dead until a full reload. They now use the
library's retryableAsyncComponent, which retries the import.
The load-time pre-warm made the regions fill and border paints data-driven, but every
clear (clearCountryHighlight, clearAllHighlights and the reset branches) wrote the
plain constants back. All 18 writes now use regionIdle(v), the same identical-branch
case the pre-warm uses, so the property never returns to its constant variant and a
second highlight cannot re-pay a variant switch. Nothing changes on screen.
The (G-rail) probe queried `.upg-rail-batch[title="Batch Select"]`. Once the
title becomes a bound `:title="BATCH_SELECT_TITLE"` (studio L53) that selector
matches under `en` and nowhere else, and the gate reports "button not in the
toolbar" — a PRODUCT failure for what is really a broken query. The class is
already proven unique by the railBtns === 1 assertion in the same probe, so it
is the identity; the title is still checked, as the property that actually
matters for a11y: NON-EMPTY, which holds in every locale. That also catches a
regression the equality missed — a :title binding resolving to '' or undefined.
The (4) map-controls check had the same coupling, three times. Those titles are
ALREADY bound (ZoomInButton.vue: :title="t('buttons.zoomIn')"), but the page is
pinned to en-US at newPage({ locale }) — L4/LESSON-03 — so they are not failing
today. The expected strings now come from the same i18n source the component
reads (library/i18n/locales/en/buttons.json) rather than being hardcoded, so the
gate survives being run in another locale. Verified the three keys resolve to
the literals they replace, so this asserts exactly what it asserted before.
The theme button cannot use that shim: its title is DYNAMIC ("Switch to dark" /
"Switch to light"), so there is no single key to resolve, and it stays a prefix
match on English prose. Its assertion moves from `> 0` to `=== 1` (studio's
call): that does not remove the coupling, but it stops 0 and 2 both collapsing
into "not > 0", and the failure line already prints the count.
Those four .mcb buttons share one class with no stable per-control hook, and
aria-label mirrors the localized title, so the title is the only identity
available to a query. The durable fix is data-control="zoom-in|zoom-out|
fullscreen|theme|location" on library/components/map-controls/MapControlButton
— studio's under 5B. They are riding it on the next landing that rebuilds the
17 consumers anyway rather than spending a rebuild on it alone; this shim comes
out when that lands.