mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-22 23:05:31 -04:00
refactor: replace runtime Intl.DisplayNames with a static CLDR-generated country table
- Add `scripts/generate-countries.mjs` to enumerate all current ISO 3166-1 alpha-2 codes via CLDR and emit `src/lib/countries-data.ts`; retired, exceptionally-reserved, and CLDR-only pseudo codes are explicitly excluded, XK (Kosovo) is kept - Replace the live `Intl.DisplayNames` calls in `countries.ts` with a direct lookup against the generated `COUNTRY_NAMES` record, eliminating variance between Node/ICU versions - Build the reverse `nameToCode` map from `COUNTRY_NAMES` entries instead of a double-nested loop over the alphabet - Fix `countryCodeFromName` to fall through to `ALIASES` for two-letter inputs that aren't valid codes (e.g. `"UK"` → `"GB"`) - Add `myanmar` / `"myanmar burma"` aliases so the CLDR-style name and the legacy WHOIS variant both resolve to `MM` - Extend `countries.test.ts` to assert pinned names, XK presence, retired/reserved code exclusion, and `UK` alias behaviour
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// Regenerates src/lib/countries-data.ts from the local runtime's ICU (CLDR) region names.
|
||||
// Usage: node scripts/generate-countries.mjs
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const dn = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
// CLDR also knows reserved, retired and pseudo codes that aren't current ISO 3166-1 countries.
|
||||
// Keep only current ISO 3166-1 alpha-2 codes plus XK (Kosovo, the de-facto user-assigned code).
|
||||
const EXCLUDE = new Set([
|
||||
"AC",
|
||||
"CP",
|
||||
"DG",
|
||||
"EA",
|
||||
"EU",
|
||||
"EZ",
|
||||
"IC",
|
||||
"TA",
|
||||
"UN", // exceptionally reserved
|
||||
"AN",
|
||||
"BU",
|
||||
"CQ",
|
||||
"CS",
|
||||
"DD",
|
||||
"DY",
|
||||
"FX",
|
||||
"HV",
|
||||
"NH",
|
||||
"RH",
|
||||
"SU",
|
||||
"TP",
|
||||
"VD",
|
||||
"YD",
|
||||
"YU",
|
||||
"ZR", // retired
|
||||
"UK", // reserved for GB; WHOIS "UK" is handled as an alias in countries.ts
|
||||
"QO",
|
||||
"XA",
|
||||
"XB", // CLDR-only pseudo territories
|
||||
]);
|
||||
|
||||
const entries = [];
|
||||
for (let a = 65; a <= 90; a++) {
|
||||
for (let b = 65; b <= 90; b++) {
|
||||
const code = String.fromCharCode(a, b);
|
||||
if (EXCLUDE.has(code)) continue;
|
||||
let name;
|
||||
try {
|
||||
name = dn.of(code);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!name || name === code || name === "Unknown Region") continue;
|
||||
entries.push([code, name]);
|
||||
}
|
||||
}
|
||||
|
||||
const body = entries.map(([c, n]) => ` ${c}: ${JSON.stringify(n)},`).join("\n");
|
||||
writeFileSync(
|
||||
new URL("../src/lib/countries-data.ts", import.meta.url),
|
||||
`// Generated by scripts/generate-countries.mjs from CLDR (Unicode license). Do not edit by hand.\n` +
|
||||
`export const COUNTRY_NAMES: Record<string, string> = {\n${body}\n};\n`,
|
||||
);
|
||||
console.log(
|
||||
`Wrote ${entries.length} countries (Node ${process.version}, ICU ${process.versions.icu}, CLDR ${process.versions.cldr})`,
|
||||
);
|
||||
@@ -0,0 +1,253 @@
|
||||
// Generated by scripts/generate-countries.mjs from CLDR (Unicode license). Do not edit by hand.
|
||||
export const COUNTRY_NAMES: Record<string, string> = {
|
||||
AD: "Andorra",
|
||||
AE: "United Arab Emirates",
|
||||
AF: "Afghanistan",
|
||||
AG: "Antigua & Barbuda",
|
||||
AI: "Anguilla",
|
||||
AL: "Albania",
|
||||
AM: "Armenia",
|
||||
AO: "Angola",
|
||||
AQ: "Antarctica",
|
||||
AR: "Argentina",
|
||||
AS: "American Samoa",
|
||||
AT: "Austria",
|
||||
AU: "Australia",
|
||||
AW: "Aruba",
|
||||
AX: "Åland Islands",
|
||||
AZ: "Azerbaijan",
|
||||
BA: "Bosnia & Herzegovina",
|
||||
BB: "Barbados",
|
||||
BD: "Bangladesh",
|
||||
BE: "Belgium",
|
||||
BF: "Burkina Faso",
|
||||
BG: "Bulgaria",
|
||||
BH: "Bahrain",
|
||||
BI: "Burundi",
|
||||
BJ: "Benin",
|
||||
BL: "St. Barthélemy",
|
||||
BM: "Bermuda",
|
||||
BN: "Brunei",
|
||||
BO: "Bolivia",
|
||||
BQ: "Caribbean Netherlands",
|
||||
BR: "Brazil",
|
||||
BS: "Bahamas",
|
||||
BT: "Bhutan",
|
||||
BV: "Bouvet Island",
|
||||
BW: "Botswana",
|
||||
BY: "Belarus",
|
||||
BZ: "Belize",
|
||||
CA: "Canada",
|
||||
CC: "Cocos (Keeling) Islands",
|
||||
CD: "Congo - Kinshasa",
|
||||
CF: "Central African Republic",
|
||||
CG: "Congo - Brazzaville",
|
||||
CH: "Switzerland",
|
||||
CI: "Côte d’Ivoire",
|
||||
CK: "Cook Islands",
|
||||
CL: "Chile",
|
||||
CM: "Cameroon",
|
||||
CN: "China",
|
||||
CO: "Colombia",
|
||||
CR: "Costa Rica",
|
||||
CU: "Cuba",
|
||||
CV: "Cape Verde",
|
||||
CW: "Curaçao",
|
||||
CX: "Christmas Island",
|
||||
CY: "Cyprus",
|
||||
CZ: "Czechia",
|
||||
DE: "Germany",
|
||||
DJ: "Djibouti",
|
||||
DK: "Denmark",
|
||||
DM: "Dominica",
|
||||
DO: "Dominican Republic",
|
||||
DZ: "Algeria",
|
||||
EC: "Ecuador",
|
||||
EE: "Estonia",
|
||||
EG: "Egypt",
|
||||
EH: "Western Sahara",
|
||||
ER: "Eritrea",
|
||||
ES: "Spain",
|
||||
ET: "Ethiopia",
|
||||
FI: "Finland",
|
||||
FJ: "Fiji",
|
||||
FK: "Falkland Islands",
|
||||
FM: "Micronesia",
|
||||
FO: "Faroe Islands",
|
||||
FR: "France",
|
||||
GA: "Gabon",
|
||||
GB: "United Kingdom",
|
||||
GD: "Grenada",
|
||||
GE: "Georgia",
|
||||
GF: "French Guiana",
|
||||
GG: "Guernsey",
|
||||
GH: "Ghana",
|
||||
GI: "Gibraltar",
|
||||
GL: "Greenland",
|
||||
GM: "Gambia",
|
||||
GN: "Guinea",
|
||||
GP: "Guadeloupe",
|
||||
GQ: "Equatorial Guinea",
|
||||
GR: "Greece",
|
||||
GS: "South Georgia & South Sandwich Islands",
|
||||
GT: "Guatemala",
|
||||
GU: "Guam",
|
||||
GW: "Guinea-Bissau",
|
||||
GY: "Guyana",
|
||||
HK: "Hong Kong SAR China",
|
||||
HM: "Heard & McDonald Islands",
|
||||
HN: "Honduras",
|
||||
HR: "Croatia",
|
||||
HT: "Haiti",
|
||||
HU: "Hungary",
|
||||
ID: "Indonesia",
|
||||
IE: "Ireland",
|
||||
IL: "Israel",
|
||||
IM: "Isle of Man",
|
||||
IN: "India",
|
||||
IO: "British Indian Ocean Territory",
|
||||
IQ: "Iraq",
|
||||
IR: "Iran",
|
||||
IS: "Iceland",
|
||||
IT: "Italy",
|
||||
JE: "Jersey",
|
||||
JM: "Jamaica",
|
||||
JO: "Jordan",
|
||||
JP: "Japan",
|
||||
KE: "Kenya",
|
||||
KG: "Kyrgyzstan",
|
||||
KH: "Cambodia",
|
||||
KI: "Kiribati",
|
||||
KM: "Comoros",
|
||||
KN: "St. Kitts & Nevis",
|
||||
KP: "North Korea",
|
||||
KR: "South Korea",
|
||||
KW: "Kuwait",
|
||||
KY: "Cayman Islands",
|
||||
KZ: "Kazakhstan",
|
||||
LA: "Laos",
|
||||
LB: "Lebanon",
|
||||
LC: "St. Lucia",
|
||||
LI: "Liechtenstein",
|
||||
LK: "Sri Lanka",
|
||||
LR: "Liberia",
|
||||
LS: "Lesotho",
|
||||
LT: "Lithuania",
|
||||
LU: "Luxembourg",
|
||||
LV: "Latvia",
|
||||
LY: "Libya",
|
||||
MA: "Morocco",
|
||||
MC: "Monaco",
|
||||
MD: "Moldova",
|
||||
ME: "Montenegro",
|
||||
MF: "St. Martin",
|
||||
MG: "Madagascar",
|
||||
MH: "Marshall Islands",
|
||||
MK: "North Macedonia",
|
||||
ML: "Mali",
|
||||
MM: "Myanmar (Burma)",
|
||||
MN: "Mongolia",
|
||||
MO: "Macao SAR China",
|
||||
MP: "Northern Mariana Islands",
|
||||
MQ: "Martinique",
|
||||
MR: "Mauritania",
|
||||
MS: "Montserrat",
|
||||
MT: "Malta",
|
||||
MU: "Mauritius",
|
||||
MV: "Maldives",
|
||||
MW: "Malawi",
|
||||
MX: "Mexico",
|
||||
MY: "Malaysia",
|
||||
MZ: "Mozambique",
|
||||
NA: "Namibia",
|
||||
NC: "New Caledonia",
|
||||
NE: "Niger",
|
||||
NF: "Norfolk Island",
|
||||
NG: "Nigeria",
|
||||
NI: "Nicaragua",
|
||||
NL: "Netherlands",
|
||||
NO: "Norway",
|
||||
NP: "Nepal",
|
||||
NR: "Nauru",
|
||||
NU: "Niue",
|
||||
NZ: "New Zealand",
|
||||
OM: "Oman",
|
||||
PA: "Panama",
|
||||
PE: "Peru",
|
||||
PF: "French Polynesia",
|
||||
PG: "Papua New Guinea",
|
||||
PH: "Philippines",
|
||||
PK: "Pakistan",
|
||||
PL: "Poland",
|
||||
PM: "St. Pierre & Miquelon",
|
||||
PN: "Pitcairn Islands",
|
||||
PR: "Puerto Rico",
|
||||
PS: "Palestinian Territories",
|
||||
PT: "Portugal",
|
||||
PW: "Palau",
|
||||
PY: "Paraguay",
|
||||
QA: "Qatar",
|
||||
RE: "Réunion",
|
||||
RO: "Romania",
|
||||
RS: "Serbia",
|
||||
RU: "Russia",
|
||||
RW: "Rwanda",
|
||||
SA: "Saudi Arabia",
|
||||
SB: "Solomon Islands",
|
||||
SC: "Seychelles",
|
||||
SD: "Sudan",
|
||||
SE: "Sweden",
|
||||
SG: "Singapore",
|
||||
SH: "St. Helena",
|
||||
SI: "Slovenia",
|
||||
SJ: "Svalbard & Jan Mayen",
|
||||
SK: "Slovakia",
|
||||
SL: "Sierra Leone",
|
||||
SM: "San Marino",
|
||||
SN: "Senegal",
|
||||
SO: "Somalia",
|
||||
SR: "Suriname",
|
||||
SS: "South Sudan",
|
||||
ST: "São Tomé & Príncipe",
|
||||
SV: "El Salvador",
|
||||
SX: "Sint Maarten",
|
||||
SY: "Syria",
|
||||
SZ: "Eswatini",
|
||||
TC: "Turks & Caicos Islands",
|
||||
TD: "Chad",
|
||||
TF: "French Southern Territories",
|
||||
TG: "Togo",
|
||||
TH: "Thailand",
|
||||
TJ: "Tajikistan",
|
||||
TK: "Tokelau",
|
||||
TL: "Timor-Leste",
|
||||
TM: "Turkmenistan",
|
||||
TN: "Tunisia",
|
||||
TO: "Tonga",
|
||||
TR: "Türkiye",
|
||||
TT: "Trinidad & Tobago",
|
||||
TV: "Tuvalu",
|
||||
TW: "Taiwan",
|
||||
TZ: "Tanzania",
|
||||
UA: "Ukraine",
|
||||
UG: "Uganda",
|
||||
UM: "U.S. Outlying Islands",
|
||||
US: "United States",
|
||||
UY: "Uruguay",
|
||||
UZ: "Uzbekistan",
|
||||
VA: "Vatican City",
|
||||
VC: "St. Vincent & Grenadines",
|
||||
VE: "Venezuela",
|
||||
VG: "British Virgin Islands",
|
||||
VI: "U.S. Virgin Islands",
|
||||
VN: "Vietnam",
|
||||
VU: "Vanuatu",
|
||||
WF: "Wallis & Futuna",
|
||||
WS: "Samoa",
|
||||
XK: "Kosovo",
|
||||
YE: "Yemen",
|
||||
YT: "Mayotte",
|
||||
ZA: "South Africa",
|
||||
ZM: "Zambia",
|
||||
ZW: "Zimbabwe",
|
||||
};
|
||||
@@ -19,3 +19,12 @@ test("resolveCountry fills whichever side is missing", () => {
|
||||
countryCode: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("static table: pinned names, XK kept, retired/reserved codes excluded, UK is GB", () => {
|
||||
expect(countryNameFromCode("XK")).toBe("Kosovo");
|
||||
expect(countryNameFromCode("SU")).toBeUndefined();
|
||||
expect(countryNameFromCode("UK")).toBeUndefined();
|
||||
expect(countryCodeFromName("UK")).toBe("GB");
|
||||
expect(countryCodeFromName("Myanmar")).toBe("MM");
|
||||
expect(resolveCountry("UK", undefined)).toEqual({ country: "United Kingdom", countryCode: "GB" });
|
||||
});
|
||||
|
||||
+12
-29
@@ -1,4 +1,6 @@
|
||||
// Country name <-> ISO 3166-1 alpha-2 resolution, backed by the runtime's ICU data.
|
||||
// Country name <-> ISO 3166-1 alpha-2 resolution, backed by a static table (see
|
||||
// scripts/generate-countries.mjs) so results don't vary with the runtime's ICU version.
|
||||
import { COUNTRY_NAMES } from "./countries-data";
|
||||
|
||||
const ALIASES: Record<string, string> = {
|
||||
usa: "US",
|
||||
@@ -32,6 +34,8 @@ const ALIASES: Record<string, string> = {
|
||||
holland: "NL",
|
||||
uae: "AE",
|
||||
burma: "MM",
|
||||
myanmar: "MM",
|
||||
"myanmar burma": "MM",
|
||||
"ivory coast": "CI",
|
||||
"cote d'ivoire": "CI",
|
||||
laos: "LA",
|
||||
@@ -51,45 +55,24 @@ const normalize = (s: string) =>
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
let displayNames: Intl.DisplayNames | undefined;
|
||||
let nameToCode: Map<string, string> | undefined;
|
||||
|
||||
function getDisplayNames(): Intl.DisplayNames | undefined {
|
||||
if (displayNames) return displayNames;
|
||||
try {
|
||||
displayNames = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
} catch {
|
||||
// Runtime without Intl.DisplayNames support
|
||||
}
|
||||
return displayNames;
|
||||
}
|
||||
|
||||
/** English country name for an ISO 3166-1 alpha-2 code, or undefined if unknown. */
|
||||
export function countryNameFromCode(code: string): string | undefined {
|
||||
const c = code.trim().toUpperCase();
|
||||
if (!/^[A-Z]{2}$/.test(c)) return undefined;
|
||||
try {
|
||||
const name = getDisplayNames()?.of(c);
|
||||
return name && name !== c && name !== "Unknown Region" ? name : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return COUNTRY_NAMES[code.trim().toUpperCase()];
|
||||
}
|
||||
|
||||
/** ISO 3166-1 alpha-2 code for a country name (or the code itself), or undefined if unknown. */
|
||||
export function countryCodeFromName(value: string): string | undefined {
|
||||
const v = value.trim();
|
||||
if (!v) return undefined;
|
||||
if (/^[A-Za-z]{2}$/.test(v)) return countryNameFromCode(v) ? v.toUpperCase() : undefined;
|
||||
if (/^[A-Za-z]{2}$/.test(v)) {
|
||||
return countryNameFromCode(v) ? v.toUpperCase() : ALIASES[v.toLowerCase()];
|
||||
}
|
||||
if (!nameToCode) {
|
||||
nameToCode = new Map();
|
||||
for (let a = 65; a <= 90; a++) {
|
||||
for (let b = 65; b <= 90; b++) {
|
||||
const code = String.fromCharCode(a, b);
|
||||
const name = countryNameFromCode(code);
|
||||
if (name) nameToCode.set(normalize(name), code);
|
||||
}
|
||||
}
|
||||
nameToCode = new Map(
|
||||
Object.entries(COUNTRY_NAMES).map(([code, name]) => [normalize(name), code]),
|
||||
);
|
||||
}
|
||||
const key = normalize(v);
|
||||
return nameToCode.get(key) ?? ALIASES[key];
|
||||
|
||||
Reference in New Issue
Block a user