mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-23 00:15: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,
|
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> = {
|
const ALIASES: Record<string, string> = {
|
||||||
usa: "US",
|
usa: "US",
|
||||||
@@ -32,6 +34,8 @@ const ALIASES: Record<string, string> = {
|
|||||||
holland: "NL",
|
holland: "NL",
|
||||||
uae: "AE",
|
uae: "AE",
|
||||||
burma: "MM",
|
burma: "MM",
|
||||||
|
myanmar: "MM",
|
||||||
|
"myanmar burma": "MM",
|
||||||
"ivory coast": "CI",
|
"ivory coast": "CI",
|
||||||
"cote d'ivoire": "CI",
|
"cote d'ivoire": "CI",
|
||||||
laos: "LA",
|
laos: "LA",
|
||||||
@@ -51,45 +55,24 @@ const normalize = (s: string) =>
|
|||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
let displayNames: Intl.DisplayNames | undefined;
|
|
||||||
let nameToCode: Map<string, string> | 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. */
|
/** English country name for an ISO 3166-1 alpha-2 code, or undefined if unknown. */
|
||||||
export function countryNameFromCode(code: string): string | undefined {
|
export function countryNameFromCode(code: string): string | undefined {
|
||||||
const c = code.trim().toUpperCase();
|
return COUNTRY_NAMES[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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ISO 3166-1 alpha-2 code for a country name (or the code itself), or undefined if unknown. */
|
/** 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 {
|
export function countryCodeFromName(value: string): string | undefined {
|
||||||
const v = value.trim();
|
const v = value.trim();
|
||||||
if (!v) return undefined;
|
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) {
|
if (!nameToCode) {
|
||||||
nameToCode = new Map();
|
nameToCode = new Map(
|
||||||
for (let a = 65; a <= 90; a++) {
|
Object.entries(COUNTRY_NAMES).map(([code, name]) => [normalize(name), code]),
|
||||||
for (let b = 65; b <= 90; b++) {
|
);
|
||||||
const code = String.fromCharCode(a, b);
|
|
||||||
const name = countryNameFromCode(code);
|
|
||||||
if (name) nameToCode.set(normalize(name), code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const key = normalize(v);
|
const key = normalize(v);
|
||||||
return nameToCode.get(key) ?? ALIASES[key];
|
return nameToCode.get(key) ?? ALIASES[key];
|
||||||
|
|||||||
Reference in New Issue
Block a user