Commit graph

252 commits

Author SHA1 Message Date
Doxa Maps
5b074c1b43 Gospel Resources: an open province sheet also counts as an open sheet 2026-09-30 12:39:29 +00:00
Doxa Maps
27ce3fe8db Gospel Resources and Cities with Internet: the legend sits above its map controls, and under an open sheet 2026-09-30 12:15:27 +00:00
Doxa Maps
bd1239a496 The credit's fold clock starts once the basemap credit has joined geoBoundaries 2026-09-30 09:43:08 +00:00
Doxa Maps
0f5f991dae Gospel Resources and Cities with Internet: one compact licence credit at the bottom end corner, folded after 3 s 2026-09-30 09:32:08 +00:00
Doxa Maps
ce8d67e1e6 Gospel Resources: a comment no longer names another map's translator area, which shipped that whole catalogue 2026-09-30 09:03:58 +00:00
Doxa Maps
5b90a07fbc Gospel Resources and Cities with Internet: Expand legend / Collapse legend in each map's own keys, in all 11 locales 2026-09-30 08:50:31 +00:00
Doxa Maps
113cdc26c7 Gospel Resources and Cities with Internet: the phone legend button reads Expand legend / Collapse legend 2026-09-30 08:41:50 +00:00
Doxa Maps
fcb92ba555 Merge origin/development into gospel/phone-legend
# Conflicts:
#	public/dx-maps/cities-internet/cities-internet.js
#	public/dx-maps/cities-internet/connectivity-list/index.html
#	public/dx-maps/cities-internet/connectivity-map/index.html
#	public/dx-maps/cities-internet/data-tables/index.html
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-30 07:11:18 +00:00
Doxa Maps
916c200797 Gospel Resources' folded phone legend is slimmer, and keeps Arabic on two lines 2026-09-30 07:09:38 +00:00
Doxa Maps
f558b1b43f Cities with Internet and Gospel Resources lift their credit clear of the gallery's round ☰
On a phone the gallery's round ☰ sits in a bottom corner and covered the first ~30px of these maps'
licence credit (the last ~30-40px in Arabic, where it mirrors). The gallery publishes
--map-fab-clearance while the button shows, and every LegendMobile map lifts the engine's bottom
corners by it; these two maps have their own legends, so they never did. They now lift the same
corners, the credit's and a Mapbox wordmark's. The variable is unset without the button, so desktop
is unchanged.

Rebuilds cities-internet and gospel-resources, with their pages and the two manifests.
2026-09-30 06:19:59 +00:00
Doxa Maps
f61bce7ea6 Gospel Resources: a country or people-group selection tells the tap hint it can go
The tap hint goes away on the reader's first pin, and it listens for uiStore.selectPeopleGroup,
which this map never calls: it selects through its own refs. A selection here now says so with the
dxmaps:pin-selected event the hint also hears, from inside the map's root. The watch sits after both
refs are declared; above them it would throw at setup.

Rebuilds gospel-resources only, with its pages and the two manifests.
2026-09-30 06:07:54 +00:00
Doxa Maps
002b256ce1 Cities with Internet and Gospel Resources get the edge scroll strips on their map
Both maps build their map with new gl.Map rather than through useMapInstance, so nothing ever wired the
shared scroll strips for them: on a phone there was no scroll escape at the map's edges, where every
other map has one. They now render the two strips beside the map and hand them to
useEdgeScrollAffordance, UPG-100's pattern, flagged per embed by edgeScrollAffordance (default on).
The strips sit under everything but the canvas, so the licence credit stays on top of them.

Rebuilds cities-internet and gospel-resources, with their pages and the two manifests.
2026-09-30 05:54:22 +00:00
Doxa Maps
2b70820f24 Gospel Resources on a phone: a slimmer folded legend, and an open one that stops at 70%
Folded, the legend took three rows of large pill-round controls, about 30% of a phone's page. The
Gap / Resources found buttons are 32px now and squared to the fold button's corners, the search box
is slimmer, and the fold button shares the mode row: two rows instead of three. Open, the legend stops
at 70% of the map and scrolls inside, instead of covering all of it. 32px stays above WCAG 2.5.8's
24px floor. Desktop is unchanged.

Rebuilds gospel-resources only, with its pages and the two manifests.
2026-09-30 05:35:14 +00:00
Doxa Maps
7378832cc9 On a phone, Cities' and Gospel Resources' control column starts below the folded legend
The ⓘ is now the column's top control, and its one-time pointer runs sideways from it across the
map's top band. On a phone that band is these maps' legend (title and fold button), and MapToolbar's
phone rule put the column at top:72px inside it, so the pointer lay over the legend's title and
buttons. Below 700px the column now starts 8px under the FOLDED legend's measured bottom, whatever its
height in each language. It is measured only while folded and before the toolbar exists, so opening
the legend never moves the controls. Desktop is unchanged.

