1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Intl\Data\Generator; 13 14use Symfony\Component\Filesystem\Filesystem; 15use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface; 16use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface; 17use Symfony\Component\Intl\Data\Util\ArrayAccessibleResourceBundle; 18use Symfony\Component\Intl\Data\Util\LocaleScanner; 19use Symfony\Component\Intl\Exception\MissingResourceException; 20use Symfony\Component\Intl\Locale; 21 22/** 23 * The rule for compiling the zone bundle. 24 * 25 * @author Roland Franssen <franssen.roland@gmail.com> 26 * 27 * @internal 28 */ 29class TimezoneDataGenerator extends AbstractDataGenerator 30{ 31 use FallbackTrait; 32 33 /** 34 * Collects all available zone IDs. 35 * 36 * @var string[] 37 */ 38 private $zoneIds = []; 39 private $zoneToCountryMapping = []; 40 private $localeAliases = []; 41 42 /** 43 * {@inheritdoc} 44 */ 45 protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array 46 { 47 $this->localeAliases = $scanner->scanAliases($sourceDir.'/locales'); 48 49 return $scanner->scanLocales($sourceDir.'/zone'); 50 } 51 52 /** 53 * {@inheritdoc} 54 */ 55 protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir) 56 { 57 $filesystem = new Filesystem(); 58 $filesystem->mkdir($tempDir.'/region'); 59 $compiler->compile($sourceDir.'/region', $tempDir.'/region'); 60 $compiler->compile($sourceDir.'/zone', $tempDir); 61 $compiler->compile($sourceDir.'/misc/timezoneTypes.txt', $tempDir); 62 $compiler->compile($sourceDir.'/misc/metaZones.txt', $tempDir); 63 $compiler->compile($sourceDir.'/misc/windowsZones.txt', $tempDir); 64 } 65 66 /** 67 * {@inheritdoc} 68 */ 69 protected function preGenerate() 70 { 71 $this->zoneIds = []; 72 $this->zoneToCountryMapping = []; 73 } 74 75 /** 76 * {@inheritdoc} 77 */ 78 protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array 79 { 80 if (!$this->zoneToCountryMapping) { 81 $this->zoneToCountryMapping = self::generateZoneToCountryMapping($reader->read($tempDir, 'windowsZones')); 82 } 83 84 // Don't generate aliases, as they are resolved during runtime 85 // Unless an alias is needed as fallback for de-duplication purposes 86 if (isset($this->localeAliases[$displayLocale]) && !$this->generatingFallback) { 87 return null; 88 } 89 90 $localeBundle = $reader->read($tempDir, $displayLocale); 91 92 if (!isset($localeBundle['zoneStrings']) || null === $localeBundle['zoneStrings']) { 93 return null; 94 } 95 96 $data = [ 97 'Version' => $localeBundle['Version'], 98 'Names' => $this->generateZones($reader, $tempDir, $displayLocale), 99 'Meta' => self::generateZoneMetadata($localeBundle), 100 ]; 101 102 // Don't de-duplicate a fallback locale 103 // Ensures the display locale can be de-duplicated on itself 104 if ($this->generatingFallback) { 105 return $data; 106 } 107 108 // Process again to de-duplicate locales and their fallback locales 109 // Only keep the differences 110 $fallback = $this->generateFallbackData($reader, $tempDir, $displayLocale); 111 if (isset($fallback['Names'])) { 112 $data['Names'] = array_diff($data['Names'], $fallback['Names']); 113 } 114 if (isset($fallback['Meta'])) { 115 $data['Meta'] = array_diff($data['Meta'], $fallback['Meta']); 116 } 117 if (!$data['Names'] && !$data['Meta']) { 118 return null; 119 } 120 121 $this->zoneIds = array_merge($this->zoneIds, array_keys($data['Names'])); 122 123 return $data; 124 } 125 126 /** 127 * {@inheritdoc} 128 */ 129 protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array 130 { 131 $rootBundle = $reader->read($tempDir, 'root'); 132 133 return [ 134 'Version' => $rootBundle['Version'], 135 'Meta' => self::generateZoneMetadata($rootBundle), 136 ]; 137 } 138 139 /** 140 * {@inheritdoc} 141 */ 142 protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array 143 { 144 $rootBundle = $reader->read($tempDir, 'root'); 145 146 $this->zoneIds = array_unique($this->zoneIds); 147 148 sort($this->zoneIds); 149 ksort($this->zoneToCountryMapping); 150 151 $data = [ 152 'Version' => $rootBundle['Version'], 153 'Zones' => $this->zoneIds, 154 'ZoneToCountry' => $this->zoneToCountryMapping, 155 'CountryToZone' => self::generateCountryToZoneMapping($this->zoneToCountryMapping), 156 ]; 157 158 return $data; 159 } 160 161 private function generateZones(BundleEntryReaderInterface $reader, string $tempDir, string $locale): array 162 { 163 $typeBundle = $reader->read($tempDir, 'timezoneTypes'); 164 $available = []; 165 foreach ($typeBundle['typeMap']['timezone'] as $zone => $_) { 166 if ('Etc:Unknown' === $zone || preg_match('~^Etc:GMT[-+]\d+$~', $zone)) { 167 continue; 168 } 169 170 $available[$zone] = true; 171 } 172 173 $metaBundle = $reader->read($tempDir, 'metaZones'); 174 $metazones = []; 175 foreach ($metaBundle['metazoneInfo'] as $zone => $info) { 176 foreach ($info as $metazone) { 177 $metazones[$zone] = $metazone->get(0); 178 } 179 } 180 181 $regionFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'regionFormat']); 182 $fallbackFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'fallbackFormat']); 183 $resolveName = function (string $id, string $city = null) use ($reader, $tempDir, $locale, $regionFormat, $fallbackFormat): ?string { 184 // Resolve default name as described per http://cldr.unicode.org/translation/timezones 185 if (isset($this->zoneToCountryMapping[$id])) { 186 try { 187 $country = $reader->readEntry($tempDir.'/region', $locale, ['Countries', $this->zoneToCountryMapping[$id]]); 188 } catch (MissingResourceException $e) { 189 return null; 190 } 191 192 $name = str_replace('{0}', $country, $regionFormat); 193 194 return null === $city ? $name : str_replace(['{0}', '{1}'], [$city, $name], $fallbackFormat); 195 } 196 if (null !== $city) { 197 return str_replace('{0}', $city, $regionFormat); 198 } 199 200 return null; 201 }; 202 $accessor = static function (array $indices, array ...$fallbackIndices) use ($locale, $reader, $tempDir) { 203 foreach (\func_get_args() as $indices) { 204 try { 205 return $reader->readEntry($tempDir, $locale, $indices); 206 } catch (MissingResourceException $e) { 207 } 208 } 209 210 return null; 211 }; 212 $zones = []; 213 foreach (array_keys($available) as $zone) { 214 // lg: long generic, e.g. "Central European Time" 215 // ls: long specific (not DST), e.g. "Central European Standard Time" 216 // ld: long DST, e.g. "Central European Summer Time" 217 // ec: example city, e.g. "Amsterdam" 218 $name = $accessor(['zoneStrings', $zone, 'lg'], ['zoneStrings', $zone, 'ls']); 219 $city = $accessor(['zoneStrings', $zone, 'ec']); 220 $id = str_replace(':', '/', $zone); 221 222 if (null === $name && isset($metazones[$zone])) { 223 $meta = 'meta:'.$metazones[$zone]; 224 $name = $accessor(['zoneStrings', $meta, 'lg'], ['zoneStrings', $meta, 'ls']); 225 } 226 227 // Infer a default English named city for all locales 228 // Ensures each timezone ID has a distinctive name 229 if (null === $city && 0 !== strrpos($zone, 'Etc:') && false !== $i = strrpos($zone, ':')) { 230 $city = str_replace('_', ' ', substr($zone, $i + 1)); 231 } 232 if (null === $name) { 233 $name = $resolveName($id, $city); 234 $city = null; 235 } 236 if (null === $name) { 237 continue; 238 } 239 240 // Ensure no duplicated content is generated 241 if (null !== $city && false === mb_stripos(str_replace('-', ' ', $name), str_replace('-', ' ', $city))) { 242 $name = str_replace(['{0}', '{1}'], [$city, $name], $fallbackFormat); 243 } 244 245 $zones[$id] = $name; 246 } 247 248 return $zones; 249 } 250 251 private static function generateZoneMetadata(ArrayAccessibleResourceBundle $localeBundle): array 252 { 253 $metadata = []; 254 if (isset($localeBundle['zoneStrings']['gmtFormat'])) { 255 $metadata['GmtFormat'] = str_replace('{0}', '%s', $localeBundle['zoneStrings']['gmtFormat']); 256 } 257 if (isset($localeBundle['zoneStrings']['hourFormat'])) { 258 $hourFormat = explode(';', str_replace(['HH', 'mm', 'H', 'm'], ['%02d', '%02d', '%d', '%d'], $localeBundle['zoneStrings']['hourFormat']), 2); 259 $metadata['HourFormatPos'] = $hourFormat[0]; 260 $metadata['HourFormatNeg'] = $hourFormat[1]; 261 } 262 263 return $metadata; 264 } 265 266 private static function generateZoneToCountryMapping(ArrayAccessibleResourceBundle $windowsZoneBundle): array 267 { 268 $mapping = []; 269 270 foreach ($windowsZoneBundle['mapTimezones'] as $zoneInfo) { 271 foreach ($zoneInfo as $region => $zones) { 272 if (RegionDataGenerator::isValidCountryCode($region)) { 273 $mapping += array_fill_keys(explode(' ', $zones), $region); 274 } 275 } 276 } 277 278 ksort($mapping); 279 280 return $mapping; 281 } 282 283 private static function generateCountryToZoneMapping(array $zoneToCountryMapping): array 284 { 285 $mapping = []; 286 287 foreach ($zoneToCountryMapping as $zone => $country) { 288 $mapping[$country][] = $zone; 289 } 290 291 ksort($mapping); 292 293 return $mapping; 294 } 295} 296