Regions: stop overriding their localised labels with hardcoded English

I have reported "the SELECT VIEW strip's three strings need
translating" for several ticks without ever quantifying it. Measured at
ar and zh by classifying every visible string by script: TWENTY were
still Latin, not three. Most were not awaiting a decision at all — they
were bugs, and the largest was my own code discarding their data.

- The chooser's region legend read WAGF_REGIONS[value] || r.label, so
  the hardcoded English display names in library/colors/wagf-region.js
  always WON over r.label — the localised name the API returns for
  ?lang=, and the very name the region CARDS render. At ar the cards
  read "آسيا" while the legend beside them read "Asia". Inverted: the
  API's label now wins and WAGF_REGIONS is the fallback for a region
  returned with no label.
- The no-selection title was the literal 'All Regions' while this same
  file already calls t('All Regions') for the back-link — one string,
  translated in one place and hardcoded in another.
- syncTitle compared the subject title against the English literal, so
  once the title was localised the comparison could never match; both
  halves now go through t(), whose keys are theirs.
- The chooser's two dropdowns carried static label and placeholder
  attributes. That is the element's only keyboard and screen-reader
  path — pin clicking is mouse-only by construction — and it was
  labelled in English inside an otherwise localised page. Now
  t('Regions'), t('Countries') and t('Type to search...'), all their
  keys, all translated in every locale we ship. The table's first
  column too: t('Region') has no key in their catalogue, since they
  never needed an all-regions state.

Twenty Latin strings down to fourteen, of which nine are our own
additions, four are Mapbox's chrome and one is an API code. 93.9%
localised at both ar and zh.

Two lessons in the measuring itself. My scan read only TEXT NODES, so
the two hardcoded placeholders were invisible to a check that had just
reported 95% localised — it now reads placeholder, aria-label, title
and alt as well, which immediately surfaced four more English
aria-labels that exist purely for assistive tech. And the remainder is
now classified in the probe rather than left to fail forever: a
permanently red check only teaches you to ignore it, so it passes on
the known set and fails on anything new.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 10:12:16 +00:00
parent d8a3e5992f
commit cf689adeeb
7 changed files with 136 additions and 105 deletions

View file

