Skip to content

Commit

Permalink
[Intl] Revise timezone name generation
Browse files Browse the repository at this point in the history
  • Loading branch information
ro0NL committed May 9, 2019
1 parent 7a53e8d commit bfdb4ed
Show file tree
Hide file tree
Showing 136 changed files with 3,900 additions and 6,778 deletions.
207 changes: 111 additions & 96 deletions src/Symfony/Component/Intl/Data/Generator/TimezoneDataGenerator.php
Expand Up @@ -28,18 +28,24 @@
*/
class TimezoneDataGenerator extends AbstractDataGenerator
{
use FallbackTrait;

/**
* Collects all available zone IDs.
*
* @var string[]
*/
private $zoneIds = [];
private $zoneToCountryMapping = [];
private $localeAliases = [];

/**
* {@inheritdoc}
*/
protected function scanLocales(LocaleScanner $scanner, $sourceDir)
{
$this->localeAliases = $scanner->scanAliases($sourceDir.'/locales');

return $scanner->scanLocales($sourceDir.'/zone');
}

Expand All @@ -63,50 +69,71 @@ protected function compileTemporaryBundles(BundleCompilerInterface $compiler, $s
protected function preGenerate()
{
$this->zoneIds = [];
$this->zoneToCountryMapping = [];
}

/**
* {@inheritdoc}
*/
protected function generateDataForLocale(BundleEntryReaderInterface $reader, $tempDir, $displayLocale)
{
$localeBundle = $reader->read($tempDir, $displayLocale);
if (!$this->zoneToCountryMapping) {
$this->zoneToCountryMapping = self::generateZoneToCountryMapping($reader->read($tempDir, 'windowsZones'));
}

if (isset($localeBundle['zoneStrings']) && null !== $localeBundle['zoneStrings']) {
$localeBundles = [$localeBundle];
$fallback = $displayLocale;
while (null !== ($fallback = Locale::getFallback($fallback))) {
$localeBundles[] = $reader->read($tempDir, $fallback);
}
$metadata = [];
$data = [
'Version' => $localeBundle['Version'],
'Names' => $this->generateZones(
$reader,
$tempDir,
$displayLocale,
$localeBundles,
$metadata
),
];

if (!$data['Names'] && !$metadata) {
return;
}
// Don't generate aliases, as they are resolved during runtime
// Unless an alias is needed as fallback for de-duplication purposes
if (isset($this->localeAliases[$displayLocale]) && !$this->generatingFallback) {
return;
}

$data['Meta'] = $metadata;
$localeBundle = $reader->read($tempDir, $displayLocale);

if (!isset($localeBundle['zoneStrings']) || null === $localeBundle['zoneStrings']) {
return;
}

$this->zoneIds = array_merge($this->zoneIds, array_keys($data['Names']));
$data = [
'Version' => $localeBundle['Version'],
'Names' => $this->generateZones($reader, $tempDir, $displayLocale),
'Meta' => self::generateZoneMetadata($localeBundle),
];

// Don't de-duplicate a fallback locale
// Ensures the display locale can be de-duplicated on itself
if ($this->generatingFallback) {
return $data;
}

// Process again to de-duplicate locales and their fallback locales
// Only keep the differences
$fallback = $this->generateFallbackData($reader, $tempDir, $displayLocale);
if (isset($fallback['Names'])) {
$data['Names'] = array_diff($data['Names'], $fallback['Names']);
}
if (isset($fallback['Meta'])) {
$data['Meta'] = array_diff($data['Meta'], $fallback['Meta']);
}
if (!$data['Names'] && !$data['Meta']) {
return;
}

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

return $data;
}

/**
* {@inheritdoc}
*/
protected function generateDataForRoot(BundleEntryReaderInterface $reader, $tempDir)
{
$rootBundle = $reader->read($tempDir, 'root');

return [
'Version' => $rootBundle['Version'],
'Meta' => self::generateZoneMetadata($rootBundle),
];
}

/**
Expand All @@ -119,66 +146,21 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, $temp
$this->zoneIds = array_unique($this->zoneIds);

sort($this->zoneIds);
ksort($this->zoneToCountryMapping);

$data = [
'Version' => $rootBundle['Version'],
'Zones' => $this->zoneIds,
'ZoneToCountry' => self::generateZoneToCountryMapping($reader->read($tempDir, 'windowsZones')),
'ZoneToCountry' => $this->zoneToCountryMapping,
'CountryToZone' => self::generateCountryToZoneMapping($this->zoneToCountryMapping),
];

$data['CountryToZone'] = self::generateCountryToZoneMapping($data['ZoneToCountry']);

return $data;
}

/**
* @param ArrayAccessibleResourceBundle[] $localeBundles
*/
private function generateZones(BundleEntryReaderInterface $reader, string $tempDir, string $locale, array $localeBundles, array &$metadata = []): array
private function generateZones(BundleEntryReaderInterface $reader, string $tempDir, string $locale): array
{
$typeBundle = $reader->read($tempDir, 'timezoneTypes');
$metaBundle = $reader->read($tempDir, 'metaZones');
$windowsZonesBundle = $reader->read($tempDir, 'windowsZones');
$accessor = static function (ArrayAccessibleResourceBundle $resourceBundle, array $indices) {
$result = $resourceBundle;
foreach ($indices as $indice) {
$result = $result[$indice] ?? null;
}

return $result;
};
$accessor = static function (array $indices, &$inherited = false) use ($localeBundles, $accessor) {
$inherited = false;
foreach ($localeBundles as $i => $localeBundle) {
$nextLocaleBundle = $localeBundles[$i + 1] ?? null;
$result = $accessor($localeBundle, $indices);
if (null !== $result && (null === $nextLocaleBundle || $result !== $accessor($nextLocaleBundle, $indices))) {
$inherited = 0 !== $i;

return $result;
}
}

return null;
};
$regionFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'regionFormat']);
$fallbackFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'fallbackFormat']);
$zoneToCountry = self::generateZoneToCountryMapping($windowsZonesBundle);
$resolveName = function (string $id, string $city = null) use ($reader, $tempDir, $locale, $regionFormat, $fallbackFormat, $zoneToCountry): ?string {
if (isset($zoneToCountry[$id])) {
try {
$country = $reader->readEntry($tempDir.'/region', $locale, ['Countries', $zoneToCountry[$id]]);
} catch (MissingResourceException $e) {
return null;
}

return null === $city ? str_replace('{0}', $country, $regionFormat) : str_replace(['{0}', '{1}'], [$city, $country], $fallbackFormat);
} elseif (null !== $city) {
return str_replace('{0}', $city, $regionFormat);
} else {
return str_replace(['/', '_'], ' ', 0 === strrpos($id, 'Etc/') ? substr($id, 4) : $id);
}
};
$available = [];
foreach ($typeBundle['typeMap']['timezone'] as $zone => $_) {
if ('Etc:Unknown' === $zone || preg_match('~^Etc:GMT[-+]\d+$~', $zone)) {
Expand All @@ -188,64 +170,97 @@ private function generateZones(BundleEntryReaderInterface $reader, string $tempD
$available[$zone] = true;
}

$metaBundle = $reader->read($tempDir, 'metaZones');
$metazones = [];
foreach ($metaBundle['metazoneInfo'] as $zone => $info) {
foreach ($info as $metazone) {
$metazones[$zone] = $metazone->get(0);
}
}

$isBase = false === strpos($locale, '_');
$regionFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'regionFormat']);
$fallbackFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'fallbackFormat']);
$resolveName = function (string $id, string $city = null) use ($reader, $tempDir, $locale, $regionFormat, $fallbackFormat): ?string {
// Resolve default name as described per http://cldr.unicode.org/translation/timezones
if (isset($this->zoneToCountryMapping[$id])) {
try {
$country = $reader->readEntry($tempDir.'/region', $locale, ['Countries', $this->zoneToCountryMapping[$id]]);
} catch (MissingResourceException $e) {
return null;
}

$name = str_replace('{0}', $country, $regionFormat);

return null === $city ? $name : str_replace(['{0}', '{1}'], [$city, $name], $fallbackFormat);
}
if (null !== $city) {
return str_replace('{0}', $city, $regionFormat);
}

return null;
};
$accessor = static function (array $indices, array ...$fallbackIndices) use ($locale, $reader, $tempDir) {
foreach (\func_get_args() as $indices) {
try {
return $reader->readEntry($tempDir, $locale, $indices);
} catch (MissingResourceException $e) {
}
}

return null;
};
$zones = [];
foreach (array_keys($available) as $zone) {
// lg: long generic, e.g. "Central European Time"
// ls: long specific (not DST), e.g. "Central European Standard Time"
// ld: long DST, e.g. "Central European Summer Time"
// ec: example city, e.g. "Amsterdam"
$name = $accessor(['zoneStrings', $zone, 'lg'], $nameInherited) ?? $accessor(['zoneStrings', $zone, 'ls'], $nameInherited);
$city = $accessor(['zoneStrings', $zone, 'ec'], $cityInherited);
$name = $accessor(['zoneStrings', $zone, 'lg'], ['zoneStrings', $zone, 'ls']);
$city = $accessor(['zoneStrings', $zone, 'ec']);
$id = str_replace(':', '/', $zone);

if (null === $name && isset($metazones[$zone])) {
$meta = 'meta:'.$metazones[$zone];
$name = $accessor(['zoneStrings', $meta, 'lg'], $nameInherited) ?? $accessor(['zoneStrings', $meta, 'ls'], $nameInherited);
$name = $accessor(['zoneStrings', $meta, 'lg'], ['zoneStrings', $meta, 'ls']);
}

// Infer a default English named city for all locales
// Ensures each timezone ID has a distinctive name
if (null === $city && 0 !== strrpos($zone, 'Etc:') && false !== $i = strrpos($zone, ':')) {
$city = str_replace('_', ' ', substr($zone, $i + 1));
$cityInherited = !$isBase;
}
if ($isBase && null === $name) {
if (null === $name) {
$name = $resolveName($id, $city);
$city = null;
}
if (
($nameInherited && $cityInherited)
|| (null === $name && null === $city)
|| ($nameInherited && null === $city)
|| ($cityInherited && null === $name)
) {
if (null === $name) {
continue;
}
if (null === $name) {
$name = $resolveName($id, $city);
} elseif (null !== $city && false === mb_stripos(str_replace('-', ' ', $name), str_replace('-', ' ', $city))) {

// Ensure no duplicated content is generated
if (null !== $city && false === mb_stripos(str_replace('-', ' ', $name), str_replace('-', ' ', $city))) {
$name = str_replace(['{0}', '{1}'], [$city, $name], $fallbackFormat);
}

$zones[$id] = $name;
}

$gmtFormat = $accessor(['zoneStrings', 'gmtFormat'], $gmtFormatInherited) ?? 'GMT{0}';
if (!$gmtFormatInherited || $isBase) {
$metadata['GmtFormat'] = str_replace('{0}', '%s', $gmtFormat);
}
return $zones;
}

$hourFormat = $accessor(['zoneStrings', 'hourFormat'], $hourFormatInherited) ?? '+HH:mm;-HH:mm';
if (!$hourFormatInherited || $isBase) {
$metadata['HourFormat'] = explode(';', str_replace(['HH', 'mm', 'H', 'm'], ['%02d', '%02d', '%d', '%d'], $hourFormat), 2);
private static function generateZoneMetadata(ArrayAccessibleResourceBundle $localeBundle): array
{
$metadata = [];
if (isset($localeBundle['zoneStrings']['gmtFormat'])) {
$metadata['GmtFormat'] = str_replace('{0}', '%s', $localeBundle['zoneStrings']['gmtFormat']);
}
if (isset($localeBundle['zoneStrings']['hourFormat'])) {
$hourFormat = explode(';', str_replace(['HH', 'mm', 'H', 'm'], ['%02d', '%02d', '%d', '%d'], $localeBundle['zoneStrings']['hourFormat']), 2);
$metadata['HourFormatPos'] = $hourFormat[0];
$metadata['HourFormatNeg'] = $hourFormat[1];
}

return $zones;
return $metadata;
}

private static function generateZoneToCountryMapping(ArrayAccessibleResourceBundle $windowsZoneBundle): array
Expand Down
24 changes: 9 additions & 15 deletions src/Symfony/Component/Intl/Resources/data/timezones/af.json
Expand Up @@ -99,7 +99,7 @@
"America\/Detroit": "Noord-Amerikaanse oostelike tyd (Detroit)",
"America\/Dominica": "Atlantiese tyd (Dominica)",
"America\/Edmonton": "Noord-Amerikaanse bergtyd (Edmonton)",
"America\/Eirunepe": "Brasilië (Eirunepe)",
"America\/Eirunepe": "Brasilië-tyd (Eirunepe)",
"America\/El_Salvador": "Noord-Amerikaanse sentrale tyd (El Salvador)",
"America\/Fort_Nelson": "Noord-Amerikaanse bergtyd (Fort Nelson)",
"America\/Fortaleza": "Brasilia-tyd (Fortaleza)",
Expand Down Expand Up @@ -151,7 +151,7 @@
"America\/Moncton": "Atlantiese tyd (Moncton)",
"America\/Monterrey": "Noord-Amerikaanse sentrale tyd (Monterrey)",
"America\/Montevideo": "Uruguay-tyd (Montevideo)",
"America\/Montreal": "Kanada (Montreal)",
"America\/Montreal": "Kanada-tyd (Montreal)",
"America\/Montserrat": "Atlantiese tyd (Montserrat)",
"America\/Nassau": "Noord-Amerikaanse oostelike tyd (Nassau)",
"America\/New_York": "Noord-Amerikaanse oostelike tyd (New York)",
Expand All @@ -176,7 +176,7 @@
"America\/Recife": "Brasilia-tyd (Recife)",
"America\/Regina": "Noord-Amerikaanse sentrale tyd (Regina)",
"America\/Resolute": "Noord-Amerikaanse sentrale tyd (Resolute)",
"America\/Rio_Branco": "Brasilië (Rio Branco)",
"America\/Rio_Branco": "Brasilië-tyd (Rio Branco)",
"America\/Santa_Isabel": "Noordwes-Meksiko-tyd (Santa Isabel)",
"America\/Santarem": "Brasilia-tyd (Santarem)",
"America\/Santiago": "Chili-tyd (Santiago)",
Expand Down Expand Up @@ -226,7 +226,7 @@
"Asia\/Bahrain": "Arabiese tyd (Bahrein)",
"Asia\/Baku": "Aserbeidjan-tyd (Bakoe)",
"Asia\/Bangkok": "Indosjina-tyd (Bangkok)",
"Asia\/Barnaul": "Rusland (Barnaul)",
"Asia\/Barnaul": "Rusland-tyd (Barnaul)",
"Asia\/Beirut": "Oos-Europese tyd (Beiroet)",
"Asia\/Bishkek": "Kirgistan-tyd (Bisjkek)",
"Asia\/Brunei": "Broenei Darussalam-tyd",
Expand Down Expand Up @@ -288,9 +288,9 @@
"Asia\/Tehran": "Iran-tyd (Tehran)",
"Asia\/Thimphu": "Bhoetan-tyd (Thimphu)",
"Asia\/Tokyo": "Japan-tyd (Tokio)",
"Asia\/Tomsk": "Rusland (Tomsk)",
"Asia\/Tomsk": "Rusland-tyd (Tomsk)",
"Asia\/Ulaanbaatar": "Ulaanbaatar-tyd",
"Asia\/Urumqi": "Sjina (Urumqi)",
"Asia\/Urumqi": "Sjina-tyd (Urumqi)",
"Asia\/Ust-Nera": "Wladiwostok-tyd (Ust-Nera)",
"Asia\/Vientiane": "Indosjina-tyd (Vientiane)",
"Asia\/Vladivostok": "Wladiwostok-tyd",
Expand Down Expand Up @@ -341,11 +341,11 @@
"Europe\/Guernsey": "Greenwich-tyd (Guernsey)",
"Europe\/Helsinki": "Oos-Europese tyd (Helsinki)",
"Europe\/Isle_of_Man": "Greenwich-tyd (Eiland Man)",
"Europe\/Istanbul": "Turkye (Istanbul)",
"Europe\/Istanbul": "Turkye-tyd (Istanbul)",
"Europe\/Jersey": "Greenwich-tyd (Jersey)",
"Europe\/Kaliningrad": "Oos-Europese tyd (Kaliningrad)",
"Europe\/Kiev": "Oos-Europese tyd (Kiëf)",
"Europe\/Kirov": "Rusland (Kirov)",
"Europe\/Kirov": "Rusland-tyd (Kirov)",
"Europe\/Lisbon": "Wes-Europese tyd (Lissabon)",
"Europe\/Ljubljana": "Sentraal-Europese tyd (Ljubljana)",
"Europe\/London": "Greenwich-tyd (Londen)",
Expand Down Expand Up @@ -436,11 +436,5 @@
"Pacific\/Wake": "Wake-eiland-tyd",
"Pacific\/Wallis": "Wallis en Futuna-tyd (Mata-Utu)"
},
"Meta": {
"GmtFormat": "GMT%s",
"HourFormat": [
"+%02d:%02d",
"-%02d:%02d"
]
}
"Meta": []
}

0 comments on commit bfdb4ed

Please sign in to comment.