Rebuilds cities-internet and gospel-resources, with their pages and the two manifests.
2026-09-30 05:06:12 +00:00
Doxa Maps
792d4e1ae9 Cities with Internet and Gospel Resources tell the gallery their ⓘ is coming
Their control column renders only once the map exists, after the data, which can take tens of
seconds on a slow phone. At mount each map now tells the gallery's MapInfo that a control is on its
way (Prayer Progress's announcement), so MapInfo waits for the map instead of flashing its floating
ⓘ first. A failure or an unmount before the map exists withdraws the promise, so the floating ⓘ
shows at once. Gospel's single-card page builds no map and promises nothing.

Rebuilds cities-internet and gospel-resources, with their pages and the two manifests.
2026-09-30 04:48:10 +00:00
Doxa Maps
5cbabe7ffa Cities with Internet and Gospel Resources: "What can this map do?" is the top of their control column
The ⓘ becomes the first round control in each map's own column (MapInfoButton, as Prayer Progress
has it), and each tab's gallery row says trigger: 'control', so there is exactly one ⓘ: the control.
MapInfo keeps its fail-open, so a page where the control never arrives still gets the floating one.

Rebuilds cities-internet, gospel-resources and doxa-maps-spa, with their pages and the two manifests.
2026-09-30 04:44:32 +00:00
Doxa Maps
bff75e0995 Gospel Resources: in Arabic the people-group card opens opposite the legend, not on it
The desktop card was placed with right:14px while the legend sits at the inline start. In a
right-to-left page the legend mirrors to the right and the card stayed there, so opening any
people group or country covered the legend at every desktop width. The card now sits at the
inline end, as Cities' copy of the same rule already did. English is unchanged.

Rebuilds gospel-resources only, with its five pages and the two manifests.
2026-09-30 01:13:23 +00:00
Doxa Maps
9c6347aad6 Mark resources-map production, because a production page already ships it
resources-map.meta.json carried NO stage, so the manifest defaulted it to
experimental — while doxa-maps-spa, the gallery that becomes Deep Research,
mounts it: doxa-maps-spa.meta.json composes "gospel-resources/resources-map"
and TAB_DEFS names it as the Gospel Resources tab. The record disagreed with
what ships, and the record was the part that was wrong.

WHAT THIS CHANGES: the manifest stage, and nothing a reader sees. Verified —
the diff moves one "experimental" to one "production" and ZERO sha384 hashes.
No bundle is rebuilt and no byte of any map changes.

The stricter production gates now apply to this profile and it passes them:
gates:pre 103 PASS / 0 FAIL / 1 NOT MEASURED of 104. That is the part worth
having, because marking something production is only honest if it then has to
meet the production bar.

NOT a decision about which profile the Deep Research page SHOULD show.
gospel-dashboard is the production composite and remains the better end state;
it is not switched to today because its .gd-map is sized 80svh — a viewport
unit inside a pane that is not the viewport — which against rega's measured
panes gives the map 91% of the pane at 1440x900 and 95% at 390x844, and
because it has never been shot inside the gallery. That fix is written and
held local for after the deadline.
2026-09-29 18:42:06 +00:00
Doxa Maps
45d931fa40 Repair what the union resolution duplicated
Resolving package.json conflicts by unioning unique lines is right for a file
that holds one entry per line. Applied to ordinary source it duplicates
declarations, because both sides of a hunk keep their copy and nothing
de-duplicates across the join.

Eleven plugin entry points ended with the same import twice, and the poster
renderer with two identical `const sheet`. Each is a SyntaxError: every one of
those bundles would have failed to build, and the poster export would not run.

Found by parse-checking every changed .js against the merge base, not by the
gates -- gates:pre passed 101 of 102 with all twelve files broken, because its
parse gates do not cover plugin entry points.

One measurement trap worth recording. `node --check` on a .js file parses it as
CommonJS, so a bare `import` is a syntax error and ALL 28 entry points appear
broken, on development too. That reading says the tree is on fire and is simply
wrong. Copying each file to a .mjs and checking that parses it as a module,
which separates the eleven that are genuinely broken here from the rest that
are fine.
2026-09-29 16:01:45 +00:00
Doxa Maps
212ec20816 Merge remote-tracking branch 'origin/development' into bundler-2/handoff
# Conflicts:
#	package.json
2026-09-29 15:54:16 +00:00
Doxa Maps
c9ffa34e93 Merge branch 'bundler-2/provide-warnings' into bundler-2/handoff
# Conflicts:
#	internal/check-element-bootstrap.mjs
#	internal/check-provide-shadowing.mjs
#	library/elements/defineMapElement.js
#	library/elements/provide-own.js
#	package.json
#	plugin-designs/all-people-groups/index.js
#	plugin-designs/cities-internet/index.js
#	plugin-designs/country-posters/index.js
#	plugin-designs/doxa-research-map/index.js
#	plugin-designs/gospel-internet/index.js
#	plugin-designs/gospel-resources/index.js
#	plugin-designs/key-cities/index.js
#	plugin-designs/my-upg-100-list/index.js
#	plugin-designs/pplr-data-maps/index.js
#	plugin-designs/regions-dashboard/index.js
#	plugin-designs/upg-neighbor/index.js
2026-09-29 14:50:37 +00:00
Doxa Maps
4dbeb257ab The gospel data table escaped its host box when embedded alone
Same defect as cities-internet's list and table: no :host rule at all, so the
element falls back to display:inline with no height while .gt is
position:absolute;inset:0 — which then resolves against whatever the partner
page has positioned.

