Skip to content

[Intl] Optionally allow Kosovo as a component region #61024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/intl-data-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ jobs:
run: uconv -V && php -i | grep 'ICU version'

- name: Run intl-data tests
run: ./phpunit --group intl-data -v
run: |
./phpunit --group intl-data --exclude-group intl-data-isolate -v
./phpunit --group intl-data --filter testWhenEnvVarNotSet -v
./phpunit --group intl-data --filter testWhenEnvVarSetFalse -v
./phpunit --group intl-data --filter testWhenEnvVarSetTrue -v
- name: Test intl-data with compressed data
run: |
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Intl/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.4
---

* Allow Kosovo as a component region, controlled by the `SYMFONY_INTL_WITH_USER_ASSIGNED` env var

7.1
---

Expand Down
80 changes: 75 additions & 5 deletions src/Symfony/Component/Intl/Countries.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
*/
final class Countries extends ResourceBundle
{
private static bool $withUserAssigned;

/**
* Returns all available countries.
*
Expand All @@ -35,7 +37,11 @@ final class Countries extends ResourceBundle
*/
public static function getCountryCodes(): array
{
return self::readEntry(['Regions'], 'meta');
if (!self::withUserAssigned()) {
return self::readEntry(['Regions'], 'meta');
}

return array_merge(self::readEntry(['Regions'], 'meta'), self::readEntry(['UserAssignedRegions'], 'meta'));
}

/**
Expand All @@ -49,7 +55,11 @@ public static function getCountryCodes(): array
*/
public static function getAlpha3Codes(): array
{
return self::readEntry(['Alpha2ToAlpha3'], 'meta');
if (!self::withUserAssigned()) {
return self::readEntry(['Alpha2ToAlpha3'], 'meta');
}

return array_merge(self::readEntry(['Alpha2ToAlpha3'], 'meta'), self::readEntry(['UserAssignedAlpha2ToAlpha3'], 'meta'));
}

/**
Expand All @@ -65,34 +75,66 @@ public static function getAlpha3Codes(): array
*/
public static function getNumericCodes(): array
{
return self::readEntry(['Alpha2ToNumeric'], 'meta');
if (!self::withUserAssigned()) {
return self::readEntry(['Alpha2ToNumeric'], 'meta');
}

return array_merge(self::readEntry(['Alpha2ToNumeric'], 'meta'), self::readEntry(['UserAssignedAlpha2ToNumeric'], 'meta'));
}

public static function getAlpha3Code(string $alpha2Code): string
{
if (self::withUserAssigned()) {
try {
return self::readEntry(['UserAssignedAlpha2ToAlpha3', $alpha2Code], 'meta');
} catch (MissingResourceException) {
}
}

return self::readEntry(['Alpha2ToAlpha3', $alpha2Code], 'meta');
}

public static function getAlpha2Code(string $alpha3Code): string
{
if (self::withUserAssigned()) {
try {
return self::readEntry(['UserAssignedAlpha3ToAlpha2', $alpha3Code], 'meta');
} catch (MissingResourceException) {
}
}

return self::readEntry(['Alpha3ToAlpha2', $alpha3Code], 'meta');
}

public static function getNumericCode(string $alpha2Code): string
{
if (self::withUserAssigned()) {
try {
return self::readEntry(['UserAssignedAlpha2ToNumeric', $alpha2Code], 'meta');
} catch (MissingResourceException) {
}
}

return self::readEntry(['Alpha2ToNumeric', $alpha2Code], 'meta');
}

public static function getAlpha2FromNumeric(string $numericCode): string
{
if (self::withUserAssigned()) {
try {
return self::readEntry(['UserAssignedNumericToAlpha2', '_'.$numericCode], 'meta');
} catch (MissingResourceException) {
}
}

// Use an underscore prefix to force numeric strings with leading zeros to remain as strings
return self::readEntry(['NumericToAlpha2', '_'.$numericCode], 'meta');
}

public static function exists(string $alpha2Code): bool
{
try {
self::readEntry(['Names', $alpha2Code]);
self::getAlpha3Code($alpha2Code);

return true;
} catch (MissingResourceException) {
Expand Down Expand Up @@ -129,6 +171,13 @@ public static function numericCodeExists(string $numericCode): bool
*/
public static function getName(string $country, ?string $displayLocale = null): string
{
if (self::withUserAssigned()) {
try {
return self::readEntry(['UserAssignedNames', $country], $displayLocale);
} catch (MissingResourceException) {
}
}

return self::readEntry(['Names', $country], $displayLocale);
}

Expand All @@ -149,7 +198,11 @@ public static function getAlpha3Name(string $alpha3Code, ?string $displayLocale
*/
public static function getNames(?string $displayLocale = null): array
{
return self::asort(self::readEntry(['Names'], $displayLocale), $displayLocale);
if (!self::withUserAssigned()) {
return self::asort(self::readEntry(['Names'], $displayLocale), $displayLocale);
}

return self::asort(array_merge(self::readEntry(['Names'], $displayLocale), self::readEntry(['UserAssignedNames'], $displayLocale)), $displayLocale);
}

/**
Expand All @@ -170,6 +223,23 @@ public static function getAlpha3Names(?string $displayLocale = null): array
return $alpha3Names;
}

/**
* Sets the internal `withUserAssigned` flag, overriding the default `SYMFONY_INTL_WITH_USER_ASSIGNED` env var.
*
* The ISO 3166/MA has received information that the CE Commission has allocated the alpha-2 user-assigned code "XK"
* to represent Kosovo in the interim of being recognized by the UN as a member state.
*
* Set `$withUserAssigned` to true to have `XK`, `XKK` and `983` available in the other functions of this class.
*/
public static function withUserAssigned(?bool $withUserAssigned = null): bool
{
if (null === $withUserAssigned) {
return self::$withUserAssigned ??= filter_var($_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'] ?? $_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED'] ?? getenv('SYMFONY_INTL_WITH_USER_ASSIGNED'), FILTER_VALIDATE_BOOLEAN);
}

return self::$withUserAssigned = $withUserAssigned;
}

protected static function getPath(): string
{
return Intl::getDataDirectory().'/'.Intl::REGION_DIR;
Expand Down
49 changes: 39 additions & 10 deletions src/Symfony/Component/Intl/Data/Generator/RegionDataGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ class RegionDataGenerator extends AbstractDataGenerator
'QO' => true, // Outlying Oceania
'XA' => true, // Pseudo-Accents
'XB' => true, // Pseudo-Bidi
'XK' => true, // Kosovo
// Misc
'ZZ' => true, // Unknown Region
];

private const USER_ASSIGNED = [
'XK' => true, // Kosovo
];

// @see https://en.wikipedia.org/wiki/ISO_3166-1_numeric#Withdrawn_codes
private const WITHDRAWN_CODES = [
128, // Canton and Enderbury Islands
Expand Down Expand Up @@ -97,7 +100,7 @@ class RegionDataGenerator extends AbstractDataGenerator

public static function isValidCountryCode(int|string|null $region): bool
{
if (isset(self::DENYLIST[$region])) {
if (isset(self::DENYLIST[$region]) || isset(self::USER_ASSIGNED[$region])) {
return false;
}

Expand All @@ -109,6 +112,11 @@ public static function isValidCountryCode(int|string|null $region): bool
return true;
}

public static function isUserAssignedCountryCode(int|string|null $region): bool
{
return isset(self::USER_ASSIGNED[$region]);
}

protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array
{
return $scanner->scanLocales($sourceDir.'/region');
Expand All @@ -131,9 +139,7 @@ protected function generateDataForLocale(BundleEntryReaderInterface $reader, str

// isset() on \ResourceBundle returns true even if the value is null
if (isset($localeBundle['Countries']) && null !== $localeBundle['Countries']) {
$data = [
'Names' => $this->generateRegionNames($localeBundle),
];
$data = $this->generateRegionNames($localeBundle);

$this->regionCodes = array_merge($this->regionCodes, array_keys($data['Names']));

Expand All @@ -153,23 +159,39 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin
$metadataBundle = $reader->read($tempDir, 'metadata');

$this->regionCodes = array_unique($this->regionCodes);

sort($this->regionCodes);

$alpha2ToAlpha3 = $this->generateAlpha2ToAlpha3Mapping(array_flip($this->regionCodes), $metadataBundle);
$userAssignedAlpha2ToAlpha3 = $this->generateAlpha2ToAlpha3Mapping(self::USER_ASSIGNED, $metadataBundle);

$alpha3ToAlpha2 = array_flip($alpha2ToAlpha3);
asort($alpha3ToAlpha2);
$userAssignedAlpha3toAlpha2 = array_flip($userAssignedAlpha2ToAlpha3);
asort($userAssignedAlpha3toAlpha2);

$alpha2ToNumeric = $this->generateAlpha2ToNumericMapping(array_flip($this->regionCodes), $metadataBundle);
$userAssignedAlpha2ToNumeric = $this->generateAlpha2ToNumericMapping(self::USER_ASSIGNED, $metadataBundle);

$numericToAlpha2 = [];
foreach ($alpha2ToNumeric as $alpha2 => $numeric) {
// Add underscore prefix to force keys with leading zeros to remain as string keys.
$numericToAlpha2['_'.$numeric] = $alpha2;
}
$userAssignedNumericToAlpha2 = [];
foreach ($userAssignedAlpha2ToNumeric as $alpha2 => $numeric) {
// Add underscore prefix to force keys with leading zeros to remain as string keys.
$userAssignedNumericToAlpha2['_'.$numeric] = $alpha2;
}

asort($numericToAlpha2);
asort($userAssignedNumericToAlpha2);

return [
'UserAssignedRegions' => array_keys(self::USER_ASSIGNED),
'UserAssignedAlpha2ToAlpha3' => $userAssignedAlpha2ToAlpha3,
'UserAssignedAlpha3ToAlpha2' => $userAssignedAlpha3toAlpha2,
'UserAssignedAlpha2ToNumeric' => $userAssignedAlpha2ToNumeric,
'UserAssignedNumericToAlpha2' => $userAssignedNumericToAlpha2,
'Regions' => $this->regionCodes,
'Alpha2ToAlpha3' => $alpha2ToAlpha3,
'Alpha3ToAlpha2' => $alpha3ToAlpha2,
Expand All @@ -181,14 +203,19 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, strin
protected function generateRegionNames(ArrayAccessibleResourceBundle $localeBundle): array
{
$unfilteredRegionNames = iterator_to_array($localeBundle['Countries']);
$regionNames = [];
$regionNames = ['UserAssignedNames' => [], 'Names' => []];

foreach ($unfilteredRegionNames as $region => $regionName) {
if (!self::isValidCountryCode($region)) {
if (!self::isValidCountryCode($region) && !self::isUserAssignedCountryCode($region)) {
continue;
}

$regionNames[$region] = $regionName;
if (self::isUserAssignedCountryCode($region)) {
$regionNames['UserAssignedNames'][$region] = $regionName;
continue;
}

$regionNames['Names'][$region] = $regionName;
}

return $regionNames;
Expand All @@ -204,7 +231,9 @@ private function generateAlpha2ToAlpha3Mapping(array $countries, ArrayAccessible
$country = $data['replacement'];

if (2 === \strlen($country) && 3 === \strlen($alias) && 'overlong' === $data['reason']) {
if (isset(self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country])) {
if (isset($countries[$country]) && self::isUserAssignedCountryCode($country)) {
$alpha2ToAlpha3[$country] = $alias;
} elseif (isset($countries[$country]) && !self::isUserAssignedCountryCode($country) && isset(self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country])) {
// Validate to prevent typos
if (!isset($aliases[self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country]])) {
throw new RuntimeException('The statically set three-letter mapping '.self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country].' for the country code '.$country.' seems to be invalid. Typo?');
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/af.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/ak.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/am.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/ar.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/as.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/az.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/az_Cyrl.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Symfony/Component/Intl/Resources/data/regions/be.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy