'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'ā' => 'a', 'ă' => 'a', 'ą' => 'a', 'ç' => 'c', 'ć' => 'c', 'č' => 'c', 'ĉ' => 'c', 'ċ' => 'c', 'ď' => 'd', 'đ' => 'd', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ē' => 'e', 'ė' => 'e', 'ę' => 'e', 'ě' => 'e', 'ğ' => 'g', 'ģ' => 'g', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ī' => 'i', 'į' => 'i', 'ı' => 'i', 'ķ' => 'k', 'ľ' => 'l', 'ļ' => 'l', 'ł' => 'l', 'ñ' => 'n', 'ń' => 'n', 'ň' => 'n', 'ņ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', 'ō' => 'o', 'ő' => 'o', 'ŕ' => 'r', 'ř' => 'r', 'ś' => 's', 'š' => 's', 'ş' => 's', 'ș' => 's', 'ť' => 't', 'ț' => 't', 'þ' => 'th', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ū' => 'u', 'ů' => 'u', 'ű' => 'u', 'ų' => 'u', 'ý' => 'y', 'ÿ' => 'y', 'ź' => 'z', 'ż' => 'z', 'ž' => 'z', 'ß' => 'ss', 'æ' => 'ae', 'œ' => 'oe', ]; /** @var array{countries: array, states: array>}|null */ private static ?array $codeMaps = null; /** @var string[]|null */ private static ?array $countries = null; /** @var array|null */ private static ?array $regions = null; /** * Resolve a WooCommerce country value (ISO code or full name) to a valid * Aivie country name, or null when there is no equivalent. */ public static function countryName(string $country): ?string { $country = trim($country); if ('' === $country) { return null; } $byCode = self::codeMaps()['countries']; if (isset($byCode[strtoupper($country)])) { return $byCode[strtoupper($country)]; } // Already a valid Aivie value, or one that differs only in case/accents. if (in_array($country, self::countries(), true)) { return $country; } $folded = self::fold($country); foreach (self::countries() as $name) { if (self::fold($name) === $folded) { return $name; } } return null; } /** * Resolve a WooCommerce state value (state code or free text) to a valid * Aivie region name for the given country, or null when there is no * equivalent. Without a mappable country the state cannot be resolved. */ public static function stateName(string $country, string $state): ?string { $state = trim($state); $countryName = self::countryName($country); if ('' === $state || null === $countryName) { return null; } $byCode = self::codeMaps()['states'][strtoupper(trim($country))] ?? []; if (isset($byCode[strtoupper($state)])) { return $byCode[strtoupper($state)]; } // Free-text state (countries without a WooCommerce state list). $regions = self::regions()[$countryName] ?? []; if (in_array($state, $regions, true)) { return $state; } $folded = self::fold($state); foreach ($regions as $region) { if (self::fold($region) === $folded) { return $region; } } return null; } /** * Normalize a place name for matching: decode entities, lowercase, * strip accents and punctuation ("Hawke’s Bay" === "Hawkes bay"). */ public static function fold(string $value): string { $value = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); $value = mb_strtolower(trim($value), 'UTF-8'); $value = strtr($value, self::TRANSLIT); return (string) preg_replace('/[^a-z0-9]+/', '', $value); } /** * @return array{countries: array, states: array>} */ private static function codeMaps(): array { return self::$codeMaps ??= include __DIR__ . '/data/code-maps.php'; } /** * @return string[] */ private static function countries(): array { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reads bundled local JSON data, not a remote URL. return self::$countries ??= json_decode((string) file_get_contents(__DIR__ . '/data/countries.json'), true) ?: []; } /** * @return array */ private static function regions(): array { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reads bundled local JSON data, not a remote URL. return self::$regions ??= json_decode((string) file_get_contents(__DIR__ . '/data/regions.json'), true) ?: []; } }