TWO MOUNTS, ONE RULE, and they do not collide:
- STANDALONE (data-tables.meta.json) this is its own custom element, so :host
  applies and .gt is finally contained.
- COMPOSED, gospel-dashboard renders <DataTables/> as an inline Vue component
  with no shadow host of its own, so :host matches nothing there. Its
  '.gd-tables .gt{position:static;…}' override (gospel-dashboard.vue:81) keeps
  working, which is what lets the stacked section take its natural height and the
  page scroll. That override records a MEASURED failure — docScrollHeight 900
  against clientHeight 900, a wheel gesture moving 0px — and must not return.

min-height, not height alone: with no height, .gt's inset:0 would give the table
0px in an auto-height box, so it would stop escaping by becoming invisible. A
floor gives an auto host something to grow from; a definite-height slot still
decides through height:100%.

NOT the ProfileLoader 100dvh floor (ProfileLoader.vue:410), which is the separate
upstream cause of the connectivity-map overflow and is studio's card. .gt is not
that wrapper, so this fix stands on its own.

(cherry picked from commit 10f2a5c13f8d08c0086ada8eafa4ad819863bd0a)
2026-09-29 14:38:46 +00:00
Doxa Maps
c5e2131fc0 Two lists joined with an English comma in every locale
formatList (R29) replaces join(', ') at the two sites that a reader sees. The
separator was never a missing key — no key can fix punctuation between
interpolated values — which is why both survived every i18n gate.

connectivity-map.vue:365, CONJUNCTION. The Indian states with no boundary are
ALL missing one. The names land inside a translated sentence at :595, and the ar
message reads "لا توجد بعد حدود ولاية لـ {names}: …", so English commas were
being printed inside Arabic prose.

GiCombos.vue:49, DISJUNCTION, decided from the matcher and not from what reads
better: useSelectionBus.js:299 tests cur.includes(value) and toggleFilterValue
adds to the list, so two values on one facet WIDEN the result set. "Africa and
Asia" would state the opposite of what the filter does. This changes the English
too — "Africa, Asia" becomes "Africa or Asia" — which is a gain, because a comma
silently reads as ALL.

Measured, the same three names and two facet values through each type:
  en  "Ladakh, Puducherry, and Chandigarh"   "Africa or Asia"
  ar  "Ladakh وPuducherry وChandigarh"        "Africa أو Asia"
  zh  "Ladakh、Puducherry和Chandigarh"        "Africa或Asia"
  was "Ladakh, Puducherry, Chandigarh"        "Africa, Asia"
ar joins with و and no comma at all; zh uses 、. Neither is reachable by keying.

Not converted, and deliberately: useBoard.js:126 is a console.warn for
developers, and rudder-voice.vue:971 is in a component with zero i18n imports and
no catalogue area — English by construction, so it converts with the rest of
Rudder if Rudder is ever localized.

Source only. The rebuilds are regenerated after this rebases onto landed R29.
2026-09-29 14:01:38 +00:00
Doxa Maps
b1c4c03956 Four resource labels on the pin sheet read English in every locale
The chips a reader sees on a group's sheet — "Scripture,", "Bible video",
"Training", "Gospel presentation" — were plain template text. All four already
had keys with real ar and zh translations, so this routes them through
gospel.res.* and adds nothing: scripture, bibleVideo, training,
gospelPresentation.

WHAT IS DELIBERATELY LEFT ALONE, because it is not a label:
- g.audio.m keeps its raw value and its 'audio' default. That is the table's own
  media descriptor, data rather than UI text.
- gospel-resource-rows.js is already correct. add(title, type, …) translates
  title through tx() and keeps type as the category identifier the rows are
  grouped by; the gate flags those type strings, but translating them would
  break the grouping — the same value/label trap as the region headings.
- d.s.replace('Open Bible Stories', 'Bible Stories') at :2754 rewrites a DATA
  value, not a label.
- data-tables.vue:122 already carries labelKey; its `en:` is the fallback beside
  it, by design.
- The caveat prose (Driver row 80) and the title= tooltips (row 120) are
  untouched and stay that way until those rows are answered.

check:hardcoded-english on gospel-resources: 91 -> 87, which is these four.
2026-09-29 13:08:53 +00:00
Doxa Maps
2b70cc1aa5 Seven more references, in capitals, that a case-sensitive grep could not see
The gate matched "Driver" only, so "THE DRIVER'S ASK" and "HELD FOR THE DRIVER"
passed unseen — and so did my own verification, which used the same
case-sensitive pattern and reported zero. I checked the thing the gate checked
rather than the thing that mattered.

Seven lines across six files: five banner headings, one quoted requirement, and
the row-130 marker. Same rule as the first sweep — keep the reason, drop the
speaker — with the verbatim quote in build-doxa-gospel-access.mjs restated as
the question the map answers, since its content is the requirement and its
framing was attribution.

Comment-only: 0 non-comment lines changed, tree regenerates identically.
Case-insensitive grep across all three plugins now returns nothing.
2026-09-29 10:45:01 +00:00
Doxa Maps
404b2c866e Fourteen English strings in a .js module reached readers in every locale
gospel-resource-rows.js builds each resource row's display text, and its
fallbacks were English literals: a record with no name of its own showed
"Scripture in this language", "Audio Scripture", "Listen in a browser" to an
Arabic or Chinese reader — precisely the rows whose source data was least
complete. check:hardcoded-english does not read .js files, which is why the
bundle's sweep never saw them.

Thirteen keys in eleven locales, and the translator is PASSED IN rather than
imported: the module is a pure function with no component context, and it stays
optional, so a caller without one degrades to the English written at the call
site instead of throwing.

Three publisher names stay verbatim — JESUS Film, Scripture Earth, Zume — but
where a name sits inside an ordinary phrase the phrase is keyed and the name is
INTERPOLATED, so "Zume discipleship training" translates around a brand a
translator never retypes. SE_KIND's values are displayed and take keys; its keys
are the upstream feed's codes and must not be touched.

Verified by running the module both ways: without a translator every row reads as
it does today; with an Arabic one the labels and scopes are Arabic while
"Scripture Earth" and "Zume" are unchanged.

ROUTED TO STUDIO, not self-landed: the eleven locale files make this a library
change. The route test says 11, and the last time I judged a landing by which
plugin the work was for rather than which directories it touched, the scan ran
against most of the fleet and timed out twice.
2026-09-29 10:07:29 +00:00
Doxa Maps
6baf6dba65 Keep the reason, drop the speaker, across all three plugins
The 36 baselined references in gospel-internet (23), gospel-resources (9) and
cities-internet (4) each wrapped a real engineering reason around an attribution.
The reason is what a reader needs; the speaker is process vocabulary that does not
belong in shipped text.

Every one is rewritten in place, keeping the measurement and the date where they
were there: "NUMERALS ARE HELD fleet-wide (2026-09-26)", "(2026-09-27: one more
level of zoom-out was needed)", "Reported from use: it overlapped the bottom of
the page". Where a quote carried the requirement, the requirement is now stated
directly rather than quoted.

Comment-only: 36 insertions against 36 deletions, one line for one line, and no
built text changes. Route test passes — 0 files under library/ or internal/, so
this self-lands rather than going through studio.

Two things I nearly shipped by accident, both caught before committing:
`git add -A` swept in a 1,961-line vite.config timestamp artefact from my own
build, and editing the JSON meta through a parser reformatted "composes" from one
line to three. The artefact is deleted and the JSON is edited as a string, so the
diff is only what I meant to change.
2026-09-29 09:06:28 +00:00
Doxa Maps
2795455688 The legend toggle, the chart titles and "no code" were never translated
Found while classifying the remainder rather than by the gate pointing at them:
the search box's aria-label was hardcoded English — screen-reader text, the same
class as the resize-handle bug that check:i18n-keys-exist documents — along with
the legend's open/close label, the three gap-chart titles, "unnamed language",
the unchecked-link notice, and "no code", which appears at six call sites and is
now one key.

Nine keys, sixteen call sites. 102 -> 88.

These were hiding inside the 102 I had described as mostly-deliberate. The
classification was worth doing for its own sake: four of the five classes really
are deliberate, but the fifth had real defects in it, and I would not have found
them by reading the count.
2026-09-29 08:11:50 +00:00
Doxa Maps
04b38560d1 The loading lines and the caveat lead-ins get their own keys
Five more keys: the table's loading line, the language sheet's population and
countries line, and the two bold lead-ins that introduce the caveat prose in
resources-list. The caveat prose itself stays inside its dir="ltr" island, which
the gate already marks deliberate — only the heading that introduces it is
language.

Not touched, and not oversights: "· GRN", "· Zume" and "· Door43" are a separator
plus a publisher's name, on lines whose translatable half is ALREADY keyed. A
brand name after a middot is not a sentence.

