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 @@