From ed87323592af21b9d632ce5d5f89b96017f6974d Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 01:00:04 +0000 Subject: [PATCH] Big Picture speaks Arabic where it was still speaking English: chrome and data labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rega's ?lang=ar pass (KA3, 3d64bcad) found English on a localized map in four places, and the census behind it explains why no gate saw them. Catalogue coverage is COMPLETE: of the 760 keys my bundles name across the 21 areas they reach, ZERO are missing from any of the ten non-English locales, and the per-locale English-identical values are numerals, acronyms and cognates rather than debt. Everything rega actually read was a string no catalogue ever held. · THE PROJECTION PILL said "🌐 Globe" / "▭ Flat" in all eleven locales. The icon is now the label. Neither word has a catalogue entry and authoring ten new strings for a two-state toggle to say what a glyph already says is the wrong trade — but mapui.enableGlobe / mapui.disableGlobe DO ship in all eleven, as the ACTION, which is what a button should announce. They become the accessible name and the tooltip. min-width joins min-height on the pill: the 24px used to come free from the text. · THE TABLE FOOTER said "Σ 2,107 groups". dashboard.groups has shipped in eleven locales all along (ar "مجموعة") and this one call site never read it. Written with the same singular/plural test as the header count so the two cannot disagree at one row. · THE REGION CHART labelled its bars with the prayer API's English region names. Seven regions is a closed set, so they are now CATALOGUED — five generated from vendored CLDR by internal/generate-wagf-region-names.mjs, which is the same data countryName.js already trusts. Generating is what keeps seventy translations out of my hands. middle_east and north_america_&_non-spanish_caribbean stay English: M49 145 is "Western Asia" and 021 is "Northern America", and neither means the region we draw. An English label a reader can match to the map beats a localized one that names the wrong territory. --check verifies the catalogue against CLDR. · TOP COUNTRIES labelled its bars with the API's English country names. By aggregation time the alpha-3 is gone, so countryName.js gains iso3ForEnglishName() — reversing its OWN table, exact where it matches and silent where it does not — which is precisely the ✓ case its header describes. One thing found while wiring those: the BAR label was the only chart surface still printing the raw facet key. The donut, its legend and both tooltips already went through legendLabel(); the bar list did not, which is why these labels stayed English after the donut was fixed for rega's earlier survey. Both new facets are projected there, so the two charts cannot disagree about a name again. English is left EXACTLY as authored. Both projections return the key untouched at locale 'en', which also keeps the 42 countries whose CLDR name differs from ours reading as they do today. Nothing localizes a VALUE: data-key, isActive() and every click and filter path still carry the canonical English, the rule the religion donut already followed. Verified: gates:pre 12/12, check:i18n-keys-exist / parity / never-translated PASS, and the generator's own --check PASS. Rendering on ?lang=ar goes to rega. --- internal/generate-wagf-region-names.mjs | 80 +++++++++++++++++++ library/i18n/locales/ar/dashboard.json | 9 ++- library/i18n/locales/de/dashboard.json | 9 ++- library/i18n/locales/en/dashboard.json | 9 ++- library/i18n/locales/es/dashboard.json | 9 ++- library/i18n/locales/fr/dashboard.json | 9 ++- library/i18n/locales/hi/dashboard.json | 9 ++- library/i18n/locales/it/dashboard.json | 9 ++- library/i18n/locales/pt/dashboard.json | 9 ++- library/i18n/locales/ro/dashboard.json | 9 ++- library/i18n/locales/ru/dashboard.json | 9 ++- library/i18n/locales/zh/dashboard.json | 9 ++- library/utils/countryName.js | 19 +++++ .../dashboards/DashboardChartsRail.vue | 48 ++++++++++- .../dashboards/DashboardDataTable.vue | 8 +- .../dashboards/DashboardMapPanel.vue | 27 ++++++- 16 files changed, 263 insertions(+), 18 deletions(-) create mode 100644 internal/generate-wagf-region-names.mjs diff --git a/internal/generate-wagf-region-names.mjs b/internal/generate-wagf-region-names.mjs new file mode 100644 index 000000000..1e977432e --- /dev/null +++ b/internal/generate-wagf-region-names.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/** + * generate-wagf-region-names.mjs — the WAGF region names, in the reader's language, from vendored CLDR. + * + * ★ THE DEFECT (rega KA3, 3d64bcad, ?lang=ar). Big Picture's region chart labels its bars with the + * prayer API's ENGLISH region names — "Africa", "Asia", "Latin America & Caribbean" — on an Arabic page. + * They are not chrome and no catalogue held them: they are DATA labels, so every i18n gate was green + * while the chart read English to every reader. The regions are a closed set of seven + * (library/colors/wagf-region.js WAGF_REGIONS), which is what makes cataloguing them possible at all. + * + * ★ WHY GENERATED AND NOT HAND-AUTHORED. Ten locales x seven names is seventy strings I cannot write. + * Five of the seven ARE standard UN M49 macro-regions, so CLDR already names them in every locale we + * ship, and Node's own ICU is the same data library/utils/countryName.js already trusts for countries. + * Generating is what keeps a translation out of my hands: the strings are copied from CLDR, committed, + * and reviewable, and re-running this reproduces them byte for byte. + * + * africa -> 002 · asia -> 142 · europe -> 150 · latin_america_&_caribbean -> 419 · oceania -> 009 + * + * ★ TWO REGIONS ARE DELIBERATELY NOT WRITTEN, because CLDR has no code that means them: + * · middle_east — M49 145 is "Western Asia", a different set (it includes the Caucasus and excludes + * Egypt). Naming it "Western Asia" in ten locales would silently redraw the region. + * · north_america_&_non-spanish_caribbean — M49 021 is "Northern America", which excludes the + * Caribbean this bucket exists to include. + * Both stay English until the Driver rules on a translation source. An English label a reader can + * still match to the map beats a localized label that names the wrong territory. + * + * ★ AND ONE SCOPE NOTE ON 419: CLDR's long name is "Latin America" while our label says + * "Latin America & Caribbean". The ENGLISH entry keeps our wording (it is the authored, authoritative + * one); the other ten take CLDR's, which omits the Caribbean from the NAME while the bucket still holds + * it. Recorded here rather than silently accepted — the alternative was English in ten locales. + * + * node internal/generate-wagf-region-names.mjs # write + * node internal/generate-wagf-region-names.mjs --check # verify the catalogue matches CLDR (CI) + */ +import { readFileSync, writeFileSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const CHECK = process.argv.includes('--check') +const M49 = { africa: '002', asia: '142', europe: '150', 'latin_america_&_caribbean': '419', oceania: '009' } +const HELD = ['middle_east', 'north_america_&_non-spanish_caribbean'] +const LOCALES = ['ar', 'de', 'en', 'es', 'fr', 'hi', 'it', 'pt', 'ro', 'ru', 'zh'] +// The English entries stay as the product authored them (see the 419 note above); every other locale +// is CLDR's. Read from the one file that already owns the region set, so a seventh region added there +// shows up here as a missing name rather than as a silent English label. +const { WAGF_REGIONS } = await import(join(ROOT, 'library/colors/wagf-region.js')) + +const names = (lc) => { + const dn = new Intl.DisplayNames([lc], { type: 'region' }) + const out = {} + for (const [slug, code] of Object.entries(M49)) out[slug] = lc === 'en' ? WAGF_REGIONS[slug] : dn.of(code) + return out +} +let wrote = 0 +const drift = [] +for (const lc of LOCALES) { + const p = join(ROOT, 'library/i18n/locales', lc, 'dashboard.json') + const cat = JSON.parse(readFileSync(p, 'utf8')) + const want = names(lc) + if (CHECK) { + const have = cat.wagfRegion || {} + for (const [k, v] of Object.entries(want)) if (have[k] !== v) drift.push(`${lc}.wagfRegion.${k}: catalogue ${JSON.stringify(have[k])} vs CLDR ${JSON.stringify(v)}`) + continue + } + cat.wagfRegion = want + writeFileSync(p, JSON.stringify(cat, null, 2) + '\n') + wrote++ +} +const unaccounted = Object.keys(WAGF_REGIONS).filter((s) => !M49[s] && !HELD.includes(s)) +if (unaccounted.length) { + console.error(`WAGF-REGION-NAMES: FAIL — ${unaccounted.length} region(s) in WAGF_REGIONS have neither a CLDR code nor a recorded reason: ${unaccounted.join(', ')}`) + process.exit(1) +} +if (CHECK) { + if (drift.length) { console.error(`WAGF-REGION-NAMES: FAIL — ${drift.length} catalogue entry(ies) no longer match CLDR:\n ${drift.join('\n ')}`); process.exit(1) } + console.log(`WAGF-REGION-NAMES: PASS — ${Object.keys(M49).length} name(s) x ${LOCALES.length} locale(s) match vendored CLDR; ${HELD.length} held in English on purpose (${HELD.join(', ')}).`) +} else { + console.log(`WAGF-REGION-NAMES: wrote ${Object.keys(M49).length} name(s) into ${wrote} locale catalogue(s); ${HELD.length} held in English (${HELD.join(', ')}).`) +} diff --git a/library/i18n/locales/ar/dashboard.json b/library/i18n/locales/ar/dashboard.json index 6342ab270..5a57fa3e4 100644 --- a/library/i18n/locales/ar/dashboard.json +++ b/library/i18n/locales/ar/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "المحدَّدة أولاً", "selected_first_hint": "رفع الشعوب المحدَّدة إلى الأعلى مع الإبقاء على ترتيب الأعمدة", "n_shown": "{n} معروض", - "other": "أخرى" + "other": "أخرى", + "wagfRegion": { + "africa": "أفريقيا", + "asia": "آسيا", + "europe": "أوروبا", + "latin_america_&_caribbean": "أمريكا اللاتينية", + "oceania": "أوقيانوسيا" + } } diff --git a/library/i18n/locales/de/dashboard.json b/library/i18n/locales/de/dashboard.json index f399cb726..92e270341 100644 --- a/library/i18n/locales/de/dashboard.json +++ b/library/i18n/locales/de/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "Ausgewählte zuerst", "selected_first_hint": "Die ausgewählten Volksgruppen nach oben holen, die Spaltensortierung bleibt erhalten", "n_shown": "{n} angezeigt", - "other": "Andere" + "other": "Andere", + "wagfRegion": { + "africa": "Afrika", + "asia": "Asien", + "europe": "Europa", + "latin_america_&_caribbean": "Lateinamerika", + "oceania": "Ozeanien" + } } diff --git a/library/i18n/locales/en/dashboard.json b/library/i18n/locales/en/dashboard.json index 3d80f9444..2cecac398 100644 --- a/library/i18n/locales/en/dashboard.json +++ b/library/i18n/locales/en/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "Selected first", "selected_first_hint": "Float the selected people groups to the top, keeping your column sort", "n_shown": "{n} shown", - "other": "Other" + "other": "Other", + "wagfRegion": { + "africa": "Africa", + "asia": "Asia", + "europe": "Europe", + "latin_america_&_caribbean": "Latin America & Caribbean", + "oceania": "Oceania" + } } diff --git a/library/i18n/locales/es/dashboard.json b/library/i18n/locales/es/dashboard.json index b4efad87c..4d831b0a7 100644 --- a/library/i18n/locales/es/dashboard.json +++ b/library/i18n/locales/es/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "Los seleccionados primero", "selected_first_hint": "Llevar arriba los grupos de personas seleccionados, manteniendo el orden de las columnas", "n_shown": "{n} mostrados", - "other": "Otros" + "other": "Otros", + "wagfRegion": { + "africa": "África", + "asia": "Asia", + "europe": "Europa", + "latin_america_&_caribbean": "Latinoamérica", + "oceania": "Oceanía" + } } diff --git a/library/i18n/locales/fr/dashboard.json b/library/i18n/locales/fr/dashboard.json index e2dcea6b6..c2625f4ac 100644 --- a/library/i18n/locales/fr/dashboard.json +++ b/library/i18n/locales/fr/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "La sélection en premier", "selected_first_hint": "Faire remonter les groupes de personnes sélectionnés, en conservant le tri des colonnes", "n_shown": "{n} affichés", - "other": "Autres" + "other": "Autres", + "wagfRegion": { + "africa": "Afrique", + "asia": "Asie", + "europe": "Europe", + "latin_america_&_caribbean": "Amérique latine", + "oceania": "Océanie" + } } diff --git a/library/i18n/locales/hi/dashboard.json b/library/i18n/locales/hi/dashboard.json index f97a187a3..e2327e28e 100644 --- a/library/i18n/locales/hi/dashboard.json +++ b/library/i18n/locales/hi/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "चयनित पहले", "selected_first_hint": "चयनित जनसमूहों को ऊपर लाएँ, आपका कॉलम क्रम बनाए रखते हुए", "n_shown": "{n} दिखाए गए", - "other": "अन्य" + "other": "अन्य", + "wagfRegion": { + "africa": "अफ़्रीका", + "asia": "एशिया", + "europe": "यूरोप", + "latin_america_&_caribbean": "लैटिन अमेरिका", + "oceania": "ओशिआनिया" + } } diff --git a/library/i18n/locales/it/dashboard.json b/library/i18n/locales/it/dashboard.json index 98b3b52b7..c254e5687 100644 --- a/library/i18n/locales/it/dashboard.json +++ b/library/i18n/locales/it/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "Prima i selezionati", "selected_first_hint": "Porta in alto i gruppi di persone selezionati, mantenendo l’ordinamento delle colonne", "n_shown": "{n} mostrati", - "other": "Altri" + "other": "Altri", + "wagfRegion": { + "africa": "Africa", + "asia": "Asia", + "europe": "Europa", + "latin_america_&_caribbean": "America Latina", + "oceania": "Oceania" + } } diff --git a/library/i18n/locales/pt/dashboard.json b/library/i18n/locales/pt/dashboard.json index 223f00f75..e26093727 100644 --- a/library/i18n/locales/pt/dashboard.json +++ b/library/i18n/locales/pt/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "Os selecionados primeiro", "selected_first_hint": "Trazer para cima os grupos de pessoas selecionados, mantendo a ordenação das colunas", "n_shown": "{n} apresentados", - "other": "Outros" + "other": "Outros", + "wagfRegion": { + "africa": "África", + "asia": "Ásia", + "europe": "Europa", + "latin_america_&_caribbean": "América Latina", + "oceania": "Oceania" + } } diff --git a/library/i18n/locales/ro/dashboard.json b/library/i18n/locales/ro/dashboard.json index 82a3cf0fe..8546c87bf 100644 --- a/library/i18n/locales/ro/dashboard.json +++ b/library/i18n/locales/ro/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "Cele selectate primele", "selected_first_hint": "Ridică grupurile de oameni selectate în partea de sus, păstrând sortarea coloanelor", "n_shown": "{n} afișate", - "other": "Altele" + "other": "Altele", + "wagfRegion": { + "africa": "Africa", + "asia": "Asia", + "europe": "Europa", + "latin_america_&_caribbean": "America Latină", + "oceania": "Oceania" + } } diff --git a/library/i18n/locales/ru/dashboard.json b/library/i18n/locales/ru/dashboard.json index 987a7ddd7..1aa887b05 100644 --- a/library/i18n/locales/ru/dashboard.json +++ b/library/i18n/locales/ru/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "Выбранные сначала", "selected_first_hint": "Поднять выбранные народы наверх, сохранив сортировку столбцов", "n_shown": "{n} показано", - "other": "Другое" + "other": "Другое", + "wagfRegion": { + "africa": "Африка", + "asia": "Азия", + "europe": "Европа", + "latin_america_&_caribbean": "Латинская Америка", + "oceania": "Океания" + } } diff --git a/library/i18n/locales/zh/dashboard.json b/library/i18n/locales/zh/dashboard.json index fd0c63e2b..6a0408dd9 100644 --- a/library/i18n/locales/zh/dashboard.json +++ b/library/i18n/locales/zh/dashboard.json @@ -78,5 +78,12 @@ "selected_first": "所选优先", "selected_first_hint": "将所选民族置顶,同时保留您的列排序", "n_shown": "显示 {n} 个", - "other": "其他" + "other": "其他", + "wagfRegion": { + "africa": "非洲", + "asia": "亚洲", + "europe": "欧洲", + "latin_america_&_caribbean": "拉丁美洲", + "oceania": "大洋洲" + } } diff --git a/library/utils/countryName.js b/library/utils/countryName.js index 16ac44a0f..e8f9b9cd8 100644 --- a/library/utils/countryName.js +++ b/library/utils/countryName.js @@ -42,6 +42,7 @@ import countries from '../data/countries.json' const ALPHA2 = table.map const ENGLISH = new Map(countries.map((c) => [c.value, c.label])) +const BY_ENGLISH = new Map(countries.map((c) => [String(c.label).trim().toLowerCase(), c.value])) /** The alpha-2 for an alpha-3 code, or undefined. Exported because callers building URLs and flag * assets need the same lookup, and a second copy of it would drift. */ @@ -49,6 +50,24 @@ export function alpha2Of(iso3) { return iso3 ? ALPHA2[String(iso3).toUpperCase()] : undefined } +/** + * The alpha-3 for a canonical ENGLISH country name, or undefined. + * + * ★ FOR LABELS THAT ARRIVE IN ENGLISH WITH NO CODE ATTACHED — the case the header's ✓ names. Big + * Picture's top-countries chart aggregates on the prayer API's English country LABEL, so by the time a + * bar is drawn the alpha-3 is gone and only "India" is in hand. Reversing our own countries.json is + * exact where it matches and silent where it does not, which is the only safe shape here: an unmatched + * name keeps rendering as itself rather than becoming a code or a blank. + * + * Built from the SAME table englishName() reads, so the two can never disagree about which name is + * canonical. Names are compared case-insensitively and trimmed; nothing else is normalized, because a + * looser match is how "Congo" would quietly become the wrong country. + */ +export function iso3ForEnglishName(name) { + if (!name) return undefined + return BY_ENGLISH.get(String(name).trim().toLowerCase()) +} + /** The canonical English name, or undefined. */ export function englishName(iso3) { return iso3 ? ENGLISH.get(String(iso3).toUpperCase()) : undefined diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue index ecf3d8932..25788be59 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue @@ -1,6 +1,8 @@