diff --git a/src/Symfony/Component/Intl/CHANGELOG.md b/src/Symfony/Component/Intl/CHANGELOG.md index 7c8d46745533d..adc1405631d7a 100644 --- a/src/Symfony/Component/Intl/CHANGELOG.md +++ b/src/Symfony/Component/Intl/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * added `Languages` and `Scripts` in favor of `Intl::getLanguageBundle()` * added `Locales` in favor of `Intl::getLocaleBundle()` * added `Regions` in favor of `Intl::getRegionBundle()` + * added `Timezones` 4.2.0 ----- diff --git a/src/Symfony/Component/Intl/Data/Generator/TimezoneDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/TimezoneDataGenerator.php new file mode 100644 index 0000000000000..3f2412ff0b269 --- /dev/null +++ b/src/Symfony/Component/Intl/Data/Generator/TimezoneDataGenerator.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Data\Generator; + +use Symfony\Component\Intl\Data\Bundle\Compiler\GenrbCompiler; +use Symfony\Component\Intl\Data\Bundle\Reader\BundleReaderInterface; +use Symfony\Component\Intl\Data\Util\ArrayAccessibleResourceBundle; +use Symfony\Component\Intl\Data\Util\LocaleScanner; + +/** + * The rule for compiling the zone bundle. + * + * @author Roland Franssen + * + * @internal + */ +class TimezoneDataGenerator extends AbstractDataGenerator +{ + /** + * Collects all available zone codes. + * + * @var string[] + */ + private $zoneCodes = []; + + /** + * {@inheritdoc} + */ + protected function scanLocales(LocaleScanner $scanner, $sourceDir) + { + return $scanner->scanLocales($sourceDir.'/zone'); + } + + /** + * {@inheritdoc} + */ + protected function compileTemporaryBundles(GenrbCompiler $compiler, $sourceDir, $tempDir) + { + $compiler->compile($sourceDir.'/zone', $tempDir); + $compiler->compile($sourceDir.'/misc/timezoneTypes.txt', $tempDir); + $compiler->compile($sourceDir.'/misc/metaZones.txt', $tempDir); + } + + /** + * {@inheritdoc} + */ + protected function preGenerate() + { + $this->zoneCodes = []; + } + + /** + * {@inheritdoc} + */ + protected function generateDataForLocale(BundleReaderInterface $reader, $tempDir, $displayLocale) + { + $localeBundle = $reader->read($tempDir, $displayLocale); + + if (isset($localeBundle['zoneStrings']) && null !== $localeBundle['zoneStrings']) { + $data = [ + 'Version' => $localeBundle['Version'], + 'Names' => self::generateZones( + $reader->read($tempDir, 'timezoneTypes'), + $reader->read($tempDir, 'metaZones'), + $reader->read($tempDir, 'root'), + $localeBundle + ), + ]; + + $this->zoneCodes = array_merge($this->zoneCodes, array_keys($data['Names'])); + + return $data; + } + } + + /** + * {@inheritdoc} + */ + protected function generateDataForRoot(BundleReaderInterface $reader, $tempDir) + { + } + + /** + * {@inheritdoc} + */ + protected function generateDataForMeta(BundleReaderInterface $reader, $tempDir) + { + $rootBundle = $reader->read($tempDir, 'root'); + + $this->zoneCodes = array_unique($this->zoneCodes); + + sort($this->zoneCodes); + + $data = [ + 'Version' => $rootBundle['Version'], + 'Zones' => $this->zoneCodes, + ]; + + return $data; + } + + private static function generateZones(ArrayAccessibleResourceBundle $typeBundle, ArrayAccessibleResourceBundle $metaBundle, ArrayAccessibleResourceBundle $rootBundle, ArrayAccessibleResourceBundle $localeBundle): array + { + $available = []; + foreach ($typeBundle['typeMap']['timezone'] as $zone => $_) { + if ('Etc:Unknown' === $zone || preg_match('~^Etc:GMT[-+]\d+$~', $zone)) { + continue; + } + + $available[$zone] = true; + } + + $metazones = []; + foreach ($metaBundle['metazoneInfo'] as $zone => $info) { + foreach ($info as $metazone) { + $metazones[$zone] = $metazone->get(0); + } + } + + $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 = $localeBundle['zoneStrings'][$zone]['lg'] ?? $rootBundle['zoneStrings'][$zone]['lg'] ?? $localeBundle['zoneStrings'][$zone]['ls'] ?? $rootBundle['zoneStrings'][$zone]['ls'] ?? null; + $city = $localeBundle['zoneStrings'][$zone]['ec'] ?? $rootBundle['zoneStrings'][$zone]['ec'] ?? null; + + if (null === $name && isset($metazones[$zone])) { + $meta = 'meta:'.$metazones[$zone]; + $name = $localeBundle['zoneStrings'][$meta]['lg'] ?? $rootBundle['zoneStrings'][$meta]['lg'] ?? $localeBundle['zoneStrings'][$meta]['ls'] ?? $rootBundle['zoneStrings'][$meta]['ls'] ?? null; + } + if (null === $city && 0 !== strrpos($zone, 'Etc:') && false !== $i = strrpos($zone, ':')) { + $city = str_replace('_', ' ', substr($zone, $i + 1)); + } + if (null === $name) { + continue; + } + if (null !== $city) { + $name .= ' ('.$city.')'; + } + + $id = str_replace(':', '/', $zone); + $zones[$id] = $name; + } + + return $zones; + } +} diff --git a/src/Symfony/Component/Intl/Intl.php b/src/Symfony/Component/Intl/Intl.php index 7d121971e880d..e3763b7f52c60 100644 --- a/src/Symfony/Component/Intl/Intl.php +++ b/src/Symfony/Component/Intl/Intl.php @@ -64,6 +64,11 @@ final class Intl */ const REGION_DIR = 'regions'; + /** + * The directory name of the zone data. + */ + public const TIMEZONE_DIR = 'timezones'; + /** * @var ResourceBundle\CurrencyBundleInterface */ diff --git a/src/Symfony/Component/Intl/Resources/bin/update-data.php b/src/Symfony/Component/Intl/Resources/bin/update-data.php index d6e25af474569..b683d99308934 100644 --- a/src/Symfony/Component/Intl/Resources/bin/update-data.php +++ b/src/Symfony/Component/Intl/Resources/bin/update-data.php @@ -20,6 +20,7 @@ use Symfony\Component\Intl\Data\Generator\LocaleDataGenerator; use Symfony\Component\Intl\Data\Generator\RegionDataGenerator; use Symfony\Component\Intl\Data\Generator\ScriptDataGenerator; +use Symfony\Component\Intl\Data\Generator\TimezoneDataGenerator; use Symfony\Component\Intl\Data\Provider\LanguageDataProvider; use Symfony\Component\Intl\Data\Provider\RegionDataProvider; use Symfony\Component\Intl\Data\Provider\ScriptDataProvider; @@ -195,6 +196,7 @@ $targetDir.'/'.Intl::LOCALE_DIR, $targetDir.'/'.Intl::REGION_DIR, $targetDir.'/'.Intl::SCRIPT_DIR, + $targetDir.'/'.Intl::TIMEZONE_DIR, ]); } @@ -256,6 +258,11 @@ // //$filesystem->remove($txtDir); +echo "Generating zone data...\n"; + +$generator = new TimezoneDataGenerator($compiler, Intl::TIMEZONE_DIR); +$generator->generateData($config); + echo "Resource bundle compilation complete.\n"; $gitInfo = << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests; + +use Symfony\Component\Intl\Timezones; + +/** + * @group intl-data + */ +class TimezonesTest extends ResourceBundleTestCase +{ + // The below arrays document the state of the ICU data bundled with this package. + + private static $zones = [ + 'Africa/Abidjan', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Algiers', + 'Africa/Asmera', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/El_Aaiun', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Sao_Tome', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Anguilla', + 'America/Antigua', + 'America/Araguaina', + 'America/Argentina/La_Rioja', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Aruba', + 'America/Asuncion', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Buenos_Aires', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Catamarca', + 'America/Cayenne', + 'America/Cayman', + 'America/Chicago', + 'America/Chihuahua', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Costa_Rica', + 'America/Creston', + 'America/Cuiaba', + 'America/Curacao', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Dominica', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Fort_Nelson', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Godthab', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Indianapolis', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Jujuy', + 'America/Juneau', + 'America/Kentucky/Monticello', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Louisville', + 'America/Lower_Princes', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Mendoza', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/Montreal', + 'America/Montserrat', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Ojinaga', + 'America/Panama', + 'America/Pangnirtung', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port-au-Prince', + 'America/Port_of_Spain', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Santa_Isabel', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Sitka', + 'America/St_Barthelemy', + 'America/St_Johns', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Thunder_Bay', + 'America/Tijuana', + 'America/Toronto', + 'America/Tortola', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/McMurdo', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Bahrain', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Brunei', + 'Asia/Calcutta', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Colombo', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Hebron', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Katmandu', + 'Asia/Khandyga', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Kuwait', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Muscat', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qostanay', + 'Asia/Qyzylorda', + 'Asia/Rangoon', + 'Asia/Riyadh', + 'Asia/Saigon', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ulaanbaatar', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vientiane', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faeroe', + 'Atlantic/Madeira', + 'Atlantic/Reykjavik', + 'Atlantic/South_Georgia', + 'Atlantic/St_Helena', + 'Atlantic/Stanley', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Currie', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/Perth', + 'Australia/Sydney', + 'CST6CDT', + 'EST5EDT', + 'Etc/GMT', + 'Etc/UTC', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Busingen', + 'Europe/Chisinau', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Guernsey', + 'Europe/Helsinki', + 'Europe/Isle_of_Man', + 'Europe/Istanbul', + 'Europe/Jersey', + 'Europe/Kaliningrad', + 'Europe/Kiev', + 'Europe/Kirov', + 'Europe/Lisbon', + 'Europe/Ljubljana', + 'Europe/London', + 'Europe/Luxembourg', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Mariehamn', + 'Europe/Minsk', + 'Europe/Monaco', + 'Europe/Moscow', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Skopje', + 'Europe/Sofia', + 'Europe/Stockholm', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Ulyanovsk', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'Europe/Zurich', + 'Indian/Antananarivo', + 'Indian/Chagos', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Maldives', + 'Indian/Mauritius', + 'Indian/Mayotte', + 'Indian/Reunion', + 'MST7MDT', + 'PST8PDT', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Enderbury', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Funafuti', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Johnston', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Marquesas', + 'Pacific/Midway', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Ponape', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Saipan', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + 'Pacific/Truk', + 'Pacific/Wake', + 'Pacific/Wallis', + ]; + + public function testGetTimezones() + { + $this->assertEquals(self::$zones, Timezones::getIds()); + } + + /** + * @dataProvider provideLocales + */ + public function testGetNames($displayLocale) + { + $zones = array_keys(Timezones::getNames($displayLocale)); + + sort($zones); + + $this->assertNotEmpty($zones); + $this->assertEmpty(array_diff($zones, self::$zones)); + } + + public function testGetNamesDefaultLocale() + { + \Locale::setDefault('de_AT'); + + $this->assertSame(Timezones::getNames('de_AT'), Timezones::getNames()); + } + + /** + * @dataProvider provideLocaleAliases + */ + public function testGetNamesSupportsAliases($alias, $ofLocale) + { + // Can't use assertSame(), because some aliases contain scripts with + // different collation (=order of output) than their aliased locale + // e.g. sr_Latn_ME => sr_ME + $this->assertEquals(Timezones::getNames($ofLocale), Timezones::getNames($alias)); + } + + /** + * @dataProvider provideLocales + */ + public function testGetName($displayLocale) + { + $names = Timezones::getNames($displayLocale); + + foreach ($names as $language => $name) { + $this->assertSame($name, Timezones::getName($language, $displayLocale)); + } + } + + public function testGetNameDefaultLocale() + { + \Locale::setDefault('de_AT'); + + $names = Timezones::getNames('de_AT'); + + foreach ($names as $language => $name) { + $this->assertSame($name, Timezones::getName($language)); + } + } +} diff --git a/src/Symfony/Component/Intl/Timezones.php b/src/Symfony/Component/Intl/Timezones.php new file mode 100644 index 0000000000000..89577ca7f85c6 --- /dev/null +++ b/src/Symfony/Component/Intl/Timezones.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl; + +use Symfony\Component\Intl\Exception\MissingResourceException; + +/** + * Gives access to timezone-related ICU data. + * + * @author Roland Franssen + */ +final class Timezones extends ResourceBundle +{ + /** + * @return string[] + */ + public static function getIds(): array + { + return self::readEntry(['Zones'], 'meta'); + } + + public static function exists(string $timezone): bool + { + try { + self::readEntry(['Names', $timezone]); + + return true; + } catch (MissingResourceException $e) { + return false; + } + } + + public static function getName(string $timezone, string $displayLocale = null): string + { + return self::readEntry(['Names', $timezone], $displayLocale); + } + + /** + * @return string[] + */ + public static function getNames(string $displayLocale = null): array + { + return self::asort(self::readEntry(['Names'], $displayLocale), $displayLocale); + } + + protected static function getPath(): string + { + return Intl::getDataDirectory().'/'.Intl::TIMEZONE_DIR; + } +}