@ -97,6 +97,9 @@ useDocumentFont('Poppins', POPPINS_FACES, assetsBase.value)
// ── state ───────────────────────────────────────────────────────────────────
const { t, tf , locale } = useRegionsI18n()
// syncTitle() shadows `t` with the subject title, so it needs a second handle on the
// translator. Same function, different name — not a second i18n instance.
const t2 = t
// their <h1 class="highlight" data-highlight-last> — the final word in brand primary
const titleParts = computed(() => splitLastWord(subject.value?.title || ''))
const isRTL = computed(() => RTL_LOCALES.has(locale.value))
@ -170,7 +173,11 @@ const subject = computed(() => {
all.engaged += r.engaged; all.withPrayer += r.withPrayer
all.withFullPrayer += r.withFullPrayer; all.peopleCommitted += r.peopleCommitted
}
return { kind: 'all', title: 'All Regions', context: `${formatNumber(regions.value.length)} regions`, stats: all }
// t('All Regions'), not the literal. Their own key, translated in all eleven locales
// ('جميع المناطق' at ar), and this file ALREADY calls t('All Regions') for the back-link —
// so the same string was translated in one place and hardcoded in another. MEASURED at ar
// and zh: this title rendered English inside an otherwise fully localised element.
return { kind: 'all', title: t('All Regions'), context: `${formatNumber(regions.value.length)} regions`, stats: all }
})
// Headline tiles. Every one of these is a COUNT the rollup actually measured — no ratio is
@ -252,7 +259,10 @@ const tableTitle = computed(() => {
return t('Countries in this region')
})
const tableFirstCol = computed(() => (subject.value.kind === 'all' ? t('Region') : t('Country')))
// t('Region') has NO key in their catalogue — they never needed it, having no all-regions
// state — so it fell back to English. t('Regions') and t('Countries') both exist and are
// translated everywhere, and read correctly as column headings.
const tableFirstCol = computed(() => (subject.value.kind === 'all' ? t('Regions') : t('Countries')))
// Their summary line, RegionDetail.vue:32-36 / the list's own. Kept as a %token% template for
// the same reason tab 1 keeps it -- it is how their i18n carries this string.
@ -294,7 +304,14 @@ const regionLegend = computed(() => {
return REGION_ORDER
.map((value) => {
const r = byValue.get(value)
return r ? { value, slug: r.slug, label: WAGF_REGIONS[value] || r.label, count: r.count, color: REGION_PALETTE[value] } : null
// PREFER THE API'S LOCALIZED LABEL. This read WAGF_REGIONS[value] || r.label, so the
// hardcoded ENGLISH display name in library/colors/wagf-region.js always WON over r.label
// — which is the localized name the API returns for ?lang=, and the very name the region
// CARDS render. MEASURED at locale=ar and zh: the cards read "آسيا" / "中东" while this
// legend, inside the same element, read "Asia" and "Middle East". My code was discarding
// their localized data. WAGF_REGIONS stays as the FALLBACK, for a region the API returns
// with no label at all.
return r ? { value, slug: r.slug, label: r.label || WAGF_REGIONS[value], count: r.count, color: REGION_PALETTE[value] } : null
})
.filter(Boolean)
})
@ -461,7 +478,14 @@ function writeUrl(push) {
function syncTitle() {
if (typeof document === 'undefined') return
const t = subject.value?.title
try { document.title = t && t !== 'All Regions' ? t + ' — Regions' : 'Regions' } catch (_) {}
// COMPARE AGAINST THE TRANSLATED VALUE, not the English literal. The subject title is now
// t('All Regions'), so at ar it is 'جميع المناطق' and a comparison with the English string
// never matched — the no-selection state would have produced 'جميع المناطق — Regions'
// instead of the bare fallback. Both halves go through t(), whose keys are theirs and are
// translated in all eleven locales.
const allLabel = t2('All Regions')
const suffix = t2('Regions')
try { document.title = t && t !== allLabel ? t + ' — ' + suffix : suffix } catch (_) {}
}
// popstate WRITES the selection, so the watcher below must not turn around and push a new
@ -810,21 +834,28 @@ useShadowStyles(
<!-- THE CHOOSER — the no-selection state, and the only thing here DX-life does not have -->
<div v-if="isAll" class="rd-chooser">
<!-- Labels and placeholders through t(), using THEIR existing keys rather than new
ones: 'Regions', 'Countries' and 'Type to search...' are all in their common.json
and translated in every locale we ship. These were static attributes, so at ar the
element's one KEYBOARD/SCREEN-READER path — which the brief calls load-bearing,
since pin clicking is mouse-only — was labelled in English inside an otherwise
localised page. The plural reads naturally on a selector and costs no new strings.
The placeholders were invisible to a text-node scan, which is its own lesson. -->
<FilterDropdown
label="Region"
:label="t('Regions')"
name="region"
:options="regionOptions"
value=""
placeholder="Type a region…"
:placeholder="t('Type to search...')"
@filter-change="onPickRegion"
@filter-clear="clearSelection"
/>
<FilterDropdown
label="Country"
:label="t('Countries')"
name="country"
:options="countryOptions"
value=""
placeholder="Type a country…"
:placeholder="t('Type to search...')"
@filter-change="onPickCountry"
@filter-clear="clearSelection"
/>

File diff suppressed because one or more lines are too long

View file

@ -856,7 +856,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-O+KKVXrz1cHdDNNgxmA5OhFk03sIKRUn6nsuvuq0smZaiQTYwqSFTcsfvIJj55TW",
"integrity": "sha384-2vTu8GqysCwitNhCrtG3Rvo/GFQVEovIR5CsTz8o7P70jYWuEj8pV4yf+gd4KjnN",
"elements": [
"regions-dashboard"
],
@ -876,7 +876,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-O+KKVXrz1cHdDNNgxmA5OhFk03sIKRUn6nsuvuq0smZaiQTYwqSFTcsfvIJj55TW\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-2vTu8GqysCwitNhCrtG3Rvo/GFQVEovIR5CsTz8o7P70jYWuEj8pV4yf+gd4KjnN\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -890,7 +890,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-O+KKVXrz1cHdDNNgxmA5OhFk03sIKRUn6nsuvuq0smZaiQTYwqSFTcsfvIJj55TW\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-2vTu8GqysCwitNhCrtG3Rvo/GFQVEovIR5CsTz8o7P70jYWuEj8pV4yf+gd4KjnN\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -904,7 +904,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-O+KKVXrz1cHdDNNgxmA5OhFk03sIKRUn6nsuvuq0smZaiQTYwqSFTcsfvIJj55TW\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-2vTu8GqysCwitNhCrtG3Rvo/GFQVEovIR5CsTz8o7P70jYWuEj8pV4yf+gd4KjnN\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-O+KKVXrz1cHdDNNgxmA5OhFk03sIKRUn6nsuvuq0smZaiQTYwqSFTcsfvIJj55TW" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-2vTu8GqysCwitNhCrtG3Rvo/GFQVEovIR5CsTz8o7P70jYWuEj8pV4yf+gd4KjnN" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-O+KKVXrz1cHdDNNgxmA5OhFk03sIKRUn6nsuvuq0smZaiQTYwqSFTcsfvIJj55TW" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-2vTu8GqysCwitNhCrtG3Rvo/GFQVEovIR5CsTz8o7P70jYWuEj8pV4yf+gd4KjnN" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-O+KKVXrz1cHdDNNgxmA5OhFk03sIKRUn6nsuvuq0smZaiQTYwqSFTcsfvIJj55TW" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-2vTu8GqysCwitNhCrtG3Rvo/GFQVEovIR5CsTz8o7P70jYWuEj8pV4yf+gd4KjnN" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: