28
28
*/
29
29
class TimezoneDataGenerator extends AbstractDataGenerator
30
30
{
31
+ use FallbackTrait;
32
+
31
33
/**
32
34
* Collects all available zone IDs.
33
35
*
34
36
* @var string[]
35
37
*/
36
38
private $ zoneIds = [];
39
+ private $ zoneToCountryMapping = [];
40
+ private $ localeAliases = [];
37
41
38
42
/**
39
43
* {@inheritdoc}
40
44
*/
41
45
protected function scanLocales (LocaleScanner $ scanner , $ sourceDir )
42
46
{
47
+ $ this ->localeAliases = $ scanner ->scanAliases ($ sourceDir .'/locales ' );
48
+
43
49
return $ scanner ->scanLocales ($ sourceDir .'/zone ' );
44
50
}
45
51
@@ -63,50 +69,71 @@ protected function compileTemporaryBundles(BundleCompilerInterface $compiler, $s
63
69
protected function preGenerate ()
64
70
{
65
71
$ this ->zoneIds = [];
72
+ $ this ->zoneToCountryMapping = [];
66
73
}
67
74
68
75
/**
69
76
* {@inheritdoc}
70
77
*/
71
78
protected function generateDataForLocale (BundleEntryReaderInterface $ reader , $ tempDir , $ displayLocale )
72
79
{
73
- $ localeBundle = $ reader ->read ($ tempDir , $ displayLocale );
80
+ if (!$ this ->zoneToCountryMapping ) {
81
+ $ this ->zoneToCountryMapping = self ::generateZoneToCountryMapping ($ reader ->read ($ tempDir , 'windowsZones ' ));
82
+ }
74
83
75
- if (isset ($ localeBundle ['zoneStrings ' ]) && null !== $ localeBundle ['zoneStrings ' ]) {
76
- $ localeBundles = [$ localeBundle ];
77
- $ fallback = $ displayLocale ;
78
- while (null !== ($ fallback = Locale::getFallback ($ fallback ))) {
79
- $ localeBundles [] = $ reader ->read ($ tempDir , $ fallback );
80
- }
81
- $ metadata = [];
82
- $ data = [
83
- 'Version ' => $ localeBundle ['Version ' ],
84
- 'Names ' => $ this ->generateZones (
85
- $ reader ,
86
- $ tempDir ,
87
- $ displayLocale ,
88
- $ localeBundles ,
89
- $ metadata
90
- ),
91
- ];
92
-
93
- if (!$ data ['Names ' ] && !$ metadata ) {
94
- return ;
95
- }
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 ;
88
+ }
96
89
97
- $ data ['Meta ' ] = $ metadata ;
90
+ $ localeBundle = $ reader ->read ($ tempDir , $ displayLocale );
91
+
92
+ if (!isset ($ localeBundle ['zoneStrings ' ]) || null === $ localeBundle ['zoneStrings ' ]) {
93
+ return ;
94
+ }
98
95
99
- $ this ->zoneIds = array_merge ($ this ->zoneIds , array_keys ($ data ['Names ' ]));
96
+ $ data = [
97
+ 'Version ' => $ localeBundle ['Version ' ],
98
+ 'Names ' => $ this ->generateZones ($ reader , $ tempDir , $ displayLocale ),
99
+ 'Meta ' => self ::generateZoneMetadata ($ localeBundle ),
100
+ ];
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 ) {
101
105
return $ data ;
102
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 ;
119
+ }
120
+
121
+ $ this ->zoneIds = array_merge ($ this ->zoneIds , array_keys ($ data ['Names ' ]));
122
+
123
+ return $ data ;
103
124
}
104
125
105
126
/**
106
127
* {@inheritdoc}
107
128
*/
108
129
protected function generateDataForRoot (BundleEntryReaderInterface $ reader , $ tempDir )
109
130
{
131
+ $ rootBundle = $ reader ->read ($ tempDir , 'root ' );
132
+
133
+ return [
134
+ 'Version ' => $ rootBundle ['Version ' ],
135
+ 'Meta ' => self ::generateZoneMetadata ($ rootBundle ),
136
+ ];
110
137
}
111
138
112
139
/**
@@ -119,66 +146,21 @@ protected function generateDataForMeta(BundleEntryReaderInterface $reader, $temp
119
146
$ this ->zoneIds = array_unique ($ this ->zoneIds );
120
147
121
148
sort ($ this ->zoneIds );
149
+ ksort ($ this ->zoneToCountryMapping );
122
150
123
151
$ data = [
124
152
'Version ' => $ rootBundle ['Version ' ],
125
153
'Zones ' => $ this ->zoneIds ,
126
- 'ZoneToCountry ' => self ::generateZoneToCountryMapping ($ reader ->read ($ tempDir , 'windowsZones ' )),
154
+ 'ZoneToCountry ' => $ this ->zoneToCountryMapping ,
155
+ 'CountryToZone ' => self ::generateCountryToZoneMapping ($ this ->zoneToCountryMapping ),
127
156
];
128
157
129
- $ data ['CountryToZone ' ] = self ::generateCountryToZoneMapping ($ data ['ZoneToCountry ' ]);
130
-
131
158
return $ data ;
132
159
}
133
160
134
- /**
135
- * @param ArrayAccessibleResourceBundle[] $localeBundles
136
- */
137
- private function generateZones (BundleEntryReaderInterface $ reader , string $ tempDir , string $ locale , array $ localeBundles , array &$ metadata = []): array
161
+ private function generateZones (BundleEntryReaderInterface $ reader , string $ tempDir , string $ locale ): array
138
162
{
139
163
$ typeBundle = $ reader ->read ($ tempDir , 'timezoneTypes ' );
140
- $ metaBundle = $ reader ->read ($ tempDir , 'metaZones ' );
141
- $ windowsZonesBundle = $ reader ->read ($ tempDir , 'windowsZones ' );
142
- $ accessor = static function (ArrayAccessibleResourceBundle $ resourceBundle , array $ indices ) {
143
- $ result = $ resourceBundle ;
144
- foreach ($ indices as $ indice ) {
145
- $ result = $ result [$ indice ] ?? null ;
146
- }
147
-
148
- return $ result ;
149
- };
150
- $ accessor = static function (array $ indices , &$ inherited = false ) use ($ localeBundles , $ accessor ) {
151
- $ inherited = false ;
152
- foreach ($ localeBundles as $ i => $ localeBundle ) {
153
- $ nextLocaleBundle = $ localeBundles [$ i + 1 ] ?? null ;
154
- $ result = $ accessor ($ localeBundle , $ indices );
155
- if (null !== $ result && (null === $ nextLocaleBundle || $ result !== $ accessor ($ nextLocaleBundle , $ indices ))) {
156
- $ inherited = 0 !== $ i ;
157
-
158
- return $ result ;
159
- }
160
- }
161
-
162
- return null ;
163
- };
164
- $ regionFormat = $ reader ->readEntry ($ tempDir , $ locale , ['zoneStrings ' , 'regionFormat ' ]);
165
- $ fallbackFormat = $ reader ->readEntry ($ tempDir , $ locale , ['zoneStrings ' , 'fallbackFormat ' ]);
166
- $ zoneToCountry = self ::generateZoneToCountryMapping ($ windowsZonesBundle );
167
- $ resolveName = function (string $ id , string $ city = null ) use ($ reader , $ tempDir , $ locale , $ regionFormat , $ fallbackFormat , $ zoneToCountry ): ?string {
168
- if (isset ($ zoneToCountry [$ id ])) {
169
- try {
170
- $ country = $ reader ->readEntry ($ tempDir .'/region ' , $ locale , ['Countries ' , $ zoneToCountry [$ id ]]);
171
- } catch (MissingResourceException $ e ) {
172
- return null ;
173
- }
174
-
175
- return null === $ city ? str_replace ('{0} ' , $ country , $ regionFormat ) : str_replace (['{0} ' , '{1} ' ], [$ city , $ country ], $ fallbackFormat );
176
- } elseif (null !== $ city ) {
177
- return str_replace ('{0} ' , $ city , $ regionFormat );
178
- } else {
179
- return str_replace (['/ ' , '_ ' ], ' ' , 0 === strrpos ($ id , 'Etc/ ' ) ? substr ($ id , 4 ) : $ id );
180
- }
181
- };
182
164
$ available = [];
183
165
foreach ($ typeBundle ['typeMap ' ]['timezone ' ] as $ zone => $ _ ) {
184
166
if ('Etc:Unknown ' === $ zone || preg_match ('~^Etc:GMT[-+]\d+$~ ' , $ zone )) {
@@ -188,64 +170,97 @@ private function generateZones(BundleEntryReaderInterface $reader, string $tempD
188
170
$ available [$ zone ] = true ;
189
171
}
190
172
173
+ $ metaBundle = $ reader ->read ($ tempDir , 'metaZones ' );
191
174
$ metazones = [];
192
175
foreach ($ metaBundle ['metazoneInfo ' ] as $ zone => $ info ) {
193
176
foreach ($ info as $ metazone ) {
194
177
$ metazones [$ zone ] = $ metazone ->get (0 );
195
178
}
196
179
}
197
180
198
- $ isBase = false === strpos ($ locale , '_ ' );
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
+ };
199
212
$ zones = [];
200
213
foreach (array_keys ($ available ) as $ zone ) {
201
214
// lg: long generic, e.g. "Central European Time"
202
215
// ls: long specific (not DST), e.g. "Central European Standard Time"
203
216
// ld: long DST, e.g. "Central European Summer Time"
204
217
// ec: example city, e.g. "Amsterdam"
205
- $ name = $ accessor (['zoneStrings ' , $ zone , 'lg ' ], $ nameInherited ) ?? $ accessor ( ['zoneStrings ' , $ zone , 'ls ' ], $ nameInherited );
206
- $ city = $ accessor (['zoneStrings ' , $ zone , 'ec ' ], $ cityInherited );
218
+ $ name = $ accessor (['zoneStrings ' , $ zone , 'lg ' ], ['zoneStrings ' , $ zone , 'ls ' ]);
219
+ $ city = $ accessor (['zoneStrings ' , $ zone , 'ec ' ]);
207
220
$ id = str_replace (': ' , '/ ' , $ zone );
208
221
209
222
if (null === $ name && isset ($ metazones [$ zone ])) {
210
223
$ meta = 'meta: ' .$ metazones [$ zone ];
211
- $ name = $ accessor (['zoneStrings ' , $ meta , 'lg ' ], $ nameInherited ) ?? $ accessor ( ['zoneStrings ' , $ meta , 'ls ' ], $ nameInherited );
224
+ $ name = $ accessor (['zoneStrings ' , $ meta , 'lg ' ], ['zoneStrings ' , $ meta , 'ls ' ]);
212
225
}
226
+
227
+ // Infer a default English named city for all locales
228
+ // Ensures each timezone ID has a distinctive name
213
229
if (null === $ city && 0 !== strrpos ($ zone , 'Etc: ' ) && false !== $ i = strrpos ($ zone , ': ' )) {
214
230
$ city = str_replace ('_ ' , ' ' , substr ($ zone , $ i + 1 ));
215
- $ cityInherited = !$ isBase ;
216
231
}
217
- if ($ isBase && null === $ name ) {
232
+ if (null === $ name ) {
218
233
$ name = $ resolveName ($ id , $ city );
219
234
$ city = null ;
220
235
}
221
- if (
222
- ($ nameInherited && $ cityInherited )
223
- || (null === $ name && null === $ city )
224
- || ($ nameInherited && null === $ city )
225
- || ($ cityInherited && null === $ name )
226
- ) {
236
+ if (null === $ name ) {
227
237
continue ;
228
238
}
229
- if ( null === $ name ) {
230
- $ name = $ resolveName ( $ id , $ city );
231
- } elseif (null !== $ city && false === mb_stripos (str_replace ('- ' , ' ' , $ name ), str_replace ('- ' , ' ' , $ city ))) {
239
+
240
+ // Ensure no duplicated content is generated
241
+ if (null !== $ city && false === mb_stripos (str_replace ('- ' , ' ' , $ name ), str_replace ('- ' , ' ' , $ city ))) {
232
242
$ name = str_replace (['{0} ' , '{1} ' ], [$ city , $ name ], $ fallbackFormat );
233
243
}
234
244
235
245
$ zones [$ id ] = $ name ;
236
246
}
237
247
238
- $ gmtFormat = $ accessor (['zoneStrings ' , 'gmtFormat ' ], $ gmtFormatInherited ) ?? 'GMT{0} ' ;
239
- if (!$ gmtFormatInherited || $ isBase ) {
240
- $ metadata ['GmtFormat ' ] = str_replace ('{0} ' , '%s ' , $ gmtFormat );
241
- }
248
+ return $ zones ;
249
+ }
242
250
243
- $ hourFormat = $ accessor (['zoneStrings ' , 'hourFormat ' ], $ hourFormatInherited ) ?? '+HH:mm;-HH:mm ' ;
244
- if (!$ hourFormatInherited || $ isBase ) {
245
- $ metadata ['HourFormat ' ] = explode ('; ' , str_replace (['HH ' , 'mm ' , 'H ' , 'm ' ], ['%02d ' , '%02d ' , '%d ' , '%d ' ], $ hourFormat ), 2 );
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 ];
246
261
}
247
262
248
- return $ zones ;
263
+ return $ metadata ;
249
264
}
250
265
251
266
private static function generateZoneToCountryMapping (ArrayAccessibleResourceBundle $ windowsZoneBundle ): array
0 commit comments