106 -> 102.
2026-09-29 08:11:50 +00:00
Doxa Maps
bfefb87d4f The list sheets count their own rows in one key
"Showing {shown} of {total}." appears twice, for the with-resources and
without-resources lists, and is ONE key used twice. The population suffix and the
related-language line join it; "way in:" reuses the key the gap list already has
rather than authoring a second one for the same words.

Three new keys, five call sites.
2026-09-29 08:11:50 +00:00
Doxa Maps
f24a9e2531 The data-tables hero is one key too
Four fragments around three counts, two bold words and a bdi island — "people
groups are", "people,", "an estimated", "deaths per day." — become one message
with named slots, the same shape as the charts hero.

Verified through a real createI18n: en renders as today, ar carries the clause
order Arabic needs, zh reorders too. The counts and the quoted phrase go in as
values, so the bold and the dir="ltr" survive without a translator reproducing
markup.

115 -> 111.
2026-09-29 08:11:50 +00:00
Doxa Maps
e08d0552d3 The resource sheet's lines are sentences carrying their counts
Twelve more keys across resources-map: the two resource chips ("Scripture,
{medium} · {source}" and "Bible video · {source}", where the type word is
language and the source is data), the sheet headings that carry a count, the
country line, the pray count, and the loading notice.

The separators stay out of the catalogue. A "·" between a type and a source is
structure, not prose, and a translator asked to reproduce it would eventually get
it wrong in a language that does not use it — so the punctuation lives in the
message and only the words vary.

124 -> 115 findings. The ratio is back to roughly one finding per key now the
duplicated spans are gone, which is what I said would happen once the one-off
sentences began.
2026-09-29 08:11:50 +00:00
Doxa Maps
6914f44b2e The gap list's lines are sentences, so each is one key
resources-map's gap list read as fragments around its values: "{count} people ·
{iso3}", "way in: {via}", "has {what} — no text found", and a "Showing only …"
whose bold phrase came from a four-branch conditional. Each was a sentence broken
around the data it carries, which is the shape that cannot be reordered.

Nine keys, and one of them does three jobs: the population line appears three
times in near-identical sibling spans, so it is ONE key used three times rather
than three keys — the duplication was in the template, not in the language.

The four filter descriptions are their own keys and slot into the sentence, so a
translator can move "Showing only" around the phrase rather than being handed two
halves to join in English order.

144 -> 124 findings for eleven keys. The ratio is better than the proving case
because the duplicates collapsed; it will worsen again on the one-off sentences.
2026-09-29 08:11:50 +00:00
Doxa Maps
c7aa670426 The gap sentence is one key, so a translator can move its clauses
charts.vue's hero read as six fragments concatenated around three counts and a
bdi island: "of", "languages have", "— the languages of", "people." Arabic does
not put those clauses in that order, so translating the fragments would have
given Arabic words in English order — grammatically broken, and worse than
leaving it English, because it looks translated and nobody re-checks it.

Now one message with named placeholders through useSentence. The counts and the
quoted phrase go in as VALUES, so the bold and the dir="ltr" survive without a
translator ever reproducing markup, and keep() hands the placeholders back to t()
intact — without it the lookup blanks them and the sentence renders with no
values at all.

Proven through a real createI18n rather than in isolation, which is the lesson
from the keep() trap: en renders as it does today, and ar and ru both LEAD with
the object and put the counts after — the reordering that fragment-by-fragment
translation cannot express. zh reorders too.

The gate moves 144 -> 142 for this one sentence. That is the honest number: six
fragments collapse into one key, but the gate counts findings, not fragments, so
the count falls more slowly than the work proceeds. ~26 fragments remain.
2026-09-29 08:11:50 +00:00
Doxa Maps
77503c07a0 Arabic readers get the language code, the link gap and the population column on the wrong side
Four rules in the resources list and tables are physical where the thing they
position follows READING ORDER, so they do not mirror under dir="rtl":

- .r-code sat at margin-left of the language name, landing before the name in Arabic
- .res a + a used margin-left for the gap that exists so "Holy Bible" and
  "JESUS Film" do not read as one string; mirrored, the gap moves outside the
  pair and the two links run together again
- .r-pop aligned right, which is the START of an Arabic row, not the end
- .gt-filter's accent bar marked the left edge rather than the leading edge

Now inline properties. The three border-left-color rules in resources-map.vue
are deliberately untouched: each sits on an element carrying dir="ltr", so its
edge is physical in every locale by design.
2026-09-28 23:43:44 +00:00
Doxa Maps
24b32e199f Release the test hooks on unmount, all twelve of them
Twelve globals were set across three plugins and none was ever deleted. Each
pins whatever it closes over — a map instance pins its canvas, its sources and
through them the whole app — so every mount left another copy behind, measured
elsewhere at roughly 10 MB each.

Released as a set rather than one at a time. A block that sets several globals
and clears the obvious one leaks the rest silently, and the obvious one is
whichever the author happened to remember.

