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.
119 lines
5.8 KiB
JavaScript
119 lines
5.8 KiB
JavaScript
/**
|
|
* countryName.js — a country's name in the reader's language.
|
|
*
|
|
* Our data speaks ISO 3166-1 ALPHA-3 ("DEU"). Intl.DisplayNames('region') accepts alpha-2 or UN M49
|
|
* and NOT alpha-3, so every country name was stuck in English until the two were joined. The join is
|
|
* library/data/iso3-to-alpha2.json, derived from vendored CLDR by generate-iso3-to-alpha2.mjs.
|
|
*
|
|
* ★ Intl.DisplayNames THROWS on a missing code. `dn.of(undefined)` is a RangeError, not a null return,
|
|
* so every path here checks the alpha-2 before asking. That is the single defect this file exists to
|
|
* not have: a country absent from the map would otherwise take down whatever rendered it.
|
|
*
|
|
* ★ AND IT THROWS ON A MALFORMED ONE TOO, which is why the regex guard is not redundant with the
|
|
* lookup. dn.of('XYZ') is also a RangeError -- 'structurally invalid' is a different failure from
|
|
* 'not found', and only the second returns the input unchanged.
|
|
*
|
|
* FALLBACK ORDER, each step a real answer rather than a placeholder:
|
|
* 1. the localized name from Intl.DisplayNames
|
|
* 2. the canonical ENGLISH name from library/data/countries.json — a real name in the wrong language
|
|
* beats a bare code for a reader, and it is what our own tables already show
|
|
* 3. the alpha-3 code itself, so something identifiable always renders
|
|
*
|
|
* No i18n framework is required: this reads a locale STRING the caller already has.
|
|
*
|
|
* ★★ WHERE NOT TO USE THIS, MEASURED. It has NO consumer yet on purpose. Intl's English names
|
|
* differ from the canonical names in library/data/countries.json for 42 OF 242 COUNTRIES -- "Antigua &
|
|
* Barbuda" vs "Antigua and Barbuda", "Cape Verde" vs "Cabo Verde", "Hong Kong SAR China" vs "Hong
|
|
* Kong", "Myanmar (Burma)" vs "Myanmar", "Caribbean Netherlands" vs "Bonaire, Sint Eustatius, and
|
|
* Saba". So this is NOT a drop-in replacement for a name we already display:
|
|
*
|
|
* ✗ do NOT swap it in where an API already returns a LOCALIZED label. region-dashboards.vue:370
|
|
* prefers the API's label deliberately, and the regions surfaces are measured against live
|
|
* doxa.life -- switching the source would change 42 English names and break clone parity, which
|
|
* is a product decision about which name is canonical, not a refactor.
|
|
* ✓ DO use it where the only thing in hand is an alpha-3 and there is no label at all, or where
|
|
* a label arrives in English regardless of the reader's locale. That is the gap it was built for.
|
|
*
|
|
* Stated here rather than left to be discovered, because the failure mode is silent: the names all
|
|
* still look like names.
|
|
*/
|
|
import table from '../data/iso3-to-alpha2.json'
|
|
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. */
|
|
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
|
|
}
|
|
|
|
// One DisplayNames per locale. Constructing it is not cheap and a country list rebuilds it per row;
|
|
// a Map keyed by locale is the whole optimisation. A locale that throws is cached as null so the
|
|
// failure costs one construction, not one per call.
|
|
const cache = new Map()
|
|
function displayNames(locale) {
|
|
const key = locale || 'en'
|
|
if (cache.has(key)) return cache.get(key)
|
|
let dn = null
|
|
try {
|
|
dn = new Intl.DisplayNames([key], { type: 'region' })
|
|
} catch (_) {
|
|
dn = null // an unsupported locale tag, or no Intl.DisplayNames at all
|
|
}
|
|
cache.set(key, dn)
|
|
return dn
|
|
}
|
|
|
|
/**
|
|
* @param {string} iso3 ISO 3166-1 alpha-3, e.g. 'DEU'. Case-insensitive.
|
|
* @param {string} [locale] BCP-47 tag, e.g. 'ar'. Defaults to English.
|
|
* @returns {string} the country's name in that locale, its English name, or the code.
|
|
*/
|
|
export function countryName(iso3, locale) {
|
|
const code = iso3 ? String(iso3).toUpperCase() : ''
|
|
const a2 = ALPHA2[code]
|
|
const fallback = ENGLISH.get(code) || code
|
|
|
|
// Guard BEFORE asking: dn.of(undefined) and dn.of('XYZ') both throw.
|
|
if (!a2 || !/^[A-Z]{2}$/.test(a2)) return fallback
|
|
|
|
const dn = displayNames(locale)
|
|
if (!dn) return fallback
|
|
try {
|
|
const name = dn.of(a2)
|
|
// An UNKNOWN-but-well-formed region comes back as the input string. That is not a name, so it
|
|
// must not win over our English label -- this is the difference between "no translation exists"
|
|
// and "the translation is literally 'DE'".
|
|
return name && name !== a2 ? name : fallback
|
|
} catch (_) {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
export default countryName
|