import { getCountries, getCountryCallingCode, type CountryCode } from 'libphonenumber-js/mobile/es6' export interface CountryPhoneOption { value: string label: string countryCode: string countryName: string } /** Module-level cache: country catalog built once per display locale. */ const countryOptionsCache = new Map() const displayNamesCache = new Map() function resolveDisplayLocale(locale: string): string { const trimmed = locale.trim() if (!trimmed) { return 'sv-SE' } return trimmed } function createCountryDisplayNames(locale: string): Intl.DisplayNames | undefined { const cacheKey = resolveDisplayLocale(locale) if (displayNamesCache.has(cacheKey)) { return displayNamesCache.get(cacheKey) } const candidates = [cacheKey, cacheKey.split('-')[0], cacheKey.split('_')[0]].filter( (value, index, list): value is string => !!value && list.indexOf(value) === index, ) let displayNames: Intl.DisplayNames | undefined for (const candidate of candidates) { try { displayNames = new Intl.DisplayNames([candidate], { type: 'region' }) break } catch { continue } } displayNamesCache.set(cacheKey, displayNames) return displayNames } function getLocalizedCountryName(iso2: string, locale: string): string { const displayNames = createCountryDisplayNames(locale) return displayNames?.of(iso2) ?? iso2 } const PRIORITIZED_COUNTRIES = ['SE', 'NO', 'DK', 'FI'] as const export function isPrioritizedCountry(iso2: string): boolean { return PRIORITIZED_COUNTRIES.includes(iso2 as (typeof PRIORITIZED_COUNTRIES)[number]) } export function sortCountryOptionsByName( options: CountryPhoneOption[], locale = 'sv-SE', _preferredCountry = 'SE', ): CountryPhoneOption[] { const sortLocale = resolveDisplayLocale(locale).split(/[-_]/)[0] || 'sv' return [...options].sort((a, b) => { const priorityA = PRIORITIZED_COUNTRIES.indexOf(a.value as (typeof PRIORITIZED_COUNTRIES)[number]) const priorityB = PRIORITIZED_COUNTRIES.indexOf(b.value as (typeof PRIORITIZED_COUNTRIES)[number]) if (priorityA !== -1 || priorityB !== -1) { if (priorityA === -1) return 1 if (priorityB === -1) return -1 if (priorityA !== priorityB) return priorityA - priorityB } const nameA = a.countryName || a.label const nameB = b.countryName || b.label return nameA.localeCompare(nameB, sortLocale) }) } function buildCountryOption(iso2: string, locale: string): CountryPhoneOption { const countryName = getLocalizedCountryName(iso2, locale) const countryCode = getCountryCallingCode(iso2 as CountryCode) return { value: iso2, label: `+${countryCode} ${countryName}`, countryCode, countryName, } } /** Single country option without building the full catalog (e.g. fixed-country UI). */ export function getCountryOption(iso2: string, locale = 'sv-SE'): CountryPhoneOption | null { const normalized = iso2.trim().toUpperCase() if (!/^[A-Z]{2}$/.test(normalized)) { return null } try { return buildCountryOption(normalized, resolveDisplayLocale(locale)) } catch { return null } } /** Country options for phone fields; SE/NO/DK/FI first, then alphabetical by name. Cached per locale. */ export function buildCountryOptions(locale = 'sv-SE', preferredCountry = 'SE'): CountryPhoneOption[] { const cacheKey = resolveDisplayLocale(locale) const cached = countryOptionsCache.get(cacheKey) if (cached) { return cached } const options = sortCountryOptionsByName( getCountries().map((iso2) => buildCountryOption(iso2, cacheKey)), cacheKey, preferredCountry, ) countryOptionsCache.set(cacheKey, options) return options } /** ISO 3166-1 alpha-2 to regional indicator flag emoji, e.g. `SE` β†’ πŸ‡ΈπŸ‡ͺ */ export function countryCodeToFlag(iso2: string): string { const normalized = iso2.trim().toUpperCase() if (normalized.length !== 2 || !/^[A-Z]{2}$/.test(normalized)) { return '' } return String.fromCodePoint(...[...normalized].map((char) => 0x1f1e6 + char.charCodeAt(0) - 65)) } export function filterCountryOptions(options: CountryPhoneOption[], query: string): CountryPhoneOption[] { const normalizedQuery = query.trim().toLowerCase().replace(/^\+/, '') if (!normalizedQuery) { return options } return options.filter((option) => { const dialCode = option.countryCode.toLowerCase() return ( option.label.toLowerCase().includes(normalizedQuery) || option.countryName.toLowerCase().includes(normalizedQuery) || option.value.toLowerCase().includes(normalizedQuery) || dialCode.includes(normalizedQuery) || `+${dialCode}`.includes(normalizedQuery) ) }) }