Identity-checked, which is why the value is recorded as it is set rather than
cleared by name. Two instances can share a page, and clearing by name alone
would let the second unmount delete the first's live hooks — trading a leak for
a harder bug in which a live map loses its test surface at a distance.

A first attempt compared against a name that did not exist. Inside the try/catch
that guards these, the ReferenceError was swallowed and every delete silently did
nothing — a leak fix that looks exactly like a leak fix and leaks. The guard that
makes the assignment safe is the same guard that hides its failure, which is the
argument for recording what was set rather than inferring it at teardown.
2026-09-28 20:32:59 +00:00
Doxa Maps
042111bf3f Seat the legend caret in the title bar, and let the target keep a narrow screen
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Two things the Driver found, both about a control being in the reader's way
rather than about it not working.

The collapse caret was absolutely positioned over the panel's top-right, and the
title was hidden when collapsed — so collapsing left a lone glyph floating on the
map with nothing to say what it would reopen. It now sits inline in a header row
beside the title, which is where the shared legend puts its own: a flex item with
no positioning at all. The header stays visible when collapsed, so the control
always carries its label.

Side by side there is room for the target and its decoder. Stacked on a narrow
pane there is not: the decoder is a tall card that pushes the chart off the
screen, so a reader who asked to see the target got a glossary. Below the
breakpoint the legend now starts closed behind a named toggle. It is not hidden —
the control says what it opens, and opening is one tap.

The toggle reuses keys that already exist in every locale rather than adding a
word of its own. A new string would have meant eleven catalogue entries owned by
another tab and landing in another batch: a cross-owner dependency bought for one
label, when the fleet already had the sentence.
2026-09-28 19:13:38 +00:00
Doxa Maps
a45f1914a6 Let the legend collapse on desktop, the way it already does on mobile
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Mobile has had a fold control since the narrow layout existed; desktop had none,
so a reader who wanted to see the map underneath had no way to move the legend
off it. The state was already there — every section read "not narrow OR open", so
on desktop the OR was always satisfied and the flag governed nothing. Now the
flag alone decides at both widths.

The control is deliberately not a new one. Same 22x20 caret, same rotation, same
aria contract as the shared SemanticTreeLegend's collapse button, which the
mobile legend also copies. Matching it costs nothing and means a reader who has
met one of these legends has met this one.

Collapsed, the panel shrinks to the caret. Hiding the contents alone would have
left a 380px box of empty background over the map: the reader presses collapse
and gets a blank panel instead of their map, which is worse than having no
control at all.

The choice is remembered for the session, so it survives working with the map but
not a fresh visit, and a remembered choice beats the width default — otherwise
dragging a window across the breakpoint would silently reopen a legend that had
been deliberately closed, undoing a decision rather than responding to one.
Storage access is wrapped: a sandboxed embed throws on it, and a legend that
cannot remember is much better than a map that will not load.
2026-09-28 18:45:13 +00:00
Doxa Maps
51a3cadd62 Eleven entries called the helper without importing it
My scripted migration inserted the import behind a guard that silently did not fire:
`if 'defineMapElement' not in s.split('function')[0]` became false as soon as the
inserted CALL appeared before the first `function` keyword — so the files that most
needed the import were exactly the ones that did not get it. 11 of 16.

Every one of those elements would have thrown "defineMapElement is not defined" on
load. A bundler does not error on a free identifier; only loading does. studio caught it
in L64 via landing-preconditions, which imports each profile, and fixed it there
(37f75601) plus added an arm to check:element-bootstrap.

This repairs the same 11 on this branch so it is not a landmine if it lands after L64,
and it is byte-equivalent to studio's fix in effect. Verified by PARSING each entry and
resolving the identifier against its import bindings — 16 of 16 bound — rather than by
re-running the kind of regex that wrote the bug.
2026-09-28 17:42:31 +00:00
Doxa Maps
a25e0a6f99 Eleven plugin entries import the element helper they call
The element-bootstrap migration called defineMapElement in eleven entry files that never
imported it. A build does not fail on an unbound name; the element does, the moment it loads,
with 'defineMapElement is not defined'. The landing precondition caught it by importing each
profile. check:element-bootstrap now fails when an entry calls the helper without importing it.
2026-09-28 17:40:35 +00:00
Doxa Maps
5ab4af2e44 Five plugins were silently monolingual, because the bootstrap was copied sixteen times
Goal (c), and it turned into a goal (d) fix. Every profile element is
defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four
steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped.

ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from
profile-config. Without the provide the watcher holds null and the drive NEVER RUNS --
while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the
host. The element renders a convincing MIXTURE of the reader's language and English, and
nothing throws, because the t() shim falls back to its key. It reads as "a few strings
not translated yet".

SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and
my-upg-100-list. regions-plugin censused all 19 entry files independently and found the
same five and no sixth. The measurement is regions-dashboard's, from when they hit it:
a Spanish catalogue present, es set, and every t() string still English.

All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed
by construction rather than by five separate edits. Behaviour is otherwise identical --
same options, same element names, same per-plugin extras. wall-of-people-groups needed
the helper to hand its pinia back, because it takes a store off its own instance and a
helper that hid pinia would have given it a second one: two stores and a silent state
split.

★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function
BODIES; eleven sites are not `function buildXElement()` at all, they are
`const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in
the census as a caveat and it was the headline within the hour -- a 4x undercount of the
item it recommended. So this gate greps for the SHAPE, not for a known list: a check
written around the four that matched would have passed the twelve that did not.

check-element-bootstrap has two arms. No entry may build a profile element by hand, and
the helper itself must provide appI18n -- PLANTED as a negative case, so the regression
cannot return by editing library instead of a plugin. It also fails if it finds fewer
than ten entries, because a detection that stops matching reports the same green as a
tree with no problems.

THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate
matched the helper's DOCSTRING, so it would have passed a helper that only mentions the
provide in prose -- comments are stripped now, because a check that reads comments is
reading the author's intentions rather than the program. And the plant itself used /m
instead of /gm, so it deleted the documentation and left the code, then reported that it
could not detect an absence that was not there.
2026-09-28 17:40:34 +00:00
Doxa Maps
b64fea5fa4 Five plugins were silently monolingual, because the bootstrap was copied sixteen times
Goal (c), and it turned into a goal (d) fix. Every profile element is
defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four
steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped.

ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from
profile-config. Without the provide the watcher holds null and the drive NEVER RUNS --
while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the
host. The element renders a convincing MIXTURE of the reader's language and English, and
nothing throws, because the t() shim falls back to its key. It reads as "a few strings
not translated yet".

SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and
my-upg-100-list. regions-plugin censused all 19 entry files independently and found the
same five and no sixth. The measurement is regions-dashboard's, from when they hit it:
a Spanish catalogue present, es set, and every t() string still English.

All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed
by construction rather than by five separate edits. Behaviour is otherwise identical --
same options, same element names, same per-plugin extras. wall-of-people-groups needed
the helper to hand its pinia back, because it takes a store off its own instance and a
helper that hid pinia would have given it a second one: two stores and a silent state
split.

★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function
BODIES; eleven sites are not `function buildXElement()` at all, they are
`const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in
the census as a caveat and it was the headline within the hour -- a 4x undercount of the
item it recommended. So this gate greps for the SHAPE, not for a known list: a check
written around the four that matched would have passed the twelve that did not.

check-element-bootstrap has two arms. No entry may build a profile element by hand, and
the helper itself must provide appI18n -- PLANTED as a negative case, so the regression
cannot return by editing library instead of a plugin. It also fails if it finds fewer
than ten entries, because a detection that stops matching reports the same green as a
tree with no problems.

THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate
matched the helper's DOCSTRING, so it would have passed a helper that only mentions the
provide in prose -- comments are stripped now, because a check that reads comments is
reading the author's intentions rather than the program. And the plant itself used /m
instead of /gm, so it deleted the documentation and left the code, then reported that it
could not detect an absence that was not there.
2026-09-28 16:42:31 +00:00
Doxa Maps
439bea6754 Use the keys that landed for three strings the map still said in English
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-28 12:57:56 +00:00
Doxa Maps
20de4c457b Translate the three strings that were built from fragments or hardcoded
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.
2026-09-28 11:24:16 +00:00
Doxa Maps
33362f5592 Let the gospel map's own text follow the reader's direction
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Eight CSS declarations in the production profile still named a physical side, so in Arabic they
pinned translated text to the left: the legend key's wrapped label, the search-match rows, the
people-group buttons in the gap list and in the detail panel, the error line, two chip margins,
and the row-count badge beside each table tab. Every one of them now uses the logical property —
text-align:start, margin-inline-start — which is identical to today in every left-to-right locale
and correct in the two right-to-left ones.

Found by auditing the production profile's localization rather than by a rendering failure, which
is how this kind of defect stays in: the Arabic page looks translated, because it is, and only the
alignment quietly disagrees with it.

Nothing in the English-prose islands changed. Those carry dir="ltr" on purpose — the caveats stay
English by the standing ruling, and the isolation is what keeps them from disturbing the Arabic
text around them.

Rebuilt gospel-resources only, then fix:sri; check:sri passes at 64/64 leaves.
2026-09-28 09:22:02 +00:00
Doxa Maps
e893db3718 Name the three gospel profiles the Studio was calling by their ids
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
charts, data-tables and resources-list had no <profile>.meta.json at all, so the manifest carried
no label for them and the Studio rail humanized the folder name: "Charts", "Data Tables",
"Resources List". Those say what the file is called, not what the page answers.

