A host embedding this dashboard with profile-config {"locale":"ar"} got an
entirely English page. The config was threaded correctly and reached the
children; nothing ever turned it into an i18n locale.
WHY THIS COMPOSITE AND NOT ITS SIBLING, which passes the same gate untouched.
Simple Research mounts its children BY TAG, so every child is its own custom
element with its own ProfileLoader — and ProfileLoader is the only thing in the
tree that watches `config.locale || config.lang` and drives the i18n locale from
it. Big Picture BAKES its children in (it imports the .vue files), so there is
no loader anywhere beneath it and nobody was doing that job. The entry does
provide appI18n; it was simply never told which language to use.
So this composite now does what ProfileLoader does for the other one: inject the
i18n instance and drive its locale from the host's lang/locale.
MEASURED with check:host-locale-contract, before and after:
dashboard-plus-grid host 0 of 2121 -> host 32 of 2121
32, not 2121, and the remaining gap has a single known cause: most of this
dashboard's chrome is read through tStatic, which resolves the locale from the
URL and the document and cannot see a locale set at runtime. That fix lives in
library/i18n/index.js, is measured, and is parked behind another agent's landing
in the same file — the measurement also showed it must be backed by a
window-level store rather than module state, because a composite loads several
bundles and each carries its own copy of that module.
resources-list's chrome is translated in all eleven — the three filter tabs, the
generic resource names, the people-group count — and the table's column headers
are localised at render in data-tables. resources-map, data-tables and charts now
report zero leaks by the corrected gate; what remains is in resources-list's
data-driven rows.
A CLAIM I MADE AND HAD NOT MEASURED. The previous commit said the CSV export
"keeps the data file's English field names". It does not: Tabulator exports the
column titles it is given, so a Russian reader's CSV heads "Народ","Страна",
"Регион". I checked only after asserting the opposite. Left translated on
purpose — this is a gift for people to use and someone who exports what is on
their screen should get what was on their screen — with the stable machine
contract being data/tables/ itself, which is untouched and English.
THE GATE NEEDED A THIRD CORRECTION, and this one mattered most. Substring
matching over the rendered text reported keys that were never rendered at all:
"Language" matched inside the DATA — the people-group language "Sign Language" —
and "Region" inside a region name. No length filter separates a label from a
word sitting inside one. The unit is now an element's OWN label, and a leak is a
label that EQUALS the English value while a translation exists. A language name
can no longer impersonate a UI string.
Scripts are alphabetised per the fleet convention, so parallel landings stop
colliding on the last line of package.json.
- check:host-locale-contract: every leaf loaded with ONLY profile-config.locale="ar" must draw at
least half the Arabic the same page draws at ?lang=ar; regions-list is the positive control.
Fails on this commit for 18 leaves (bundles that never provide appI18n, and every composite).
- check:staging-lang: the staging page at ?lang=ar embeds its leaf with lang=ar and the Gospel map
renders gospel.mode.gap in Arabic. staging-forge.js (iframe + open link) and
staging-reference-v2.html now forward ?lang= / ?locale= via withLang(); fails on the parent.
- browser-slot.sh: `exec 9>f 2>/dev/null` redirected the whole script's stderr, and the gate
exec'd after it, to /dev/null — FAIL lines and "not measuring" messages were being discarded.
The redirection is now scoped to the open.
- verify-map-paint: the two regions map leaves join the default list (both paint, local and live).
- Vision card reads ?lang= / ?locale= like the rest of the library.
A reader on an Arabic page saw a handful of English words among the Arabic:
the table's "People Groups" title, its count line, the Engagement column, the
Gospel Access chart title, and the side drawer's close button. Every one of them
had a correct Arabic entry sitting in the catalogue; nothing looked it up.
THE CHECK CAME FIRST, AND IT REPLACES A SWEEP THAT PASSED A BROKEN TREE.
The old method sorted every rendered string into third-party / data / chrome and
counted only chrome. That split is a guess, and it guessed wrong exactly where
the defect lived: single words like "Region", "Prayer" and "Country" are as
plausible as values as they are as labels, so they scored as data and the sweep
reported zero on a page that was visibly leaking.
check:composite-locale-leaks asks a question with an answer instead:
the English catalogue holds this exact string as a value, AND
this locale holds a DIFFERENT value for that key, AND
the page still renders the English one.
No judgement about what kind of word it is — if the translation exists and the
reader is not getting it, that is the defect. The two surfaces a lookup cannot
judge (vendor map chrome, and table/facet cells carrying API values) are muted
by NAME with a stated reason, never by word, so muting one can never hide the
next real leak that happens to reuse it. The gate failed on this tree naming 9
strings before any of them were fixed.
Also threaded here: both composites now accept `lang` / `locale` in
profile-config and pass it to the children they mount, alongside `tk` and
`provider`. A partner writing {"lang":"ar"} reached nothing before — the knob
was documented and read by nobody.
The prayer facet's label in DashboardResearchFilters is now a catalogue getter
too. Measured before touching it: dashboard.config.js does define prayer_level
and FACETS spreads the config last, so that literal never rendered for this
config — it is a default for a config that omits the facet, and it stays, with
the merge order written down so the next reader need not re-derive it.
Four strings remain English with no catalogue entry at all — "Top countries",
"Select all {n}", the table dock's title, and the singular "group". They need
wording in ten languages that cannot be composed from existing entries, so they
are a DECIDE card for the Driver rather than translations invented here.
The Driver asked for "all experimental plugins working"; this makes it a
measurement. The gate derives the shelf from the Research Gallery's own rules
(manifest minus curated tabs and NOT_EXPERIMENTAL), then checks every bundle on
its leaf page and on its Gallery tab, under mapbox and maplibre. It requires
content inside the plugin's own element, 0 page errors, 0 failed requests, no
loading text, and for each map 'load' fired AND its own GeoJSON data drawn. An
empty source does not count. Maps are found by wrapping the engine's Map class
in an init script, so no bundle changes for it. It serves this checkout on an
OS-assigned port, and self-tests first: a deliberately broken bundle must fail,
or it exits 3.
Prayer Walk Planner's data is the user's own (zones in localStorage), so the
gate seeds one zone and requires it drawn, printing the seed and reason on every
run rather than exempting the bundle.
It found two real defects on the way. Both are fixed here, and both failed the
gate before the fix:
- UPG Neighbor: every label layer asked MapLibre's default fonts (or Mapbox's
DIN) from OpenFreeMap, which 404s, so no label drew under maplibre. labelFont()
now picks Noto Sans under MapLibre; Mapbox is unchanged.
- Prayer Walk Planner (always MapLibre): zone titles never drew, for the same
reason. The label layer now names Noto Sans Regular.
Result: 28/28 PASS across all 7 shelf bundles.
The Driver asked for full-screen, and for "a print that stays sharp when
zoomed".
Full screen. The planner mounts the shared FullscreenButton (the library's
Fullscreen-API control, with its CSS pseudo-fullscreen fallback for iOS
Safari) as a map overlay. It full-screens the whole planner, toolbar and
map, not just the canvas. Measured from a 520px embed in the Research
Gallery on a foreign page: the planner took 1280x800 and the map grew 459
→ 739px. The label is translated (Arabic "تبديل ملء الشاشة"), because the
planner now keeps the shared vue-i18n instance on its own locale. Before,
Share and Full screen took the PAGE language even when the gallery sent
another.
Print resolution, measured, not asserted. The printed map lays out at
7.53 x 8.4 in on the letter sheet (printable width = 8.5in minus the two
12mm margins; emulateMedia alone does not apply @page margins, which
misread it once as 8.48in). Its image is 2400x2688 px, so 319 x 320 DPI.
A 1:1 crop of the zoomed print was read: small street names (Sancaktar
Yokuşu Sokağı, Baki Dede Sokağı), individual building outlines and
footpaths are crisp, because the map is rendered from vector tiles at
pixelRatio 2, not upscaled from the screen. One PDF page.
check:prayer-walk-planner gains both: full screen from a 520px embed must
take the viewport, and the printed map must measure >= 300 DPI. 10/10
PASS.
The Instagram-style prayer wall (2,107 DX people groups as photo cards on a d3
canvas, with a pray dialog and comments) existed only as a no-build SPA whose
components were template strings compiled in the browser. It is now a bundle:
the components are SFCs in src/components, and the root is the app's own
container without the router. The API developer panel does not ship. Pinia comes
from the package. The canvas is found by ref, not document id, which cannot see
into a shadow root. Font Awesome is registered on the document too, because
@font-face does not load from a shadow root.
Data is the live pray.doxa.life list. The uupg.doxa.life endpoint no longer
answers, so that source and every photo URL built on it are gone. The card and
both dialogs show the API's image_url instead; before this, every dialog read
"No image". The 2.6 MB CSV fallback duplicated the live API and does not ship.
Missing photos draw inline, not from via.placeholder.com. Prayers and comments
stay in localStorage under a wall-pg: prefix, and the "Print (Coming Soon)"
button, which only alerted, is gone.
Measured on a served built tree: the leaf page, a script embed on another
origin, the Research Gallery tab and a 390 px phone each load all 2,107 groups
with photos and Font Awesome, 0 page errors, 0 failed requests. Zooming in and
tapping a card's heart opens the prayer dialog with the group, its status and
its photo.
A reader who picks a religion family on the map now sees the grid narrow to the
same peoples, and a language selection travels the same way. Region already
linked; these two did not, because the two halves do not name a religion the
same way and nothing yet proved what the honest translation was.
The map filters religion through its legend's flat tab, keyed by the FIRST
LETTER of the religion code ('B'); the grid filters through a derived axis that
runs library/colors/religion.js forward per row ('BLA' -> 'Buddhism'). One
shared table, read in opposite directions, copied by neither half — so the wire
carries the family NAME and each side resolves it against its own data.
Two measurements changed the design on the way:
- mapStore.filters.religions, which the map's filter panel writes, is read by
NOTHING: the mapLayers.applyFilters() it was meant to reach is called and
never implemented anywhere in the tree. A link routed through it would have
set state, passed a smoke test and dimmed no pin.
- the map's legend selections are mutually exclusive by design, so it can
light exactly one of region/religion/language. The composite mirrors that
rather than letting the grid hold three facets the map cannot show.
Religion links at the FAMILY and not at the code, deliberately: a map cannot
distinguish 'BLA' from 'BMA' — both dim as Buddhism — so linking at the code
would promise a precision one half cannot keep.
check:linked-vocabulary proves it by measurement rather than by reading: it
compares the total reached over the composite's wire against the total the grid
gives for the same facet on its own (83 of 2,107 both ways), and runs the MUTANT
first — the legend key 'B' instead of 'Buddhism' — failing as NO-TEETH if the
wrong mapping scores the same as the right one. Measured: mutant 0, real 83.
The CLI doc's apk:debug quote was realigned in passing; package.json had gained
app:start-page and the doc still described the older chain.
On a 390px phone the toolbar wrapped to three rows, 139px of an 844px
screen above the map a walker is trying to read. Below 640px it is now one
row that scrolls sideways: no label is truncated, and the two actions a
walk needs, Draw and Print, lead the row so they are always on screen.
Under provider=maplibre (the keyless gallery) four of the seven map views never
finished loading. They built `new mapboxgl.NavigationControl` in the load
handler, and that global does not exist under MapLibre, so they sat on
"Loading…". Popups and the pin marker had the same fault. Every such call now
goes through glFor(map), the namespace of the engine that built the map. The
views' styles and the Dark/Satellite/Light switch now use registry names (ink,
hybrid, paper), which are the same Mapbox styles under Mapbox and OpenFreeMap
under MapLibre.
The search box loads its geocoder by engine, as GeocoderComponent does. Under
MapLibre it is MaplibreGeocoder over this map's own local index, so the keyless
gallery makes no request to api.mapbox.com at all.
The legend's row caret, clear chip and collapse/reopen buttons carry accessible
names and aria-expanded from legend.a11y.* and aria.* (optional i18n, English
fallback), the keys the library legend uses. check:shared-reached now counts
pplr-data-maps as carrying legend-a11y-keys.
window.__pplrMap() reports what the active view's map did (engine, load fired,
style layers, this bundle's data sources), so "loaded" is asserted, not inferred.
Measured on a served built tree, host page on another origin: all ten views
loaded under MapLibre and under Mapbox (load fired, style layers and data
sources present, canvas not blank, 0 errors), with 0 api.mapbox.com requests
under MapLibre. The same check on the parent build fails four map views under
MapLibre. A control with no provider makes 13 Mapbox requests.
Driver: the batch tool is "super buggy", and letting you change the
colour of regions is NOT what is wanted - "the batch selection tool
should let you create your own poster or prayer packet based on your own
selection."
So the selection stops being a thing you PAINT and becomes an INPUT to
the two artefacts this app already makes. Gone: the eight colour
swatches, the custom hex input, and "Copy selection", which produced a
clipboard string and a recolour and nothing a reader could pray from.
In their place the HUD offers Prayer List, Poster Map and Clear, in all
eleven locales - the keys already existed, so this adds no translation
debt.
NO THIRD PIPELINE, which the brief rules out. Both actions end in
printPacket / printPoster, the same calls the legend and the poster wall
make. The selection is handed over as a SYNTHETIC list whose selector is
a pg_slugs union, because 12 of the 27 lists already use pg_slugs and
the evaluator THROWS on that type: resolving the union to concrete slugs
once, against a single fetch of the people-groups catalogue, gives every
existing consumer the shape it already handles. Resolving per zone would
refetch the whole catalogue per zone and still leave the caller to merge.
The packet's map page is per-zone artwork, so a multi-zone selection
prints a cover WITHOUT the map rather than letting one zone's map stand
in for all of them. A poster has no artwork for an arbitrary union at
all, so with several territories selected it says so plainly instead of
silently printing the first zone's poster as though it covered the
selection - generating one would be the new pipeline the brief forbids.
MEASURED at the rendered layer, driving the documented __UPG_SELECT__
gate API rather than guessing at the interaction:
selection 3 territories, exactly Middle East Core / East Africa /
Sudan, batch mode on
recolour UI swatches gone, hex gone, copy gone
buttons en "Prayer List / Poster Map / Clear selection",
ar "قائمة الصلاة / خريطة الملصق / إلغاء التحديد"
page errors 0
Wrote this after shipping a page that returned 200 for every asset, reported
zero console errors, and was completely dead: a throw during setup dies inside
the custom-element boundary. Zero errors is not the same as working, so this
does not trust their absence — it looks for PRESENCE, on all five leaves, in
five steps that each name their own failure: the tag is defined, the host is in
the document, a shadowRoot is attached, it has children, it renders text.
Its first real run went red on all five, and it was right. Every gospel leaf was
dead with "Identifier 'Ge' has already been declared" — while the SRI hash
still matched and every request still returned 200. My own doing: four profiles
imported en/gospel.json directly for an English fallback, and library/i18n
already pulls that exact file in through an eager glob, so the IIFE carried two
instances of one module.
The fix is one module: library/i18n now exports EN_MESSAGES and the profiles
read it from there. The duplicate is gone.
Measured after: all five leaves mount with 3,228 to 233,307 characters of
rendered text, verify-gospel-map passes with 2,107 pins and the gap cutting to
370, and --poison confirms the check goes red on a page with no element at all.
The lesson is the gate's whole reason to exist. I had rich assertions about pin
counts and filters and none of them ran on data-tables, charts or the list, so a
bundle-level failure could take out every leaf while the one gate I did run was
pointed elsewhere. Cheap and universal beats rich and narrow for THIS question.
Every map translated its own chrome but left the basemap's country, city
and street names in the style default (English on Mapbox, Latin + native
on OpenFreeMap) in all eleven locales. A reader in Arabic saw an Arabic
legend over an English map.
library/maps/localize-labels.js uses each engine's supported mechanism:
- Mapbox: map.setLanguage(lang). It also localizes the Standard style's
IMPORTED basemap, which a layer rewrite cannot reach.
- MapLibre: basemap symbol layers point at
['coalesce', name:<lang>, name:latin, name]. English restores each
layer's original expression, re-captured on every style load so a theme
switch never restores the previous style's labels.
Only layers whose text-field reads an OSM name field are touched. A
plugin's own label layers (zone titles, group names) are never rewritten.
useMapInstance applies it on every style.load and when the reader's
language changes (ProfileLoader's 'lang', else profile-config locale, else
?lang / <html lang>). The Prayer Walk Planner drops its private copy for
the shared one.
Measured on the Prayer Progress map, ar/zh/ru/hi:
- Mapbox getLanguage() == locale in all four.
- MapLibre: 13/14 symbol layers localized, and the plugin's own label
layer untouched.
Read in the Arabic screenshots: القاهرة, الإسكندرية, السويس, الأردن on
both engines. check:gallery-embed (+ :maplibre) and
check:prayer-walk-planner pass. The shared change reaches all 11 bundles
that compile initializeMap, and all are rebuilt here. check:shared-reached
marker basemap-labels-localized: 10/10 consumers.
The table offered 107 groups a next step the map did not show. The candidate is
now computed once, in the pin build beside the curated bridge, and both surfaces
read it — the same rule that put the tier in one place. 1,167 candidates across
the research index; 107 of them reach Doxa groups the curated bridge misses.
The map's sheet renders it as its own weaker panel, and the words carry the
limit: "Possibly a way in — worth checking locally", with the language, how
many groups there speak it and what share of the country they are, and "not
ranked and not recommended". No strength, because none can honestly be
computed. Never for a sign language.
A REGRESSION I INTRODUCED AND ALMOST SHIPPED. The edit that wired this used
pinCandidates without declaring it — a string replace that matched nothing and
said nothing. The ReferenceError died inside the custom-element boundary, so the
map never mounted AND the page reported zero errors. My probe then failed with a
30-second timeout that I had caused separately, by passing playwright's timeout
as the waitForFunction ARG rather than its options, which had been silently
using the default in eleven scripts. Two faults stacked: a broken page that
looked error-free, and a probe too impatient to say so.
Zero page errors is not the same as working, and that is the only reason this
was caught: __grState never appeared, which no assertion about pins could have
explained away. Verified after the fix: 2,107 pins, gap cuts to 370, the
candidate panel renders for Abai Sungai naming Malay, and 25 Deaf sheets still
show no bridge at all.
E RESIDUALS, from rega's in-gallery re-sweep. All three lived in SHARED
components, which is why a grep of my own plugin found nothing:
SearchFilterHeader its label map was a literal object of English defaults —
the fallbacks every host gets, since no host passes
`labels`. Filters, Selected, World and the chip remove
button now come from the catalogue; a host's own labels
still win, which is what the prop is for.
ResearchMapFilterPanel the two "Any" options and the "{n} active" pill.
DashboardFacetRail the Expand/Collapse aria template, which repeats per row:
one untranslated template is dozens of English strings in
a screen reader.
dashboard.config.js the facet SECTION titles (People, Place, …). The earlier
pass converted `label:` and missed `title:`.
★ AND A LOOKUP BUG THAT LOOKED EXACTLY LIKE A MISSING TRANSLATION. 'Any' and
'0 active' stayed English after being catalogued, because the panel asks for
`controls.<key>` while I had authored them under `dashboard.<key>`. The
catalogue had the words; the component was looking one namespace over. Keys now
live where the panel reads, and its lookup falls back across both, so a string
authored in either place renders.
MEASURED after, ru, on the built tree: 0 English chrome strings on Simple
Research and 0 on Big Picture.
One probe correction worth keeping: a substring match on 'People'/'World'
flagged 'South Asian Peoples' and 'Arab World' inside aria labels whose frame
was already translated — data, not chrome. Whole-string matching separates them.
TOUCH TARGET (regions' phone sweep): .rd-proj "🌐 Globe" measured 74x23 at
390x844, one pixel under WCAG 2.5.8 AA. min-height:24px under
@media (pointer:coarse) — stating the 24 rather than growing padding, because
the height is padding plus a line box the renderer derives from the font and the
arithmetic lands at 23.7. Mouse sizing untouched. MEASURED after: 73.5x34.
Every bundle embedding the changed shared components is rebuilt in this commit —
a shared fix is not landed until its consumers are.
composition/_switcher.scss has three rules and the clone carried two. The third —
`.switcher > :nth-last-child(n + 6), .switcher > :nth-last-child(n + 6) ~ *
{ flex-basis: 100% }` — gives every child a full row once there are six or more,
instead of letting them share. It is now ported onto .rd-switcher.
It changes nothing today and that is measured, not assumed: the rendered page has
3 direct children at widths 372/372/372, the same three goal cards their
RegionDetail.vue lays out, so a threshold of 6 is out of reach. It is carried
because the brief is an exact clone, and because a fifth or sixth goal card would
otherwise diverge silently from doxa.life — a one-line rule now, or a puzzle later
about why six cards behave differently here.
The other two rules in that file are deliberately NOT ported, which is a
measurement rather than an oversight: .switcher-item{display:flex} never applies
because their switcher's children are `.stack | card`, not `.switcher-item`, and
.three-part-switcher belongs to a component the regions page does not use.
The probe proves both halves, because a rule that cannot be shown to fire is
decoration. It reads the real switcher, asserts no child has flex-basis:100% at
the live count, then clones cards INTO it until there are six and reads the basis
back as 100% before restoring it. Two of my own errors are recorded in it: a first
run drove the page with ?region=africa, got the chooser, and reported "no
.rd-switcher" — a reading of the wrong page state, since the selection comes from
the hash; and a second hardcoded "+2 clones = 6" from a static tag-count that said
4, so it only reached 5 and called the rule dead when its own arithmetic was wrong.
It now derives the clones to add from the count it just measured.
The doxa.life script embed was blocked on a server change: the live root
manifest.json answers without Access-Control-Allow-Origin, and the gallery
fetch()es it cross-origin, so no tab could mount. Measured on nuxtbase,
though, the same static layer DOES send ACAO on every path inside a folder
of the tree (doxa-maps-spa/<anything>, even a 404; assets/…).
So generate-tree writes a byte-identical manifest copy at
doxa-maps-spa/manifest.json in the same step as the root one (they cannot
diverge). The gallery reads it FIRST when the tree is on another origin (no
CORS error in the host's console) and the root first when same-origin.
Script URLs still resolve against the tree root, never the copy's folder.
check:gallery-embed:live-cors serves the tree with the live rule
(serve-strict LIVE_CORS=1: no ACAO on the root manifest only).
- The current gallery under it: "did not boot within 60s" (negative
control, reproducing doxa.life).
- With this change: js 14, iframe 14, neighbour 14, linked 4. The default
gate also passes all phases.
check:embed-targets, check:drop and check:public-hygiene pass with the copy
present.
The server-side CORS rule (serving/traefik-embed-headers.yaml) is still the
right long-term fix, and it needs server access. This unblocks the embed
without it once the tree is published.
Every conflict was a build artifact — another agent had pushed a full rebuild of
all 23 bundles. Minified .js and baked-SRI leaf pages are generated files: merging
their text is meaningless and can produce a bundle that matches neither side, so
they are resolved by taking one side and then rebuilding from the merged source.
The incoming diff carries 68 source files, among them library/composables/
useMapInstance.js and the dashboard/gospel catalogues in 10 locales, so the
rebuild is all 23 bundles and not only the ones I touched — regenerating the tree
without rebuilding has shipped a stale .js here before.
The curated bridge covers 168 of Doxa's 369 with nothing, because it comes from
a curated subset rather than from the data. A candidate IS computable for the
rest: the largest language in their country carrying both Scripture and the film.
WHAT I COULD NOT COMPUTE, AND THEREFORE DO NOT CLAIM. Ranking those candidates
needs a notion of "is this a lingua franca", and neither available proxy
survives contact. By GROUP COUNT, Oman picks Swahili — two groups — for an
Arabic-speaking people. By SHARE OF PINNED POPULATION, a 10% floor REFUSES Urdu
in Pakistan at 7.3%, the national language, because few Pakistani groups are
pinned as Urdu speakers. That is the same bad-denominator mistake this map
already fixed on the choropleth. So no strength is invented: the candidate ships
with its group count and its share, under column names that say "candidate",
and a reader who knows Pakistan can see Urdu at 7.3% and know what to do with it.
A score would have thrown it away on their behalf.
AND THE ERROR CAUGHT BEFORE SHIPPING. The candidate logic was offering 89 Deaf
groups a SPOKEN language — Deaf Afghans to Turkmen, Deaf Arubans to Dutch. That
is not a weak bridge, it is not a bridge, and a mobiliser acting on it would be
sent to do something that cannot reach the people. This build already refuses to
search GRN for sign languages because GRN publishes audio; the candidate logic
had inherited the gap. Now: 92 Deaf groups are offered none, and their gap is
named as the access problem it is.
Of the 369 with nothing: 168 curated bridge, 107 computed candidate, 92 Deaf,
2 with neither.
inheritAttrs:false on the studio root, so a host's attributes (style,
class, id) stay on the <plugin-studio> element instead of also landing
on the layout inside it: the trap that made the Prayer Walk Planner
76px tall in the gallery. Layout verified unchanged in four hosts on the
built bundle: its own page (shell 1280x900, body 836), a bare partner
snippet (85vh default), a host that sets its own inline height (the
style stays on the host; the shell root has no style attribute), and
the app-shell mounter from BUG-EMBEDDED-PLUGIN-STUDIO-BLACK-SCREEN,
display:block forced on the host: body 656px, rail and tree rendered.
That bug no longer reproduces on the dx-maps side.
internal/measure-map-load.mjs: per production map, the cold, throttled
waterfall (bundle, engine script, style, data, basemap up, visible
tiles loaded) on a served built tree, from the leaf page or, with
--embed, from a partner page that pastes only the embed snippet. It
showed that Vue is already inlined in every bundle and that the engine
waits on the bundle only on partner pages.
The access table could say "nothing found" and stop. For 168 of Doxa's 369
that is not the whole truth: a language already spoken widely in their country
has Scripture and the film. Afitti in Sudan has nothing of their own; Sudanese
Arabic, spoken by 65 groups around them, has both. That is the difference
between no way in and a way in that is not their heart language, and it is the
next action a mobiliser can take — which is why it belongs in the EXPORT and not
only in the map's sheet, where it has been sitting unexportable.
Four columns: the bridge language, its ISO code, the strength, and how many
groups in that country speak it. Keyed by code|country, because the same
language bridges differently in different places.
IT CLOSES NO GAP AND THE DEFINITION SAYS SO. A bridge is a way in, not a
translation. These 168 are still counted in "nothing found", because they still
have nothing in the language they think in, and the first believers among them
still cannot be trained from a text that does not exist.
The other 201 have nothing AND no bridge. That number is worth having on its own:
it is the isolated set, where neither their own language nor anything spoken
around them carries the gospel.
Verified end to end: the CSV exports 2,107 rows carrying Bridge language, Bridge
ISO, Bridge strength and Groups speaking it, and the Afitti row reads Arabic,
Sudanese.
library/maps/rtl-text.js installed the RTL text plugin for MAPBOX only
(window.mapboxgl, from api.mapbox.com). So every MapLibre map, i.e. every
keyless embed, drew RTL place names as disconnected, reversed glyphs.
Measured on the Prayer Walk Planner's printed Arabic sheet.
Under it was a second gap. Bundle entries call the installer at LOAD
time, before useMapInstance has lazily injected a MapLibre engine, so even
an engine-aware installer found nothing to install into.
- rtl-text.js is engine-aware. Mapbox is unchanged (same URL, same lazy
call). MapLibre gets the same plugin package from jsDelivr, keyless and
not api.mapbox.com, so keyless pages still make zero Mapbox requests.
New ensureRtlTextPlugin(gl) waits until it is LOADED, for callers that
capture a single frame (print).
- useMapInstance installs it the moment an engine global exists, the one
point every map passes through.
- The planner drops its private copy for the shared helper.
Measured: a print map of downtown Cairo (MapLibre, keyless) shows shaped
Arabic street and place names (شارع التحرير, مسجد الطباخ …); plugin
status "loaded", fetched from jsDelivr. check:gallery-embed passes js,
iframe, neighbour and linked (14 tabs); :maplibre passes;
check:prayer-walk-planner passes.
The shared change reaches all 11 bundles that compile initializeMap, and
all are rebuilt here. check:shared-reached gains an rtl-text-maplibre
marker (10/10 consumers carry it; cities-internet, gospel-resources and
my-upg-100-list boot maps another way and are not affected).
Also: the new Experimental bundle pplr-data-maps loads the Mapbox geocoder
under maplibre (pplrData.js:38). It is an owned, printed MAPBOX_KNOWN entry
routed to its owner; delete it in their fix commit.
TWO FIXES THAT BOTH SHOW UP ONLY WHEN THESE PLUGINS ARE SOMEBODY ELSE'S GUEST.
1. ATTRIBUTE FALLTHROUGH. These SFCs mount AS custom elements, so a style the
host puts on the tag lands on the component's own root element. MEASURED with
a host style of "display:flex; position:relative; height:100%": that exact
string appeared verbatim on the inner root div of people-groups-grid,
stack-demo, research-map-plus-grid and dashboard-plus-grid. The gallery sets
one on every tab, and the same fallthrough collapsed the Prayer Walk Planner
to 76px. inheritAttrs:false on each root — a host styling the TAG is styling
the box, not our internals.
AFTER: the inner root carries no inline style on any of the four, and each
still renders full height (2997-4010px in the harness), so the guard did not
become the collapse it prevents.
Worth knowing for the next probe: shadowRoot.firstElementChild is a <style>
node — Vue injects styles first — so a probe that trusts it measures nothing
and reports "no fallthrough, nothing to fix". Take the first non-style child.
2. THE RESIDUAL CHROME the in-gallery re-measure found: the dashboard title,
Align Dashboard and its tooltip, World, Vertical Legend, groups in scope, the
per-row aria templates (Filter by / Remove filter / groups), and the map
drawer's aria label. Catalogued in all 11 locales and read through tStatic,
reusing existing wording where the catalogue already had the word.
The aria TEMPLATES matter more than their size suggests: they repeat per row, so
one untranslated template is dozens of English strings in a screen-reader's ear.
resources-list and charts turned out to be almost entirely explanatory prose.
Their actual chrome is four strings — two page titles, the loading line and
Close — and those are now in all eleven locales. The blanket dir=ltr is off both
roots, so Arabic gets its layout, and each English prose block declares its own
direction where it sits.
That completes option (b) across all five gospel profiles. What stays English is
what carries doctrinal and statistical precision: which pools were searched, what
"nothing found" does and does not claim, why Open Bible Stories is not
Scripture. A machine translation of those would read as authoritative while
being wrong, and one translated line in each locale says so in the reader's own
language.
Verified on a served built tree whose identity I proved first — the Russian title
appears once served and once on disk. en/ar/ru on both leaves: heading
translated, 0 clipped, dir=rtl only for Arabic, no page errors and nothing 4xx.
A composite shell copies a whitelist of keys from its own profile-config down to
each map it nests. `provider` was not on any of the three lists, so a nested pane
read it as undefined and fell back to Mapbox: a partner who asked for MapLibre,
and who holds no Mapbox token, got Mapbox maps in every inner pane. Same shape as
the missing `urlSync` here before it -- an inner map cannot honour a key the shell
never passed down.
The report named country-posters-mfe. All three composites had it: regions-mfe and
upg-100-mfe were missing `provider` too, so fixing only the one named would have
left two thirds of the defect in place.
Verification is a probe, not an inspection, and it needs a control. "0 requests to
api.mapbox.com" passes trivially on a page where nothing mounted -- the most likely
way this measurement goes wrong -- so each composite is loaded twice: once with
provider=maplibre expecting zero, once with provider omitted expecting more than
zero. The second run is what proves the first measured anything; a run that
renders no canvas is reported UNPROVEN rather than passing.
It also cannot be measured on a leaf page. Every leaf loads its map peer in its
own <head> (regions-mfe/index.html line 13 is a hardcoded api.mapbox.com script),
so on a leaf zero mapbox requests is unreachable whatever provider says. The
question only has meaning on a host that picks the peer itself, which is what a
partner embedding the .js actually does.
The last commit recorded "in RTL the leftmost data column clips against the
edge" as known debt. Measured rather than eyeballed, that is wrong. Counting
cells falling outside the table holder: English and Arabic are IDENTICAL — 13 of
20 columns off-screen in both, scrollWidth 3146 against clientWidth 1400 either
way.
A twenty-column table is wider than 1,400px, and RTL mirrors which edge the
overflow sits on. What I saw in the Arabic screenshot was English's right-edge
overflow appearing on the left, which is what mirroring means. I read a picture
instead of measuring, and filed the result as debt — which would have sent the
next person chasing a defect that does not exist.
Whether twenty columns is right for a 1,400px screen is a real question. It is a
design question, and it is the same question in English.