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 * The current default locale. 26 * 27 * @var string 28 */ 29 protected static $defaultLocale = 'en_US'; 30 31 /** 32 * The fallback locale (used if default locale is not found). 33 * 34 * @var string 35 */ 36 protected static $fallbackLocale = 'en_US'; 37 38 /** 39 * Return the current default locale. 40 * 41 * @return string 42 */ 43 public static function getDefaultLocale() 44 { 45 return static::$defaultLocale; 46 } 47 48 /** 49 * Return the current default language. 50 * 51 * @return string 52 */ 53 public static function getDefaultLanguage() 54 { 55 $info = static::explodeLocale(static::$defaultLocale); 56 57 return $info['language']; 58 } 59 60 /** 61 * Set the current default locale and language. 62 * 63 * @param string $locale 64 * 65 * @throws \Punic\Exception\InvalidLocale Throws an exception if $locale is not a valid string 66 */ 67 public static function setDefaultLocale($locale) 68 { 69 if (static::explodeLocale($locale) === null) { 70 throw new Exception\InvalidLocale($locale); 71 } 72 static::$defaultLocale = $locale; 73 } 74 75 /** 76 * Return the current fallback locale (used if default locale is not found). 77 * 78 * @return string 79 */ 80 public static function getFallbackLocale() 81 { 82 return static::$fallbackLocale; 83 } 84 85 /** 86 * Return the current fallback language (used if default locale is not found). 87 * 88 * @return string 89 */ 90 public static function getFallbackLanguage() 91 { 92 $info = static::explodeLocale(static::$fallbackLocale); 93 94 return $info['language']; 95 } 96 97 /** 98 * Set the current fallback locale and language. 99 * 100 * @param string $locale 101 * 102 * @throws \Punic\Exception\InvalidLocale Throws an exception if $locale is not a valid string 103 */ 104 public static function setFallbackLocale($locale) 105 { 106 if (static::explodeLocale($locale) === null) { 107 throw new Exception\InvalidLocale($locale); 108 } 109 if (static::$fallbackLocale !== $locale) { 110 static::$fallbackLocale = $locale; 111 static::$cache = array(); 112 } 113 } 114 115 /** 116 * Get the locale data. 117 * 118 * @param string $identifier The data identifier 119 * @param string $locale The locale identifier (if empty we'll use the current default locale) 120 * 121 * @return array 122 * 123 * @throws \Punic\Exception Throws an exception in case of problems 124 * 125 * @internal 126 */ 127 public static function get($identifier, $locale = '') 128 { 129 if (!(is_string($identifier) && isset($identifier[0]))) { 130 throw new Exception\InvalidDataFile($identifier); 131 } 132 if (empty($locale)) { 133 $locale = static::$defaultLocale; 134 } 135 if (!isset(static::$cache[$locale])) { 136 static::$cache[$locale] = array(); 137 } 138 if (!isset(static::$cache[$locale][$identifier])) { 139 if (!@preg_match('/^[a-zA-Z0-9_\\-]+$/', $identifier)) { 140 throw new Exception\InvalidDataFile($identifier); 141 } 142 $dir = static::getLocaleFolder($locale); 143 if (!isset($dir[0])) { 144 throw new Exception\DataFolderNotFound($locale, static::$fallbackLocale); 145 } 146 $file = $dir.DIRECTORY_SEPARATOR.$identifier.'.json'; 147 if (!is_file(__DIR__.DIRECTORY_SEPARATOR.$file)) { 148 throw new Exception\DataFileNotFound($identifier, $locale, static::$fallbackLocale); 149 } 150 $json = @file_get_contents(__DIR__.DIRECTORY_SEPARATOR.$file); 151 //@codeCoverageIgnoreStart 152 // In test enviro we can't replicate this problem 153 if ($json === false) { 154 throw new Exception\DataFileNotReadable($file); 155 } 156 //@codeCoverageIgnoreEnd 157 $data = @json_decode($json, true); 158 //@codeCoverageIgnoreStart 159 // In test enviro we can't replicate this problem 160 if (!is_array($data)) { 161 throw new Exception\BadDataFileContents($file, $json); 162 } 163 //@codeCoverageIgnoreEnd 164 static::$cache[$locale][$identifier] = $data; 165 } 166 167 return static::$cache[$locale][$identifier]; 168 } 169 170 /** 171 * Get the generic data. 172 * 173 * @param string $identifier The data identifier 174 * 175 * @return array 176 * 177 * @throws Exception Throws an exception in case of problems 178 * 179 * @internal 180 */ 181 public static function getGeneric($identifier) 182 { 183 if (!(is_string($identifier) && isset($identifier[0]))) { 184 throw new Exception\InvalidDataFile($identifier); 185 } 186 if (isset(static::$cacheGeneric[$identifier])) { 187 return static::$cacheGeneric[$identifier]; 188 } 189 if (!preg_match('/^[a-zA-Z0-9_\\-]+$/', $identifier)) { 190 throw new Exception\InvalidDataFile($identifier); 191 } 192 $file = 'data'.DIRECTORY_SEPARATOR."$identifier.json"; 193 if (!is_file(__DIR__.DIRECTORY_SEPARATOR.$file)) { 194 throw new Exception\DataFileNotFound($identifier); 195 } 196 $json = @file_get_contents(__DIR__.DIRECTORY_SEPARATOR.$file); 197 //@codeCoverageIgnoreStart 198 // In test enviro we can't replicate this problem 199 if ($json === false) { 200 throw new Exception\DataFileNotReadable($file); 201 } 202 //@codeCoverageIgnoreEnd 203 $data = @json_decode($json, true); 204 //@codeCoverageIgnoreStart 205 // In test enviro we can't replicate this problem 206 if (!is_array($data)) { 207 throw new Exception\BadDataFileContents($file, $json); 208 } 209 //@codeCoverageIgnoreEnd 210 static::$cacheGeneric[$identifier] = $data; 211 212 return $data; 213 } 214 215 /** 216 * Return a list of available locale identifiers. 217 * 218 * @param bool $allowGroups Set to true if you want to retrieve locale groups (eg. 'en-001'), false otherwise 219 * 220 * @return array 221 */ 222 public static function getAvailableLocales($allowGroups = false) 223 { 224 $locales = array(); 225 $dir = __DIR__.DIRECTORY_SEPARATOR.'data'; 226 if (is_dir($dir) && is_readable($dir)) { 227 $contents = @scandir($dir); 228 if (is_array($contents)) { 229 foreach (array_diff($contents, array('.', '..')) as $item) { 230 if (is_dir($dir.DIRECTORY_SEPARATOR.$item)) { 231 if ($item === 'root') { 232 $item = 'en-US'; 233 } 234 $info = static::explodeLocale($item); 235 if (is_array($info)) { 236 if ((!$allowGroups) && preg_match('/^[0-9]{3}$/', $info['territory'])) { 237 foreach (Territory::getChildTerritoryCodes($info['territory'], true) as $territory) { 238 if (isset($info['script'][0])) { 239 $locales[] = "{$info['language']}-{$info['script']}-$territory"; 240 } else { 241 $locales[] = "{$info['language']}-$territory"; 242 } 243 } 244 $locales[] = $item; 245 } else { 246 $locales[] = $item; 247 } 248 } 249 } 250 } 251 } 252 } 253 254 return $locales; 255 } 256 257 /** 258 * Try to guess the full locale (with script and territory) ID associated to a language. 259 * 260 * @param string $language The language identifier (if empty we'll use the current default language) 261 * @param string $script The script identifier (if $language is empty we'll use the current default script) 262 * 263 * @return string Returns an empty string if the territory was not found, the territory ID otherwise 264 */ 265 public static function guessFullLocale($language = '', $script = '') 266 { 267 $result = ''; 268 if (empty($language)) { 269 $defaultInfo = static::explodeLocale(static::$defaultLocale); 270 $language = $defaultInfo['language']; 271 $script = $defaultInfo['script']; 272 } 273 $data = static::getGeneric('likelySubtags'); 274 $keys = array(); 275 if (!empty($script)) { 276 $keys[] = "$language-$script"; 277 } 278 $keys[] = $language; 279 foreach ($keys as $key) { 280 if (isset($data[$key])) { 281 $result = $data[$key]; 282 if (isset($script[0]) && (stripos($result, "$language-$script-") !== 0)) { 283 $parts = static::explodeLocale($result); 284 if ($parts !== null) { 285 $result = "{$parts['language']}-$script-{$parts['territory']}"; 286 } 287 } 288 break; 289 } 290 } 291 292 return $result; 293 } 294 295 /** 296 * Return the terrotory associated to the locale (guess it if it's not present in $locale). 297 * 298 * @param string $locale The locale identifier (if empty we'll use the current default locale) 299 * @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 300 * 301 * @return string 302 */ 303 public static function getTerritory($locale = '', $checkFallbackLocale = true) 304 { 305 $result = ''; 306 if (empty($locale)) { 307 $locale = static::$defaultLocale; 308 } 309 $info = static::explodeLocale($locale); 310 if (is_array($info)) { 311 if (!isset($info['territory'][0])) { 312 $fullLocale = static::guessFullLocale($info['language'], $info['script']); 313 if (strlen($fullLocale)) { 314 $info = static::explodeLocale($fullLocale); 315 } 316 } 317 if (isset($info['territory'][0])) { 318 $result = $info['territory']; 319 } elseif ($checkFallbackLocale) { 320 $result = static::getTerritory(static::$fallbackLocale, false); 321 } 322 } 323 324 return $result; 325 } 326 327 /** 328 * @deprecated 329 */ 330 protected static function getParentTerritory($territory) 331 { 332 return Territory::getParentTerritoryCode($territory); 333 } 334 335 /** 336 * @deprecated 337 */ 338 protected static function expandTerritoryGroup($parentTerritory) 339 { 340 return Territory::getChildTerritoryCodes($parentTerritory, true); 341 } 342 343 /** 344 * Return the node associated to the locale territory. 345 * 346 * @param array $data The parent array for which you want the territory node 347 * @param string $locale The locale identifier (if empty we'll use the current default locale) 348 * 349 * @return mixed Returns null if the node was not found, the node data otherwise 350 * 351 * @internal 352 */ 353 public static function getTerritoryNode($data, $locale = '') 354 { 355 $result = null; 356 $territory = static::getTerritory($locale); 357 while (isset($territory[0])) { 358 if (isset($data[$territory])) { 359 $result = $data[$territory]; 360 break; 361 } 362 $territory = Territory::getParentTerritoryCode($territory); 363 } 364 365 return $result; 366 } 367 368 /** 369 * Return the node associated to the language (not locale) territory. 370 * 371 * @param array $data The parent array for which you want the language node 372 * @param string $locale The locale identifier (if empty we'll use the current default locale) 373 * 374 * @return mixed Returns null if the node was not found, the node data otherwise 375 * 376 * @internal 377 */ 378 public static function getLanguageNode($data, $locale = '') 379 { 380 $result = null; 381 if (empty($locale)) { 382 $locale = static::$defaultLocale; 383 } 384 foreach (static::getLocaleAlternatives($locale) as $l) { 385 if (isset($data[$l])) { 386 $result = $data[$l]; 387 break; 388 } 389 } 390 391 return $result; 392 } 393 394 /** 395 * Returns the item of an array associated to a locale. 396 * 397 * @param array $data The data containing the locale info 398 * @param string $locale The locale identifier (if empty we'll use the current default locale) 399 * 400 * @return mixed Returns null if $data is not an array or it does not contain locale info, the array item otherwise 401 * 402 * @internal 403 */ 404 public static function getLocaleItem($data, $locale = '') 405 { 406 $result = null; 407 if (is_array($data)) { 408 if (empty($locale)) { 409 $locale = static::$defaultLocale; 410 } 411 foreach (static::getLocaleAlternatives($locale) as $alternative) { 412 if (isset($data[$alternative])) { 413 $result = $data[$alternative]; 414 break; 415 } 416 } 417 } 418 419 return $result; 420 } 421 422 /** 423 * Parse a string representing a locale and extract its components. 424 * 425 * @param string $locale 426 * 427 * @return null|string[] Return null if $locale is not valid; if $locale is valid returns an array with keys 'language', 'script', 'territory', 'parentLocale' 428 * 429 * @internal 430 */ 431 public static function explodeLocale($locale) 432 { 433 $result = null; 434 if (is_string($locale)) { 435 if ($locale === 'root') { 436 $locale = 'en-US'; 437 } 438 $chunks = explode('-', str_replace('_', '-', strtolower($locale))); 439 if (count($chunks) <= 3) { 440 if (preg_match('/^[a-z]{2,3}$/', $chunks[0])) { 441 $language = $chunks[0]; 442 $script = ''; 443 $territory = ''; 444 $parentLocale = ''; 445 $ok = true; 446 $chunkCount = count($chunks); 447 for ($i = 1; $ok && ($i < $chunkCount); ++$i) { 448 if (preg_match('/^[a-z]{4}$/', $chunks[$i])) { 449 if (isset($script[0])) { 450 $ok = false; 451 } else { 452 $script = ucfirst($chunks[$i]); 453 } 454 } elseif (preg_match('/^([a-z]{2})|([0-9]{3})$/', $chunks[$i])) { 455 if (isset($territory[0])) { 456 $ok = false; 457 } else { 458 $territory = strtoupper($chunks[$i]); 459 } 460 } else { 461 $ok = false; 462 } 463 } 464 if ($ok) { 465 $parentLocales = static::getGeneric('parentLocales'); 466 if (isset($script[0]) && isset($territory[0]) && isset($parentLocales["$language-$script-$territory"])) { 467 $parentLocale = $parentLocales["$language-$script-$territory"]; 468 } elseif (isset($script[0]) && isset($parentLocales["$language-$script"])) { 469 $parentLocale = $parentLocales["$language-$script"]; 470 } elseif (isset($territory[0]) && isset($parentLocales["$language-$territory"])) { 471 $parentLocale = $parentLocales["$language-$territory"]; 472 } elseif (isset($parentLocales[$language])) { 473 $parentLocale = $parentLocales[$language]; 474 } 475 $result = array( 476 'language' => $language, 477 'script' => $script, 478 'territory' => $territory, 479 'parentLocale' => $parentLocale, 480 ); 481 } 482 } 483 } 484 } 485 486 return $result; 487 } 488 489 /** 490 * Returns the path of the locale-specific data, looking also for the fallback locale. 491 * 492 * @param string $locale The locale for which you want the data folder 493 * 494 * @return string Returns an empty string if the folder is not found, the absolute path to the folder otherwise 495 */ 496 protected static function getLocaleFolder($locale) 497 { 498 static $cache = array(); 499 $result = ''; 500 if (is_string($locale)) { 501 $key = $locale.'/'.static::$fallbackLocale; 502 if (!isset($cache[$key])) { 503 foreach (static::getLocaleAlternatives($locale) as $alternative) { 504 $dir = 'data'.DIRECTORY_SEPARATOR.$alternative; 505 if (is_dir(__DIR__.DIRECTORY_SEPARATOR.$dir)) { 506 $result = $dir; 507 break; 508 } 509 } 510 $cache[$key] = $result; 511 } 512 $result = $cache[$key]; 513 } 514 515 return $result; 516 } 517 518 /** 519 * Returns a list of locale identifiers associated to a locale. 520 * 521 * @param string $locale The locale for which you want the alternatives 522 * @param string $addFallback Set to true to add the fallback locale to the result, false otherwise 523 * 524 * @return array 525 */ 526 protected static function getLocaleAlternatives($locale, $addFallback = true) 527 { 528 $result = array(); 529 $localeInfo = static::explodeLocale($locale); 530 if (!is_array($localeInfo)) { 531 throw new Exception\InvalidLocale($locale); 532 } 533 $language = $localeInfo['language']; 534 $script = $localeInfo['script']; 535 $territory = $localeInfo['territory']; 536 $parentLocale = $localeInfo['parentLocale']; 537 if (!isset($territory[0])) { 538 $fullLocale = static::guessFullLocale($language, $script); 539 if (isset($fullLocale[0])) { 540 $localeInfo = static::explodeLocale($fullLocale); 541 $language = $localeInfo['language']; 542 $script = $localeInfo['script']; 543 $territory = $localeInfo['territory']; 544 $parentLocale = $localeInfo['parentLocale']; 545 } 546 } 547 $territories = array(); 548 while (isset($territory[0])) { 549 $territories[] = $territory; 550 $territory = Territory::getParentTerritoryCode($territory); 551 } 552 if (isset($script[0])) { 553 foreach ($territories as $territory) { 554 $result[] = "{$language}-{$script}-{$territory}"; 555 } 556 } 557 if (isset($script[0])) { 558 $result[] = "{$language}-{$script}"; 559 } 560 foreach ($territories as $territory) { 561 $result[] = "{$language}-{$territory}"; 562 if ("{$language}-{$territory}" === 'en-US') { 563 $result[] = 'root'; 564 } 565 } 566 if (isset($parentLocale[0])) { 567 $result = array_merge($result, static::getLocaleAlternatives($parentLocale, false)); 568 } 569 $result[] = $language; 570 if ($addFallback && ($locale !== static::$fallbackLocale)) { 571 $result = array_merge($result, static::getLocaleAlternatives(static::$fallbackLocale, false)); 572 } 573 for ($i = count($result) - 1; $i > 1; --$i) { 574 for ($j = 0; $j < $i; ++$j) { 575 if ($result[$i] === $result[$j]) { 576 array_splice($result, $i, 1); 577 break; 578 } 579 } 580 } 581 $i = array_search('root', $result, true); 582 if ($i !== false) { 583 array_splice($result, $i, 1); 584 $result[] = 'root'; 585 } 586 587 return $result; 588 } 589} 590