1<?php
2
3namespace Punic;
4
5/**
6 * Common data helper stuff.
7 */
8class Data
9{
10 /**
11 * Let's cache already loaded files (locale-specific).
12 *
13 * @var array
14 */
15 protected static $cache = array();
16
17 /**
18 * Let's cache already loaded files (not locale-specific).
19 *
20 * @var array
21 */
22 protected static $cacheGeneric = array();
23
24 /**
25 * Custom overrides of CLDR data (locale-specific).
26 *
27 * @var array
28 */
29 protected static $overrides = array();
30
31 /**
32 * Custom overrides of CLDR data (not locale-specific).
33 *
34 * @var array
35 */
36 protected static $overridesGeneric = array();
37
38 /**
39 * The current default locale.
40 *
41 * @var string
42 */
43 protected static $defaultLocale = 'en_US';
44
45 /**
46 * The fallback locale (used if default locale is not found).
47 *
48 * @var string
49 */
50 protected static $fallbackLocale = 'en_US';
51
52 /**
53 * The data root directory.
54 *
55 * @var string
56 */
57 protected static $directory;
58
59 /**
60 * Return the current default locale.
61 *
62 * @return string
63 */
64 public static function getDefaultLocale()
65 {
66 return static::$defaultLocale;
67 }
68
69 /**
70 * Return the current default language.
71 *
72 * @return string
73 */
74 public static function getDefaultLanguage()
75 {
76 $info = static::explodeLocale(static::$defaultLocale);
77
78 return $info['language'];
79 }
80
81 /**
82 * Set the current default locale and language.
83 *
84 * @param string $locale
85 *
86 * @throws \Punic\Exception\InvalidLocale Throws an exception if $locale is not a valid string
87 */
88 public static function setDefaultLocale($locale)
89 {
90 if (static::explodeLocale($locale) === null) {
91 throw new Exception\InvalidLocale($locale);
92 }
93 static::$defaultLocale = $locale;
94 }
95
96 /**
97 * Return the current fallback locale (used if default locale is not found).
98 *
99 * @return string
100 */
101 public static function getFallbackLocale()
102 {
103 return static::$fallbackLocale;
104 }
105
106 /**
107 * Return the current fallback language (used if default locale is not found).
108 *
109 * @return string
110 */
111 public static function getFallbackLanguage()
112 {
113 $info = static::explodeLocale(static::$fallbackLocale);
114
115 return $info['language'];
116 }
117
118 /**
119 * Set the current fallback locale and language.
120 *
121 * @param string $locale
122 *
123 * @throws \Punic\Exception\InvalidLocale Throws an exception if $locale is not a valid string
124 */
125 public static function setFallbackLocale($locale)
126 {
127 if (static::explodeLocale($locale) === null) {
128 throw new Exception\InvalidLocale($locale);
129 }
130 if (static::$fallbackLocale !== $locale) {
131 static::$fallbackLocale = $locale;
132 static::$cache = array();
133 }
134 }
135
136 /**
137 * Get custom overrides of CLDR locale data.
138 *
139 * If a locale is specified, overrides for that locale are returned, indexed
140 * by identifier. If no locale is specified, overrides for all locales are
141 * returned index by locale.
142 *
143 * @param null|mixed $locale
144 *
145 * @return array Associative array
146 */
147 public static function getOverrides($locale = null)
148 {
149 if (!$locale) {
150 return static::$overrides;
151 } elseif (isset(static::$overrides[$locale])) {
152 return static::$overrides[$locale];
153 }
154
155 return array();
156 }
157
158 /**
159 * Set custom overrides of CLDR locale data.
160 *
161 * Overrides may be provides either one locale at a time or all locales at once.
162 *
163 * @param array $overrides Associative array index by locale (if $locale is null) or identifier
164 * @param string $locale
165 */
166 public static function setOverrides(array $overrides, $locale = null)
167 {
168 static::$cache = array();
169
170 if ($locale) {
171 static::$overrides[$locale] = $overrides;
172 } else {
173 static::$overrides = $overrides;
174 }
175 }
176
177 /**
178 * Get custom overrides of CLDR generic data.
179 *
180 * @return array Associative array indexed by identifier
181 */
182 public static function getOverridesGeneric()
183 {
184 return static::$overridesGeneric;
185 }
186
187 /**
188 * Set custom overrides of CLDR locale.
189 *
190 * @param array Associative array indexed by identifier
191 * @param array $overrides
192 */
193 public static function setOverridesGeneric(array $overrides)
194 {
195 static::$cacheGeneric = array();
196
197 static::$overridesGeneric = $overrides;
198 }
199
200 /**
201 * Get the data root directory.
202 *
203 * @return string
204 */
205 public static function getDataDirectory()
206 {
207 if (!isset(static::$directory)) {
208 static::$directory = __DIR__.DIRECTORY_SEPARATOR.'data';
209 }
210
211 return static::$directory;
212 }
213
214 /**
215 * Set the data root directory.
216 *
217 * @param string $directory
218 */
219 public static function setDataDirectory($directory)
220 {
221 static::$directory = $directory;
222
223 static::$cache = array();
224 }
225
226 /**
227 * Get the locale data.
228 *
229 * @param string $identifier The data identifier
230 * @param string $locale The locale identifier (if empty we'll use the current default locale)
231 * @param bool $exactMatch when $locale is specified, don't look for alternatives
232 *
233 * @throws \Punic\Exception Throws an exception in case of problems
234 *
235 * @return array
236 *
237 * @internal
238 */
239 public static function get($identifier, $locale = '', $exactMatch = false)
240 {
241 if (!is_string($identifier) || $identifier === '') {
242 throw new Exception\InvalidDataFile($identifier);
243 }
244 if (empty($locale)) {
245 $locale = static::$defaultLocale;
246 $exactMatch = false;
247 } elseif ($exactMatch && !preg_match('/^\w+$/', $locale)) {
248 $exactMatch = false;
249 }
250 $cacheKey = $locale.'@'.($exactMatch ? '1' : '0');
251 if (!isset(static::$cache[$cacheKey])) {
252 static::$cache[$cacheKey] = array();
253 }
254 if (!isset(static::$cache[$cacheKey][$identifier])) {
255 if (!@preg_match('/^[a-zA-Z0-9_\\-]+$/', $identifier)) {
256 throw new Exception\InvalidDataFile($identifier);
257 }
258 if ($exactMatch) {
259 $dir = $locale;
260 } else {
261 $dir = static::getLocaleFolder($locale);
262 if ($dir === '') {
263 throw new Exception\DataFolderNotFound($locale, static::$fallbackLocale);
264 }
265 }
266 $file = static::getDataDirectory().DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.$identifier.'.php';
267 if (!is_file($file)) {
268 throw new Exception\DataFileNotFound($identifier, $locale, static::$fallbackLocale);
269 }
270 $data = include $file;
271 //@codeCoverageIgnoreStart
272 // In test enviro we can't replicate this problem
273 if (!is_array($data)) {
274 throw new Exception\BadDataFileContents($file, file_get_contents($file));
275 }
276 if (isset(static::$overrides[$locale][$identifier])) {
277 $data = static::merge($data, static::$overrides[$locale][$identifier]);
278 }
279 //@codeCoverageIgnoreEnd
280 static::$cache[$cacheKey][$identifier] = $data;
281 }
282
283 return static::$cache[$cacheKey][$identifier];
284 }
285
286 /**
287 * Get the generic data.
288 *
289 * @param string $identifier The data identifier
290 *
291 * @throws Exception Throws an exception in case of problems
292 *
293 * @return array
294 *
295 * @internal
296 */
297 public static function getGeneric($identifier)
298 {
299 if (!is_string($identifier) || $identifier === '') {
300 throw new Exception\InvalidDataFile($identifier);
301 }
302 if (isset(static::$cacheGeneric[$identifier])) {
303 return static::$cacheGeneric[$identifier];
304 }
305 if (!preg_match('/^[a-zA-Z0-9_\\-]+$/', $identifier)) {
306 throw new Exception\InvalidDataFile($identifier);
307 }
308 $file = static::getDataDirectory().DIRECTORY_SEPARATOR."$identifier.php";
309 if (!is_file($file)) {
310 throw new Exception\DataFileNotFound($identifier);
311 }
312 $data = include $file;
313 //@codeCoverageIgnoreStart
314 // In test enviro we can't replicate this problem
315 if (!is_array($data)) {
316 throw new Exception\BadDataFileContents($file, file_get_contents($file));
317 }
318 //@codeCoverageIgnoreEnd
319 if (isset(static::$overridesGeneric[$identifier])) {
320 $data = static::merge($data, static::$overridesGeneric[$identifier]);
321 }
322 static::$cacheGeneric[$identifier] = $data;
323
324 return $data;
325 }
326
327 /**
328 * Return a list of available locale identifiers.
329 *
330 * @param bool $allowGroups Set to true if you want to retrieve locale groups (eg. 'en-001'), false otherwise
331 *
332 * @return array
333 */
334 public static function getAvailableLocales($allowGroups = false)
335 {
336 $locales = array();
337 $dir = static::getDataDirectory();
338 if (is_dir($dir) && is_readable($dir)) {
339 $contents = @scandir($dir);
340 if (is_array($contents)) {
341 foreach (array_diff($contents, array('.', '..')) as $item) {
342 if (is_dir($dir.DIRECTORY_SEPARATOR.$item)) {
343 if ($item === 'root') {
344 $item = 'en-US';
345 }
346 $info = static::explodeLocale($item);
347 if (is_array($info)) {
348 if ((!$allowGroups) && preg_match('/^[0-9]{3}$/', $info['territory'])) {
349 foreach (Territory::getChildTerritoryCodes($info['territory'], true) as $territory) {
350 if ($info['script'] !== '') {
351 $locales[] = "{$info['language']}-{$info['script']}-$territory";
352 } else {
353 $locales[] = "{$info['language']}-$territory";
354 }
355 }
356 $locales[] = $item;
357 } else {
358 $locales[] = $item;
359 }
360 }
361 }
362 }
363 }
364 }
365
366 return $locales;
367 }
368
369 /**
370 * Try to guess the full locale (with script and territory) ID associated to a language.
371 *
372 * @param string $language The language identifier (if empty we'll use the current default language)
373 * @param string $script The script identifier (if $language is empty we'll use the current default script)
374 *
375 * @return string Returns an empty string if the territory was not found, the territory ID otherwise
376 */
377 public static function guessFullLocale($language = '', $script = '')
378 {
379 $result = '';
380 if (empty($language)) {
381 $defaultInfo = static::explodeLocale(static::$defaultLocale);
382 $language = $defaultInfo['language'];
383 $script = $defaultInfo['script'];
384 }
385 $data = static::getGeneric('likelySubtags');
386 $keys = array();
387 if (!empty($script)) {
388 $keys[] = "$language-$script";
389 }
390 $keys[] = $language;
391 foreach ($keys as $key) {
392 if (isset($data[$key])) {
393 $result = $data[$key];
394 if ($script !== '' && stripos($result, "$language-$script-") !== 0) {
395 $parts = static::explodeLocale($result);
396 if ($parts !== null) {
397 $result = "{$parts['language']}-$script-{$parts['territory']}";
398 }
399 }
400 break;
401 }
402 }
403
404 return $result;
405 }
406
407 /**
408 * Return the terrotory associated to the locale (guess it if it's not present in $locale).
409 *
410 * @param string $locale The locale identifier (if empty we'll use the current default locale)
411 * @param bool $checkFallbackLocale Set to true to check the fallback locale if $locale (or the default locale) don't have an associated territory, false to don't fallback to fallback locale
412 *
413 * @return string
414 */
415 public static function getTerritory($locale = '', $checkFallbackLocale = true)
416 {
417 $result = '';
418 if (empty($locale)) {
419 $locale = static::$defaultLocale;
420 }
421 $info = static::explodeLocale($locale);
422 if (is_array($info)) {
423 if ($info['territory'] === '') {
424 $fullLocale = static::guessFullLocale($info['language'], $info['script']);
425 if ($fullLocale !== '') {
426 $info = static::explodeLocale($fullLocale);
427 }
428 }
429 if ($info['territory'] !== '') {
430 $result = $info['territory'];
431 } elseif ($checkFallbackLocale) {
432 $result = static::getTerritory(static::$fallbackLocale, false);
433 }
434 }
435
436 return $result;
437 }
438
439 /**
440 * Return the node associated to the locale territory.
441 *
442 * @param array $data The parent array for which you want the territory node
443 * @param string $locale The locale identifier (if empty we'll use the current default locale)
444 *
445 * @return null|mixed Returns null if the node was not found, the node data otherwise
446 *
447 * @internal
448 */
449 public static function getTerritoryNode($data, $locale = '')
450 {
451 $result = null;
452 $territory = static::getTerritory($locale);
453 while ($territory !== '') {
454 if (isset($data[$territory])) {
455 $result = $data[$territory];
456 break;
457 }
458 $territory = Territory::getParentTerritoryCode($territory);
459 }
460
461 return $result;
462 }
463
464 /**
465 * Return the node associated to the language (not locale) territory.
466 *
467 * @param array $data The parent array for which you want the language node
468 * @param string $locale The locale identifier (if empty we'll use the current default locale)
469 *
470 * @return null|mixed Returns null if the node was not found, the node data otherwise
471 *
472 * @internal
473 */
474 public static function getLanguageNode($data, $locale = '')
475 {
476 $result = null;
477 if (empty($locale)) {
478 $locale = static::$defaultLocale;
479 }
480 foreach (static::getLocaleAlternatives($locale) as $l) {
481 if (isset($data[$l])) {
482 $result = $data[$l];
483 break;
484 }
485 }
486
487 return $result;
488 }
489
490 /**
491 * Returns the item of an array associated to a locale.
492 *
493 * @param array $data The data containing the locale info
494 * @param string $locale The locale identifier (if empty we'll use the current default locale)
495 *
496 * @return null|mixed Returns null if $data is not an array or it does not contain locale info, the array item otherwise
497 *
498 * @internal
499 */
500 public static function getLocaleItem($data, $locale = '')
501 {
502 $result = null;
503 if (is_array($data)) {
504 if (empty($locale)) {
505 $locale = static::$defaultLocale;
506 }
507 foreach (static::getLocaleAlternatives($locale) as $alternative) {
508 if (isset($data[$alternative])) {
509 $result = $data[$alternative];
510 break;
511 }
512 }
513 }
514
515 return $result;
516 }
517
518 /**
519 * Parse a string representing a locale and extract its components.
520 *
521 * @param string $locale
522 *
523 * @return null|string[] Return null if $locale is not valid; if $locale is valid returns an array with keys 'language', 'script', 'territory', 'parentLocale'
524 *
525 * @internal
526 */
527 public static function explodeLocale($locale)
528 {
529 $result = null;
530 if (is_string($locale)) {
531 if ($locale === 'root') {
532 $locale = 'en-US';
533 }
534 $chunks = explode('-', str_replace('_', '-', strtolower($locale)));
535 if (count($chunks) <= 3) {
536 if (preg_match('/^[a-z]{2,3}$/', $chunks[0])) {
537 $language = $chunks[0];
538 $script = '';
539 $territory = '';
540 $parentLocale = '';
541 $ok = true;
542 $chunkCount = count($chunks);
543 for ($i = 1; $ok && ($i < $chunkCount); ++$i) {
544 if (preg_match('/^[a-z]{4}$/', $chunks[$i])) {
545 if ($script !== '') {
546 $ok = false;
547 } else {
548 $script = ucfirst($chunks[$i]);
549 }
550 } elseif (preg_match('/^([a-z]{2})|([0-9]{3})$/', $chunks[$i])) {
551 if ($territory !== '') {
552 $ok = false;
553 } else {
554 $territory = strtoupper($chunks[$i]);
555 }
556 } else {
557 $ok = false;
558 }
559 }
560 if ($ok) {
561 $parentLocales = static::getGeneric('parentLocales');
562 if ($script !== '' && $territory !== '' && isset($parentLocales["$language-$script-$territory"])) {
563 $parentLocale = $parentLocales["$language-$script-$territory"];
564 } elseif ($script !== '' && isset($parentLocales["$language-$script"])) {
565 $parentLocale = $parentLocales["$language-$script"];
566 } elseif ($territory !== '' && isset($parentLocales["$language-$territory"])) {
567 $parentLocale = $parentLocales["$language-$territory"];
568 } elseif (isset($parentLocales[$language])) {
569 $parentLocale = $parentLocales[$language];
570 }
571 $result = array(
572 'language' => $language,
573 'script' => $script,
574 'territory' => $territory,
575 'parentLocale' => $parentLocale,
576 );
577 }
578 }
579 }
580 }
581
582 return $result;
583 }
584
585 /**
586 * Get value from nested array.
587 *
588 * @param array $data the nested array to descend into
589 * @param array $path Path of array keys. Each part of the path may be a string or an array of alternative strings.
590 *
591 * @return mixed|null
592 */
593 public static function getArrayValue(array $data, array $path)
594 {
595 $alternatives = array_shift($path);
596 if ($alternatives === null) {
597 return $data;
598 }
599 foreach ((array) $alternatives as $alternative) {
600 if (array_key_exists($alternative, $data)) {
601 $data = $data[$alternative];
602
603 return is_array($data) ? self::getArrayValue($data, $path) : ($path ? null : $data);
604 }
605 }
606
607 return null;
608 }
609
610 /**
611 * @deprecated
612 *
613 * @param string $territory
614 *
615 * @return string
616 */
617 protected static function getParentTerritory($territory)
618 {
619 return Territory::getParentTerritoryCode($territory);
620 }
621
622 /**
623 * @deprecated
624 *
625 * @param string $parentTerritory
626 *
627 * @return array
628 */
629 protected static function expandTerritoryGroup($parentTerritory)
630 {
631 return Territory::getChildTerritoryCodes($parentTerritory, true);
632 }
633
634 /**
635 * Returns the path of the locale-specific data, looking also for the fallback locale.
636 *
637 * @param string $locale The locale for which you want the data folder
638 *
639 * @return string Returns an empty string if the folder is not found, the absolute path to the folder otherwise
640 */
641 protected static function getLocaleFolder($locale)
642 {
643 static $cache = array();
644 $result = '';
645 if (is_string($locale)) {
646 $key = $locale.'/'.static::$fallbackLocale;
647 if (!isset($cache[$key])) {
648 $dir = static::getDataDirectory();
649 foreach (static::getLocaleAlternatives($locale) as $alternative) {
650 if (is_dir($dir.DIRECTORY_SEPARATOR.$alternative)) {
651 $result = $alternative;
652 break;
653 }
654 }
655 $cache[$key] = $result;
656 }
657 $result = $cache[$key];
658 }
659
660 return $result;
661 }
662
663 /**
664 * Returns a list of locale identifiers associated to a locale.
665 *
666 * @param string $locale The locale for which you want the alternatives
667 * @param bool $addFallback Set to true to add the fallback locale to the result, false otherwise
668 *
669 * @return array
670 */
671 protected static function getLocaleAlternatives($locale, $addFallback = true)
672 {
673 $result = array();
674 $localeInfo = static::explodeLocale($locale);
675 if (!is_array($localeInfo)) {
676 throw new Exception\InvalidLocale($locale);
677 }
678 $language = $localeInfo['language'];
679 $script = $localeInfo['script'];
680 $territory = $localeInfo['territory'];
681 $parentLocale = $localeInfo['parentLocale'];
682 if ($territory === '') {
683 $fullLocale = static::guessFullLocale($language, $script);
684 if ($fullLocale !== '') {
685 $localeInfo = static::explodeLocale($fullLocale);
686 $language = $localeInfo['language'];
687 $script = $localeInfo['script'];
688 $territory = $localeInfo['territory'];
689 $parentLocale = $localeInfo['parentLocale'];
690 }
691 }
692 $territories = array();
693 while ($territory !== '') {
694 $territories[] = $territory;
695 $territory = Territory::getParentTerritoryCode($territory);
696 }
697 if ($script !== '') {
698 foreach ($territories as $territory) {
699 $result[] = "{$language}-{$script}-{$territory}";
700 }
701 }
702 if ($script !== '') {
703 $result[] = "{$language}-{$script}";
704 }
705 foreach ($territories as $territory) {
706 $result[] = "{$language}-{$territory}";
707 if ("{$language}-{$territory}" === 'en-US') {
708 $result[] = 'root';
709 }
710 }
711 if ($parentLocale !== '') {
712 $result = array_merge($result, static::getLocaleAlternatives($parentLocale, false));
713 }
714 $result[] = $language;
715 if ($addFallback && ($locale !== static::$fallbackLocale)) {
716 $result = array_merge($result, static::getLocaleAlternatives(static::$fallbackLocale, false));
717 }
718 for ($i = count($result) - 1; $i > 1; --$i) {
719 for ($j = 0; $j < $i; ++$j) {
720 if ($result[$i] === $result[$j]) {
721 array_splice($result, $i, 1);
722 break;
723 }
724 }
725 }
726 if ($locale !== 'root') {
727 $i = array_search('root', $result, true);
728 if ($i !== false) {
729 array_splice($result, $i, 1);
730 $result[] = 'root';
731 }
732 }
733
734 return $result;
735 }
736
737 protected static function merge(array $data, array $overrides)
738 {
739 foreach ($overrides as $key => $override) {
740 if (isset($data[$key])) {
741 if (gettype($override) !== gettype($data[$key])) {
742 throw new Exception\InvalidOverride($data[$key], $override);
743 }
744 if (is_array($data[$key])) {
745 $data[$key] = static::merge($data[$key], $override);
746 } else {
747 $data[$key] = $override;
748 }
749 } else {
750 $data[$key] = $override;
751 }
752 }
753
754 return $data;
755 }
756}
757