Each now has a meta with a label and its stage:

  charts          Gospel Resources -- where the gap is, by language
  data-tables     Gospel Resources -- every resource and every gap, in tables
  resources-list  Gospel Resources -- what each people group has

All three declare "stage": "experimental" EXPLICITLY, which is what they already were. A meta-less
profile and a meta without a stage key both default to experimental in profile-meta.mjs, so the
value would otherwise rest on that default -- and these three sit in a bundle whose stage reads
production through gospel-dashboard, so a later change to the default would silently promote them.
Measured after the change: the three stay experimental, gospel-dashboard stays production, and
bundleStage still returns production.

Type is deliberately not declared. generate-tree resolves the composition tier from meta.json
`type`, not from whether the file exists, so omitting it leaves all three inferred `single`
exactly as before. Nothing in the manifest moves except the three labels and three stage values
that already held.
2026-09-28 07:54:11 +00:00
Doxa Maps
b3ade5c384 Neutral wording in a table strangers read
check:public-hygiene is the white-label gate for the giveaway, and translator-org-links.json
shipped an internal role noun: the held-Bible-Society reason read "...is the Driver's ruling, not
this script's". A recipient reading our data files should not meet our process vocabulary. It now
reads "a product ruling", which keeps the meaning exactly — the point was always that this build
will not decide a doctrine question on its own.

Fixed in the script that writes the table, not the artifact, or the next build would put it back;
the same phrase in that file's header comment went too, rather than waiting to be flagged
separately now studio has closed the comment half of this class.

Regenerated by re-running the verifier: 16 of 16 organisations answered again, so the table is
current rather than hand-edited. My published tables now carry no internal vocabulary — checked
with word boundaries across all of them, which is also how I learned the six "rega" hits reported
in my sweep were the word "aggregates".
2026-09-28 00:45:42 +00:00
Doxa Maps
c18d4ffb71 Drop eleven English fallbacks now their keys have landed
tOr() rendered English while a key was in flight. All eleven keys it guarded are on main, so it
is gone and every call site reads the catalogue: nParts, engineMissing, sheet.loading,
guide.wayInAll/wayInSome/wayInWhat, legend.hnrTip, guide.howNumbersMade, guide.isSignLanguage,
sheet.linkCopied, sheet.share.

I compared each default against the catalogue before deleting it, rather than assuming they
matched — deleting a fallback silently changes what a reader sees. Ten were identical. The
eleventh was not: gospel.sheet.share held my "Share this card" against the catalogue's "Share".
The catalogue wins, because it is translated in eleven locales where my phrase was not and a
button inside a card need not restate its container, so THE SHARE BUTTON NOW READS "Share" —
a visible change, deliberate, and the shorter label suits the 390px screen where those tap
targets were just fixed.

The four parameterised calls needed doing by hand: my regex for tOr(key, english, params)
stopped at the first ")" inside fmt(...), so it silently converted seven of eleven and left
four. Counted the remainder instead of trusting the count it printed.

gates:pre 11/11, ENGLISH-LEAKS PASS, and the built bundle no longer contains the inline English.
2026-09-27 20:33:29 +00:00
Doxa Maps
dd7dc0f39a Use the words that already existed in eleven locales
rega: the Scripture scope stayed English under ar, because gospel.res.newTestament and
gospel.res.fullBible exist in NO locale — while gospel.charts.newTestament and
gospel.charts.fullBible have existed, translated, in all eleven all along. Every locale was
falling back to my English defaults, and even en read my lowercase "full Bible" against the
catalogue's "Full Bible".

I asked studio for three new keys without checking whether the words were already owned. Two of
them were. The chip now reads the existing pair — verified in the built bundle: the Arabic
"العهد الجديد" and "الكتاب المقدس كاملاً" are both in it — and only the part count still needs a
key of its own, gospel.res.nParts with {n}, which I have narrowed studio's request to.

Two keys for one word is exactly the drift I have spent this flight fixing in other people's
code. The namespace reads oddly for a map chip, and that is the smaller problem.
2026-09-27 20:03:13 +00:00
Doxa Maps
918bc3a28e The scope is a number in the data and a word in the surface
dx-shepherd caught what I had just shipped: "full Bible", "New Testament" and "N parts" were
being generated in the BUILDER, so English was baked into data that no locale can translate and
no catalogue owns. The same mistake I have been fixing in other people's surfaces all day, made
one commit earlier by me.

The builder now ships FCBH's part count and nothing else — 27 on 400 codes, 66 on 41, another
count on 82, none on 367 — and the leaf names it through tOr(), so it localises the moment
studio adds three keys: gospel.res.newTestament, gospel.res.fullBible and gospel.res.nParts
with {n}. English inline until then, as always.

A count that is neither 27 nor 66 is still reported as itself and never rounded up to a
Testament, and no count still names only the source.
2026-09-27 19:50:08 +00:00