08f89f55 gave both composites a text-direction computed that calls
getRuntimeLocale(), detectLocale() and RTL_LOCALES, but imported none of them.
The build does not check free names, so the bundles on main reference three
undefined globals. watch(textDir, reflectDir) evaluates the computed during
setup, so setup throws a ReferenceError in every locale, and Vue renders
nothing for the whole composite: the map, the dashboard and the grid.
Reproduced headless with the same computed + watch shape under
@vue/runtime-core: lang '' fails with "getRuntimeLocale is not defined", and
lang 'ar' with "RTL_LOCALES is not defined"; neither renders.
After this import, both bundles build and neither contains a free reference
to the three names.
app/assets/styles/base/_reset.scss:3-8 applies border-box to every element on
DX-life. Our leaf document carries the identical rule already -- but these
components render inside a SHADOW ROOT, and a document-level `*` selector does not
cross that boundary. So every element in here was content-box while every element
of theirs was border-box, and no scoped component style could reveal it. That is
how it survived seven global-rule fixes, and it is why this codebase had accrued
seven separate per-element box-sizing:border-box patches: each was a local
workaround for the reset that never arrived.
What it cost, measured rather than described: their legend swatch is width:10px
with a 1px border -- 10px under border-box, 12px under content-box. Two swatch
columns of 1px each are exactly the 2px by which our legend panel measured wider
than theirs (167 vs 165) after the rest of the legend was aligned to the pixel.
The same label measured 129px on both sides in the same Poppins, so the text was
never the cause.
The port is a geometric NO-OP and that is why it is safe to do in one move: the
blast radius was measured FIRST, by injecting this exact rule into every shadow
root and diffing every element's rect. 0 of 431 elements moved on
region-dashboards, 0 of 678 on regions-list, 301 already border-box by their own
declarations. The detector was negative-tested, because a zero from a check that
cannot fail proves nothing. So the whole value of this commit is the DELETION --
six per-element patches plus the interim `.rcm *` subtree rule, which the global
reset strictly contains.
Placement is plugin-local, not library/useShadowStyles.js. Putting it there would
fix all 25 bundles in one line and is tempting for exactly that reason, but the
blast radius above was measured on THESE two pages only; 23 other bundles would
have their geometry changed on no evidence. RULE 2 says default to the plugin
folder until a second consumer is named.
One module rather than three copies: three roots inject into the shared shadow
root and any of them can be the one that mounts, so all three need it.
The rows under "No resource found" name the way in. For 182 of the 369 groups
with nothing, that bridge language also carries the Zume discipleship course, and
the row did not say so — which is the difference between "there is a way in" and
"there is a way in and the course to train the first believers already exists in
it". A mobiliser scanning a country is deciding where to start, and that is the
deciding fact.
The bridge's own name stays first, because the language is the decision and the
training is what makes it actionable. It reads "way in: French · Discipleship
training", with the label coming from the catalogue so it follows the reader's
locale like every other label on the row.
Nothing new is fetched or linked: it reads the same Zume URL already verified and
already on the bridge panel, so the doctrine screen is unchanged at 16,963.
The point of this map is not that somebody reads a page. It is that the first
believers in a group are trained and become the first messengers to their own
people and their neighbours. Zume is the piece that does that, and it is
published in 52 languages.
Measured across the 369 Doxa groups with nothing: ZERO have Zume in their own
language, and 182 have a bridge language that does. The map was showing Zume only
to groups that already had something — the half that needs it least — while for
half the gap the training already exists in a language their neighbours speak.
Both bridge panels now carry it beside the Scripture and the film. All 19 distinct
URLs behind those 182 groups were fetched and answer 200, with a bogus locale
404ing so the status means something; every one is on zume.training, already a
screened publisher, so the doctrine screen is unchanged at 16,963 links.
NUMERALS ARE HELD covers the whole formatted number, not just the digit system.
-u-nu-latn keeps digits Latin but lets the SEPARATORS follow the locale, so
de reads 1.234.567 and ru 1 234 567 -- and choosing that is the held decision.
I read "numerals" as "digits", shipped it at d988c6f1, and then wrote a check
asserting that grouping SHOULD vary, which passed green while certifying the
breach. A check encoding the wrong intent turns a policy breach into evidence of
compliance, which is worse than having no check. It now asserts one format across
all eleven locales, with a negative control that the reverted form produced five.
Reverted on the shepherd's instruction; gospel shipped the same change and was
asked to revert its number part too, so no single map settles this.
The try/catch goes with it rather than being kept "just in case": it existed
because a locale-derived tag can throw RangeError, and a string literal cannot.
EVIDENCE FOR THE RULING, read from their master, because it cuts both ways:
RegionDetail.vue:16 n.toLocaleString(`${locale.value}-u-nu-latn`)
their own comment: "Locale grouping separators with Latin
digits, matching the rest of the site."
CountryMap.vue:128 toLocaleString(languageCode) <- the popup, no subtag
So per-locale grouping is THEIR deliberate site-wide convention, and this
element's two other formatters clone that line verbatim. Those are left alone and
flagged, not reverted: changing them would override DX-life's documented choice
rather than stop a map from freelancing, and that is a bigger call than mine.
The popup is the one place theirs follows the locale for digits as well, so it is
also the one place the hold's own rationale actually bites.
And the rationale bites via the REGION, not the language: ar resolves
numberingSystem=latn (1,234,567) while ar-EG resolves arab. A host setting
profile-config.locale to 'ar-EG' is the real exposure, and a fixed en-US is immune.
A pool nobody here had tried, and the reason it stays out is worth writing down
so the next tick does not spend itself on the same site.
It is reachable and it 404s properly, so unlike the shells that defeated deaf
sources, its answers can be trusted. Its index publishes 118 languages with no
pagination and no stated total.
Out on coverage first: of the 294 languages in the Doxa gap, exactly ONE name
matches a folder there. 82 of the 118 match a language this index carries
somewhere, so the catalogue is real and does overlap us — just not the part with
nothing, which is the only part a new pool is worth adding for.
Out on the join key second: no ISO code is published anywhere, in the index or on
a story page. The folder IS the language name, so joining would mean matching by
name, the guess this build removed when Doxa's own API made it unnecessary, and
the guess that would have handed Akha in Laos one of three sub-varieties. The
single gap match above is a name match and worth nothing on its own.
Out on doctrine third, and that word is chosen carefully: no statement of faith
is served at any of the usual paths. This is unverified, not suspected. The brief
says leave a source out when its doctrine is unclear and say why, so that is what
this is, and it is not a finding against them.
No resource, link or count changes; the index differs only by its generated date.
Numerals are HELD pending the Driver's call: text translates, digits stay Latin.
Two of this element's three number formatters already carried -u-nu-latn; the map
popup's population was the odd one out, and it is the one a reader of an Arabic map
would actually have seen.
THE REASON IS NARROWER THAN I FIRST WROTE, and I measured it after asserting it.
Bare 'ar' was never the problem: on full-ICU Node it resolves numberingSystem=latn
and prints 1,234,567. It is the REGIONED tag that switches systems --
ar -> latn -> 1,234,567
ar-EG -> arab -> ١٬٢٣٤٬٥٦٧
so the codes this element ships (SUPPORTED_LOCALES, and localeFromUrl() which strips
the region at .split('-')[0]) were already compliant. The exposure is a tag arriving
from OUTSIDE: a host setting profile-config.locale to 'ar-EG' reaches that line with
its region intact. The subtag makes the hold hold whatever arrives, rather than
depending on every future caller to strip a region first.
-u-nu-latn is also closer to their output than the hardcoded 'en-US' it replaced,
which is the point worth keeping: it holds the DIGITS while letting the grouping
SEPARATORS follow the locale, so de still reads 1.234.567 and ru 1 234 567. Their
formatPopulation follows the locale outright; when the hold lifts, dropping the
subtag in all three restores exact parity.
Also added the try/catch the two sibling formatters have and this one lacked. A
malformed tag throws RangeError, and this runs while building popup HTML, so an
uncaught throw takes out the whole popup instead of one number.
Verified: 11/11 locales print Latin digits; 5 distinct grouping formats survive, so
it is not flattened to en-US; and the negative control uses ar-EG, an actual
violation, because a control built on bare 'ar' passed for the wrong reason and
proved nothing.
The merge brought source changes into this bundle that my own composite-freshness
run did not flag — that gate checks composites, the landing precondition checks
every bundle. The scan refused the push and named it, which is the first time my
lander has stopped a landing instead of waving it through; the '&&' between scan
and push earned its keep on its first outing.
toLocaleString() with no argument formats for the JS runtime's locale, which has
nothing to do with the locale the plugin was asked to render in. The Arabic build
on an English browser got English grouping and the same page on a French browser
got French grouping: one plugin, one locale, two different numbers. All four
leaves now format with the locale i18n actually resolved.
numberingSystem is pinned to latn, and the obvious justification for that is
wrong, so here is the measured one. Against full ICU, the eleven tags this bundle
ships all resolve to latn already — plain "ar" included — so for them the pin
changes nothing. It earns its place only if a host passes a region tag: ar-EG
resolves to arab and fa to arabext, and those would switch the digits. Numeral
format is held for the Driver, so the pin keeps that his decision rather than a
host's.
What does change is grouping, and it is visible: Hindi groups in lakhs, so
1234567 reads 12,34,567, and French and Russian use a space. Those are correct
for the locale and differ from what shipped before.
The bare calls left in the built bundle are in library components this bundle
does not render — LegendProgressBar and PosterWall — and belong to whoever owns
library, not here.
resources-map read profileConfig.tk || window.DOXA_TOKEN. DOXA_TOKEN is set by a
few dev pages and nothing else, so in the studio and on every built page this map
ignored the page's token entirely. It now takes profile-config tk, then
ProfileLoader's injected mapboxToken, then window.MAP_TOKEN, as the
universal-props contract says.
The pattern is copied from the same fix on cities-internet and key-cities
(1cf57f7a) rather than re-invented. Three raw maps each deciding for themselves
how to find a token is how all three came to be wrong in the same way.
Headless: the built bundle no longer contains DOXA_TOKEN at all, and carries the
injected token and MAP_TOKEN instead.
The map booted correctly under ?provider=maplibre and then went blank
the first time a reader touched the light/dark toggle: handleToggleTheme
called setStyle with a hardcoded mapbox:// URL, which MapLibre cannot
load at all. The boot path two hundred lines below already did this
properly — resolveNamedStyle('paper', upgProvider) — so the file
disagreed with itself about how a style is chosen.
The registry names the same INTENT for both engines: 'paper' is light
(mapbox light-v11 / OpenFreeMap positron), 'ink' is dark (dark-v11 /
OpenFreeMap dark).
BEHAVIOUR-PRESERVING ON MAPBOX, and checkable rather than asserted:
resolveNamedStyle returns URLs byte-identical to the literals this
replaces — 'paper' -> light-v11, 'ink' -> dark-v11. Only the MapLibre
branch changes, from an unloadable URL to a real one.
⚠ A STRING-PRESENCE GREP CANNOT PROVE THIS LANDED, which is worth
recording because it nearly satisfied me. The built bundle already
contained openfreemap.org/styles/dark BEFORE the change — the registry
is compiled in for the boot path — so "is the MapLibre URL in the
bundle" answers yes either way. The falsifiable question is what the
CALL SITE references, and at the compiled layer:
before setStyle(f.value?"mapbox://…/dark-v11":"mapbox://…/light-v11")
after setStyle(f.value?vi("ink",s).style||"mapbox://…/dark-v11"
:vi("paper",s).style||"mapbox://…/light-v11")
where vi is the minified resolveNamedStyle(name, provider). Measured by
building both ways.
The `||` fallbacks keep the old literals as a last resort if a name ever
stops resolving, matching the boot path's own guard rather than
inventing a second policy.
rega measured it by eye at b0cd3140 on the built tree: regions-mfe under
?provider=maplibre draws the basemap and 2,107 pins, the boundary geojson returns
200, and the fill layer holds 0 features. Mapbox draws 156. A filter that admits
nothing looks exactly like data that never arrived.
countryFilter() is two clauses ANDed. The ISO half was fine — the field bridge
three lines above it already copies iso3 -> iso_3166_1_alpha_3 onto every geojson
feature before setData, so one name is read everywhere. The WORLDVIEW half has no
bridge and cannot have one: it exists because the Mapbox tileset repeats a country
once per worldview and the highlight would otherwise draw two or three times over.
The geojson has no such duplication and carries no worldview property at all, so
both sides of the `any` were false on every feature and the `all` emptied the
layer. It is Mapbox-only, not something to translate, so MapLibre now gets the ISO
clause alone.
I NEARLY FIXED THE WRONG HALF. Reading the RAW geojson I saw { iso3, shapeGroup,
shapeName }, concluded the ISO key was wrong too, and branched it — adding a
second name for a fact the bridge already normalises. The bridge was three lines
above the filter I was editing. Reverted; the ISO clause is untouched.
Proven headless, fail-first, by EVALUATING both filters against the real 162
features with MapLibre's own parser after applying the bridge:
OLD filter admits 0 / 162 <- rega's bug, reproduced without a browser
NEW filter admits 14 / 14 <- every requested country
Mapbox path unchanged: still 14/14, and still drops a duplicate worldview copy
The control asserts the OLD filter still admits 0, so the check reports when it
has stopped demonstrating the bug it was written for.
Needs eyes: rega to re-shoot row 13 on this sha.
Driver, on MapLibre: opening a region dashboard does not zoom to the region the
way Mapbox does. This is why.
fitTheirWay read `m.getProjection().name` OUTSIDE its try. The two engines
disagree about both halves of that API:
Mapbox getProjection() -> { name: 'globe' } setProjection('mercator')
MapLibre getProjection() -> { type: 'globe' } | undefined
setProjection({ type: 'mercator' })
MEASURED in maplibre-gl@5's own source, which we load from jsDelivr:
Map.getProjection() returns `this.stylesheet?.projection`, and the OpenFreeMap
style we hand MapLibre declares no projection key at all — its top level is
version, sources, sprite, glyphs, layers. So getProjection() is UNDEFINED there
and `.name` threw a TypeError.
Because the read sat outside the try, the throw escaped fitTheirWay AND escaped
applySubject(), of which it is the last statement. The camera was never framed:
the map stayed wherever it was instead of flying to the region.
The read is now defensive and accepts both shapes, and the WRITE is
engine-correct: Mapbox takes the bare string, MapLibre reads e?.type, so handing
it a string would set undefined rather than fail loudly.
It was wrong in a second way worth recording: on a MapLibre style that DOES set
globe, the old read returned null and silently skipped the mercator correction
that exists to stop the fit pulling back. So it both crashed on one shape and
under-corrected on another.
Proven headless per the Playwright policy, with the old expression run against
the same stubs as the control: it THROWS TypeError on the real MapLibre shape and
returns null where it should see globe, while the new one returns 'globe',
'mercator', 'globe', null, null across the five engine shapes. A check that
cannot be shown to fail proves nothing, so the check fails if the old expression
ever stops throwing.
Needs eyes: MapLibre, region dashboard — does it now fly to the region.
The map sheet and both bridge panels say who produced the Scripture; the language
list was the one surface a reader can arrive at that still answered "where"
without "who". 757 of the 5,067 rows now carry the organisation and year beside
the language.
A name rather than a link, for the same reason as everywhere else: the doctrine
screen is an allowlist, and a publisher must not enter this map as a side effect
of a data refresh. Measured after the change — 16,963 links, unchanged, and not
one credit contains a URL.
It needs no catalogue entry because an organisation's name is the same in every
locale. That is also why it can sit beside the translated labels around it
without leaving a hole in them.
TWO DEFECTS FROM THE UNIVERSAL-PROPS MATRIX, and they are the same defect at two
levels: a component can be perfectly localized and still show English, and a
page can be perfectly translated and still be laid out backwards.
★ THE HEADER HAD NO i18n AT ALL. Every label at the top of Big Picture was an
English literal passed as a PROP — FilterDropdown translates nothing, it renders
what the caller hands it — so Region, Bloc, People Group and Religion sat in
English above an Arabic page. They now read the catalogue, grid namespace first
because this header drives the cap-2 grid and its sibling reads the same keys.
Reset uses controls.reset_all, so its English becomes "Reset all": an existing
entry rather than invented wording, and arguably more accurate, since it clears
the search too.
Its search placeholder and two aria-labels have no usable entry — the nearest,
grid.search, drops "or countries", which this search actually covers — so they
are on the DECIDE card rather than quietly reworded. They are invisible to
check:composite-locale-leaks by design: its criterion is that the catalogue
already holds the string, and it does not.
★ RTL IS A DIRECTION, NOT A TRANSLATION. createAppI18n reflects an RTL locale
onto document.documentElement, which is right for a leaf page it owns and
useless for an element dropped into somebody else's page — the case these
composites exist for. A host asking for Arabic got Arabic WORDS in a
left-to-right layout: readable glyphs in the wrong place, which reads as a
styling quirk rather than the bug it is.
Both composites now set `dir` on their custom-element HOST rather than on the
layout inside it, and that choice is the fix: `dir` takes effect through the
inherited CSS `direction`, and inheritance crosses shadow boundaries, so one
attribute covers the composite's own chrome AND every nested element beneath it
— including the grid half the audit found bare. Only ever SET, never cleared: a
host page that declares its own dir owns that decision.
VERIFIED HEADLESS: three bundles build; setAttribute("dir","rtl") is present in
both composites and controls.reset_all in big-picture; freshness and SRI clean,
nothing else stale; gates:pre 6/6. NEEDS EYES: Big Picture and Simple Research
at ?lang=ar — header labels in Arabic, and the layout actually mirrored.
#10b981 is Tailwind emerald and is not in this product's palette. Worse, the four
gospel leaves did not agree with each other: the map's active mode button was
near-black while the list, the tables and the charts were emerald, so the same
control looked like two different controls depending on which tab you were on.
Light is now the house dark on white, dark is the house sage on near-black, and
all four leaves share it. Contrast measured rather than eyeballed, since this
cannot be checked in a browser here: 9.86:1 and 7.89:1, both well past AA for
12px bold. Sage with #3b463d text was the obvious pairing and was rejected at
4.20:1, under the 4.5 this size needs.
The chart COLOURS are deliberately untouched. They encode meaning — scripture,
film, found, nothing, unresolved — and have to stay distinguishable from each
other, which is a different constraint from matching a palette. Recolouring them
is a judgement about legibility and belongs in its own change with someone
looking at it.
check:composite-freshness found stale on main: none. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
The two discovered Experimental tabs fell back to their English bundle titles in every
locale (Arabic rail: 'Pplr Data Maps', 'Wall Of People Groups'). Only doxa-maps-spa's own
bundle is rebuilt; its SRI refreshed in both manifests.
The sheet names who translated the Scripture, and a team printing posters or
building QR codes exports the CSV and never opens the sheet. A lead that only
exists on screen is a lead that does not reach the people doing the work, so
"Translated by" and the year are columns now: 864 of the 2,107 rows carry a named
body across 33 organisations.
Left empty rather than filled where the catalogue names a licence instead of a
translator. The column says who to ask; "public domain" answers a different
question and would read as an answer to this one.
Two pools were considered and deliberately not attributed. Faith Comes By
Hearing puts the producer inside a free-text version name — "Okpe, Delta - Bible
Society of Nigeria" has it, "Malkus Mawyon" does not — and splitting on the dash
would invent an organisation for the rows that have none; its own API, which
would answer properly, needs credentials this build does not have. Door43's URL
owner IS authoritative but is frequently a catalogue or a language namespace
rather than a body, so "translated by Door43-Catalog" would be false for 116
entries. A name nobody can act on is worse than a blank.