Files
itflow/libs/intl-tel-input/js/data/country-data.ts
2026-08-26 13:14:32 -04:00

169 lines
5.9 KiB
TypeScript

import allCountries, { type Country, type Iso2 } from "../data.js";
import type { AllOptions } from "../types/public-api.js";
export interface ProcessedDialCodes {
dialCodes: Set<string>;
dialCodeMaxLength: number;
dialCodeToIso2Map: Record<string, Iso2[]>;
}
//* Process onlyCountries or excludeCountries array if present.
export const processAllCountries = (options: AllOptions): Country[] => {
const { onlyCountries, excludeCountries } = options;
if (onlyCountries?.length) {
return allCountries.filter((country) =>
onlyCountries.includes(country.iso2),
);
} else if (excludeCountries?.length) {
return allCountries.filter(
(country) => !excludeCountries.includes(country.iso2),
);
}
//* Create new array so avoid a bug where multiple instances with different countryOrder options interfere with each other (as they were sharing the same allCountries array, which is mutated by sortCountries).
return [...allCountries];
};
//* Generate country names using Intl.DisplayNames
export const generateCountryNames = (
countries: Country[],
options: AllOptions,
): void => {
const { countryNameLocale, countryNameOverrides, uiTranslations } = options;
//* Some locales (e.g. bs, hy, is, mk, sq, uz) bundle their own country names
//* because certain browsers' Intl.DisplayNames lacks region data for them and
//* silently falls back to English (notably Chrome desktop). When the active
//* uiTranslations carries these, they take precedence over Intl.DisplayNames.
const bundledCountryNames = uiTranslations?.countryNames;
//* Populate country names using Intl.DisplayNames (per instance) with countryNameLocale.
let displayNames;
try {
const hasDisplayNames =
typeof Intl !== "undefined" &&
typeof (Intl as any).DisplayNames === "function";
if (hasDisplayNames) {
displayNames = new (Intl as any).DisplayNames(countryNameLocale, {
type: "region",
});
} else {
displayNames = null;
}
} catch (e) {
console.error(e);
displayNames = null;
}
for (const c of countries) {
c.name =
countryNameOverrides[c.iso2] ||
bundledCountryNames?.[c.iso2] ||
displayNames?.of(c.iso2.toUpperCase()) ||
"";
}
};
//* Generate dialCodes and dialCodeToIso2Map.
export const processDialCodes = (
countries: Country[],
): ProcessedDialCodes => {
//* Here we store just dial codes, where the key is the dial code, and the value is true
//* e.g. { 1: true, 7: true, 20: true, ... }.
const dialCodes = new Set<string>();
let dialCodeMaxLength = 0;
//* Here we map dialCodes (inc both dialCode and dialCode+areaCode) to iso2 codes e.g.
/*
* {
* 1: [ 'us', 'ca', ... ], # all NANP countries (with dial code "1")
* 12: [ 'us', 'ca', ... ], # subset of NANP countries (that have area codes starting with "2")
* 120: [ 'us', 'ca' ], # just US and Canada (that have area codes starting "20")
* 1204: [ 'ca' ], # only Canada (that has a "204" area code)
* ...
* }
*/
const dialCodeToIso2Map: Record<string, Iso2[]> = {};
//* Add a dial code to this.dialCodeToIso2Map.
const addToDialCodeMap = (iso2: Iso2, dialCode: string) => {
// Bail if no iso2 or dialCode (this can happen with onlyCountries or excludeCountries options).
if (!iso2 || !dialCode) {
return;
}
//* Update dialCodeMaxLength.
if (dialCode.length > dialCodeMaxLength) {
dialCodeMaxLength = dialCode.length;
}
//* If this entry doesn't already exist, then create it.
if (!Object.hasOwn(dialCodeToIso2Map, dialCode)) {
dialCodeToIso2Map[dialCode] = [];
}
const iso2List = dialCodeToIso2Map[dialCode];
//* Bail if we already have this country for this dialCode.
if (iso2List.includes(iso2)) {
return;
}
iso2List.push(iso2);
};
// Sort countries by priority so that when we add to the dialCodeToIso2Map, higher priority countries come first
const countriesSortedByPriority = [...countries].sort(
(a, b) => a.priority - b.priority,
);
for (const c of countriesSortedByPriority) {
if (!dialCodes.has(c.dialCode)) {
dialCodes.add(c.dialCode);
}
// add the dial code partial matches to the map
for (let k = 1; k < c.dialCode.length; k++) {
const partialDialCode = c.dialCode.substring(0, k);
addToDialCodeMap(c.iso2, partialDialCode);
}
// add the full dial code to the map
addToDialCodeMap(c.iso2, c.dialCode);
if (c.areaCodes) {
const rootIso2Code = dialCodeToIso2Map[c.dialCode][0];
//* For each area code.
for (const areaCode of c.areaCodes) {
//* Add partial matches: For each digit in the area code
for (let k = 1; k < areaCode.length; k++) {
const partialAreaCode = areaCode.substring(0, k);
const partialDialCode = c.dialCode + partialAreaCode;
//* Start with the root country, as that also matches this partial dial code.
addToDialCodeMap(rootIso2Code, partialDialCode);
addToDialCodeMap(c.iso2, partialDialCode);
}
//* Add the full area code.
addToDialCodeMap(c.iso2, c.dialCode + areaCode);
}
}
}
return { dialCodes, dialCodeMaxLength, dialCodeToIso2Map };
};
//* Sort countries by countryOrder option (if present), then name.
export const sortCountries = (
countries: Country[],
options: AllOptions,
): void => {
const { countryOrder } = options;
countries.sort((a: Country, b: Country): number => {
//* Primary sort: countryOrder option
if (countryOrder) {
const aIndex = countryOrder.indexOf(a.iso2);
const bIndex = countryOrder.indexOf(b.iso2);
const aIndexExists = aIndex > -1;
const bIndexExists = bIndex > -1;
if (aIndexExists || bIndexExists) {
if (aIndexExists && bIndexExists) {
return aIndex - bIndex;
}
return aIndexExists ? -1 : 1;
}
}
//* Secondary sort: country name
return a.name.localeCompare(b.name);
});
};