100 lines
4.7 KiB
JavaScript
100 lines
4.7 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]))
|
|
|
|
/** 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 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
|