Source Code: SmartFactory/LanguageManager.php

   1: <?php
   2: /**
   3:  * This file contains the implementation of the interface ILanguageManager
   4:  * in the class LanguageManager for working with localization of texts.
   5:  *
   6:  * @package System
   7:  *
   8:  * @author Oleg Schildt
   9:  */
  10: 
  11: namespace SmartFactory;
  12: 
  13: use \SmartFactory\Interfaces\ILanguageManager;
  14: 
  15: /**
  16:  * Class for working with localization of texts.
  17:  *
  18:  * @author Oleg Schildt
  19:  */
  20: class LanguageManager implements ILanguageManager
  21: {
  22:     /**
  23:      * Internal variable for storing the path with the localization files.
  24:      *
  25:      * @var string
  26:      *
  27:      * @author Oleg Schildt
  28:      */
  29:     protected string $localization_path = "";
  30: 
  31:     /**
  32:      * Internal variable for storing the paths of the additional localization files.
  33:      *
  34:      * @var string|array
  35:      *
  36:      * @author Oleg Schildt
  37:      */
  38:     protected string|array $additional_localization_files = [];
  39:     
  40:     /**
  41:      * Internal variable for storing the fallback language.
  42:      * If set and a translation is missing on a language, the translation on this language will be used.
  43:      *
  44:      * @var string
  45:      *
  46:      * @author Oleg Schildt
  47:      */
  48:      protected string $use_fallback_language = "";
  49: 
  50:     /**
  51:      * Internal variable for storing the current context.
  52:      *
  53:      * @var string
  54:      *
  55:      * @see LanguageManager::getContext()
  56:      *
  57:      * @author Oleg Schildt
  58:      */
  59:     static protected string $context = "default";
  60: 
  61:     /**
  62:      * Internal variable for storing the state whether the dictionary is loaded or not.
  63:      *
  64:      * @var bool
  65:      *
  66:      * @author Oleg Schildt
  67:      */
  68:     protected bool $dictionary_loaded = false;
  69: 
  70:     /**
  71:      * Internal variable for storing the state whether the APCU should be used.
  72:      *
  73:      * @var bool
  74:      *
  75:      * @author Oleg Schildt
  76:      */
  77:     protected bool $use_apcu = false;
  78: 
  79:     /**
  80:      * Internal variable for storing the state whether the last selected language can be stored to cookie.
  81:      *
  82:      * @var bool
  83:      *
  84:      * @author Oleg Schildt
  85:      */
  86:     protected bool $use_cookie = false;
  87: 
  88:     /**
  89:      * Internal variable for storing the cookie path.
  90:      *
  91:      * @var string
  92:      *
  93:      * @author Oleg Schildt
  94:      */
  95:     protected string $cookie_path = "/";
  96: 
  97:     /**
  98:      * Internal variable for storing the state whether the E_USER_NOTICE is triggered in the case of missing translations.
  99:      *
 100:      * @var bool
 101:      *
 102:      * @author Oleg Schildt
 103:      */
 104:     protected bool $warn_missing = true;
 105: 
 106:     /**
 107:      * Internal array for storing the list of supported languages.
 108:      *
 109:      * @var array
 110:      *
 111:      * @author Oleg Schildt
 112:      */
 113:     static protected array $supported_languages = [];
 114: 
 115:     /**
 116:      * Internal variable for storing the current language.
 117:      *
 118:      * @var array
 119:      *
 120:      * @author Oleg Schildt
 121:      */
 122:     static protected array $current_language = [];
 123: 
 124:     /**
 125:      * Internal array for storing the list of language name translations.
 126:      *
 127:      * @var array
 128:      *
 129:      * @author Oleg Schildt
 130:      */
 131:     static protected array $languages = [];
 132: 
 133:     /**
 134:      * Internal array for storing the list of country name translations.
 135:      *
 136:      * @var array
 137:      *
 138:      * @author Oleg Schildt
 139:      */
 140:     static protected array $countries = [];
 141: 
 142:     /**
 143:      * Internal array for storing the list of text translations.
 144:      *
 145:      * @var array
 146:      *
 147:      * @author Oleg Schildt
 148:      */
 149:     static protected array $texts = [];
 150: 
 151:     /**
 152:      * Initializes the language manager with parameters.
 153:      *
 154:      * @param array $parameters
 155:      * Settings for logging as an associative array in the form key => value:
 156:      *
 157:      * - $parameters["localization_path"] - the path where the localization files are stored.
 158:      *
 159:      * - $parameters["use_cookie"] - if set to true, the last selected language is stored to cookie.
 160:      *
 161:      * - $parameters["use_fallback_language"] - if set and a translation is missing on a language, the translation on this language will be used.
 162:      *
 163:      * - $parameters["cookie_path"] - Cookie path.
 164:      *
 165:      * - $parameters["use_apcu"] - if installed, apcu can be used to cache the translations in the memory.
 166:      *
 167:      * - $parameters["warn_missing"] - If it is set to true, the E_USER_NOTICE is triggered in the case of missing translations.
 168:      *
 169:      * @return void
 170:      *
 171:      * @throws \Exception
 172:      * It might throw an exception in the case of any errors.
 173:      *
 174:      * @author Oleg Schildt
 175:      */
 176:     public function init(array $parameters): void
 177:     {
 178:         if (!empty($parameters["localization_path"])) {
 179:             $this->localization_path = $parameters["localization_path"];
 180:         }
 181: 
 182:         if (!empty($parameters["use_apcu"])) {
 183:             $this->use_apcu = $parameters["use_apcu"];
 184:         }
 185: 
 186:         if (!empty($parameters["use_cookie"])) {
 187:             $this->use_cookie = $parameters["use_cookie"];
 188:         }
 189: 
 190:         if (!empty($parameters["cookie_path"])) {
 191:             $this->cookie_path = $parameters["cookie_path"];
 192:         }
 193: 
 194:         if (!empty($parameters["use_fallback_language"])) {
 195:             $this->use_fallback_language = $parameters["use_fallback_language"];
 196:         }
 197: 
 198:         if (!empty($parameters["warn_missing"])) {
 199:             $this->warn_missing = $parameters["warn_missing"];
 200:         }
 201:     }
 202: 
 203:     /**
 204:      * Adds additional localization files to the dictionary.
 205:      *
 206:      * @param string $localization_file
 207:      * The path of the additional localization file.
 208:      *
 209:      * @return void
 210:      *
 211:      * @throws \Exception
 212:      * It might throw an exception in the case of any errors.
 213:      *
 214:      * @author Oleg Schildt
 215:      */
 216:     public function addLocalizationFile(string $localization_file): void
 217:     {
 218:         $this->additional_localization_files[] = $localization_file;
 219:     }
 220: 
 221:     /**
 222:      * This is function for loading the translations from the source JSON file.
 223:      *
 224:      * @return void
 225:      *
 226:      * @throws \Exception
 227:      * It might throw the following exceptions in the case of any errors:
 228:      *
 229:      * - if the translation file is invalid.
 230:      *
 231:      * @author Oleg Schildt
 232:      */
 233:     public function loadDictionary(): void
 234:     {
 235:         if ($this->dictionary_loaded) {
 236:             return;
 237:         }
 238: 
 239:         if ($this->use_apcu) {
 240:             do {
 241:                 if (!apcu_exists("dictionary_supported_languages")) {
 242:                     break;
 243:                 }
 244: 
 245:                 self::$supported_languages = apcu_fetch("dictionary_supported_languages");
 246:                 if (empty(self::$supported_languages)) {
 247:                     break;
 248:                 }
 249: 
 250:                 if (!apcu_exists("dictionary_languages")) {
 251:                     break;
 252:                 }
 253:                 self::$languages = apcu_fetch("dictionary_languages");
 254:                 if (empty(self::$languages)) {
 255:                     break;
 256:                 }
 257: 
 258:                 if (!apcu_exists("dictionary_countries")) {
 259:                     break;
 260:                 }
 261:                 self::$countries = apcu_fetch("dictionary_countries");
 262:                 if (empty(self::$countries)) {
 263:                     break;
 264:                 }
 265: 
 266:                 if (!apcu_exists("dictionary_texts")) {
 267:                     break;
 268:                 }
 269: 
 270:                 self::$texts = apcu_fetch("dictionary_texts");
 271:                 if (empty(self::$texts)) {
 272:                     break;
 273:                 }
 274: 
 275:                 return;
 276:             } while (false);
 277:         }
 278: 
 279:         $json_array = [];
 280: 
 281:         $json = file_get_contents($this->localization_path . "config.json");
 282:         if ($json === false) {
 283:             throw new \Exception("Translation file '" . $this->localization_path . "config.json" . "' cannot be loaded or does not exist!");
 284:         }
 285: 
 286:         try {
 287:             json_to_array($json, $json_array);
 288:         } catch (\Throwable $ex) {
 289:             throw new \Exception("Translation file '" . $this->localization_path . "config.json" . "' is invalid!" . "\n\n" . $ex->getMessage());
 290:         }
 291: 
 292:         $json = file_get_contents($this->localization_path . "languages.json");
 293:         if ($json === false) {
 294:             throw new \Exception("Translation file '" . $this->localization_path . "languages.json" . "' cannot be loaded or does not exist!");
 295:         }
 296: 
 297:         try {
 298:             $json_array["languages"] = [];
 299:             json_to_array($json, $json_array["languages"]);
 300:         } catch (\Throwable $ex) {
 301:             throw new \Exception("Translation file '" . $this->localization_path . "languages.json" . "' is invalid!" . "\n\n" . $ex->getMessage());
 302:         }
 303: 
 304:         $json = file_get_contents($this->localization_path . "countries.json");
 305:         if ($json === false) {
 306:             throw new \Exception("Translation file '" . $this->localization_path . "countries.json" . "' cannot be loaded or does not exist!");
 307:         }
 308: 
 309:         try {
 310:             $json_array["countries"] = [];
 311:             json_to_array($json, $json_array["countries"]);
 312:         } catch (\Throwable $ex) {
 313:             throw new \Exception("Translation file '" . $this->localization_path . "countries.json" . "' is invalid!" . "\n\n" . $ex->getMessage());
 314:         }
 315: 
 316:         $json = file_get_contents($this->localization_path . "texts.json");
 317:         if ($json === false) {
 318:             throw new \Exception("Translation file '" . $this->localization_path . "texts.json" . "' cannot be loaded or does not exist!");
 319:         }
 320: 
 321:         try {
 322:             $json_array["texts"] = [];
 323:             json_to_array($json, $json_array["texts"]);
 324:         } catch (\Throwable $ex) {
 325:             throw new \Exception("Translation file '" . $this->localization_path . "texts.json" . "' is invalid!" . "\n\n" . $ex->getMessage());
 326:         }
 327: 
 328:         if (!empty($this->additional_localization_files)) {
 329:             foreach ($this->additional_localization_files as $localization_file) {
 330:                 $json = file_get_contents($localization_file);
 331:                 if ($json === false) {
 332:                     throw new \Exception("Translation file '" . $localization_file . "' cannot be loaded or does not exist!");
 333:                 }
 334: 
 335:                 try {
 336:                     $translation_texts = [];
 337:                     json_to_array($json, $translation_texts);
 338: 
 339:                     $json_array["texts"] = array_merge($json_array["texts"], $translation_texts);
 340:                 } catch (\Throwable $ex) {
 341:                     throw new \Exception("Translation file '" . $localization_file . "' is invalid!" . "\n\n" . $ex->getMessage());
 342:                 }
 343:             }
 344:         }
 345: 
 346:         if (!empty($json_array["interface_languages"])) {
 347:             foreach ($json_array["interface_languages"] as $lang_code) {
 348:                 self::$supported_languages[$lang_code] = $lang_code;
 349:             }
 350:         }
 351: 
 352:         if (!empty($json_array["languages"])) {
 353:             foreach ($json_array["languages"] as $text_id =>&$translations) {
 354:                 foreach ($translations as $lang_code => $translation) {
 355:                     self::$languages[$lang_code][$text_id] = $translation;
 356:                 }
 357:             }
 358:         }
 359: 
 360:         if (!empty($json_array["countries"])) {
 361:             foreach ($json_array["countries"] as $text_id => $translations) {
 362:                 foreach ($translations as $lang_code => $translation) {
 363:                     self::$countries[$lang_code][$text_id] = $translation;
 364:                 }
 365:             }
 366:         }
 367: 
 368:         if (!empty($json_array["texts"])) {
 369:             foreach ($json_array["texts"] as $text_id => $translations) {
 370:                 foreach ($translations as $lang_code => $translation) {
 371:                     self::$texts[$lang_code][$text_id] = $translation;
 372:                 }
 373:             }
 374:         }
 375: 
 376:         $this->dictionary_loaded = true;
 377: 
 378:         if ($this->use_apcu) {
 379:             apcu_store("dictionary_supported_languages", self::$supported_languages);
 380:             apcu_store("dictionary_languages", self::$languages);
 381:             apcu_store("dictionary_countries", self::$countries);
 382:             apcu_store("dictionary_texts", self::$texts);
 383:         }
 384:     } // loadDictionary
 385: 
 386:     /**
 387:      * Adds additional translations to the dictionary.
 388:      *
 389:      * @param array $dictionary
 390:      * The array with additional translations.
 391:      *
 392:      * @return void
 393:      *
 394:      * @throws \Exception
 395:      * It might throw an exception in the case of any errors.
 396:      *
 397:      * @author Oleg Schildt
 398:      */
 399:     public function extendDictionary(array $dictionary): void
 400:     {
 401:         $this->loadDictionary();
 402:         
 403:         foreach ($dictionary as $text_id => $translations) {
 404:             foreach ($translations as $lang_code => $translation) {
 405:                 self::$texts[$lang_code][$text_id] = $translation;
 406:             }
 407:         }
 408:     }
 409: 
 410:     /**
 411:      * This function should detect the current language based on cookies, browser languages etc.
 412:      *
 413:      * Priority:
 414:      *
 415:      * 1. explicitly set by the request parameter language.
 416:      * 2. header 'Content-Language'.
 417:      * 3. last language in the cookie.
 418:      * 4. browser default language.
 419:      * 5. the first one from the supported list.
 420:      * 6. English.
 421:      *
 422:      * Some applications may consist of two parts - administration
 423:      * console and public site. A usual example is a CMS system.
 424:      *
 425:      * For example, you are using administration console in English
 426:      * and editing the public site for German and French.
 427:      * When you open the public site for preview in German or French,
 428:      * you want it to be open in the corresponding language, but
 429:      * the administration console should remain in English.
 430:      *
 431:      * With the help of $context, you are able to maintain different
 432:      * languages for different parts of your application.
 433:      * If you do not need the $context, just do not specify it.
 434:      *
 435:      * @return void
 436:      *
 437:      * @author Oleg Schildt
 438:      */
 439:     public function detectLanguage(): void
 440:     {
 441:         // Let's go
 442: 
 443:         // 6. English
 444:         $language = "en";
 445: 
 446:         // 5. the first one from the supported list
 447:         foreach (self::$supported_languages as $lng) {
 448:             $language = $lng;
 449:             break;
 450:         }
 451: 
 452:         // 4. browser default language
 453:         if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"]) && trim($_SERVER["HTTP_ACCEPT_LANGUAGE"]) != "") {
 454:             $accepted = explode(',', $_SERVER["HTTP_ACCEPT_LANGUAGE"]);
 455: 
 456:             foreach ($accepted as $name) {
 457:                 $code = explode(';', $name);
 458:                 // handle the cases like en-ca => en
 459:                 $code = explode("-", $code[0]);
 460: 
 461:                 if (!empty(self::$supported_languages[$code[0]])) {
 462:                     $language = $code[0];
 463:                     break;
 464:                 }
 465:             }
 466:         }
 467: 
 468:         // 3. last language in the cookie
 469:         $tmp = get_cookie(self::$context . "_language");
 470:         if (!empty($tmp) && !empty(self::$supported_languages[$tmp])) {
 471:             $language = $tmp;
 472:         }
 473: 
 474:         // 2. header 'Content-Language'.
 475:         $header = get_header("Content-Language");
 476:         if (!empty($header) && !empty(self::$supported_languages[$header])) {
 477:             $language = $header;
 478:         }
 479: 
 480:         // 1. explicitly set by request parameter language
 481:         if (!empty($_REQUEST["language"]) && !empty(self::$supported_languages[$_REQUEST["language"]])) {
 482:             $language = $_REQUEST["language"];
 483:         }
 484: 
 485:         $this->setCurrentLanguage($language);
 486:     } // detectLanguage
 487: 
 488:     /**
 489:      * Sets the current context.
 490:      *
 491:      * Some applications may consist of two parts - administration
 492:      * console and public site. A usual example is a CMS system.
 493:      *
 494:      * For example, you are using administration console in English
 495:      * and editing the public site for German and French.
 496:      * When you open the public site for preview in German or French,
 497:      * you want it to be open in the corresponding language, but
 498:      * the administration console should remain in English.
 499:      *
 500:      * With the help of $context, you are able to maintain different
 501:      * languages for different parts of your application.
 502:      * If you do not need the $context, just do not specify it.
 503:      *
 504:      * @param string $context
 505:      * The name of the context.
 506:      *
 507:      * @return void
 508:      *
 509:      * @see LanguageManager::getContext()
 510:      *
 511:      * @author Oleg Schildt
 512:      */
 513:     public function setContext(string $context): void
 514:     {
 515:         self::$context = $context;
 516:     } // setContext
 517: 
 518:     /**
 519:      * Returns the current context.
 520:      *
 521:      * Some applications may consist of two parts - administration
 522:      * console and public site. A usual example is a CMS system.
 523:      *
 524:      * For example, you are using administration console in English
 525:      * and editing the public site for German and French.
 526:      * When you open the public site for preview in German or French,
 527:      * you want it to be open in the corresponding language, but
 528:      * the administration console should remain in English.
 529:      *
 530:      * With the help of $context, you are able to maintain different
 531:      * languages for different parts of your application.
 532:      * If you do not need the $context, just do not specify it.
 533:      *
 534:      * @see LanguageManager::setContext()
 535:      *
 536:      * @return string
 537:      * Returns the current context.
 538:      *
 539:      * @author Oleg Schildt
 540:      */
 541:     public function getContext(): string
 542:     {
 543:         return self::$context;
 544:     } // getContext
 545: 
 546:     /**
 547:      * Returns the list of supported languages.
 548:      *
 549:      * @return array
 550:      * Returns the list of supported languages.
 551:      *
 552:      * @author Oleg Schildt
 553:      */
 554:     public function getSupportedLanguages(): array
 555:     {
 556:         return self::$supported_languages;
 557:     } // getSupportedLanguages
 558: 
 559:     /**
 560:      * Sets the current language.
 561:      *
 562:      * @param string $language
 563:      * The language ISO code to be set.
 564:      *
 565:      * @return void
 566:      *
 567:      * @see LanguageManager::getCurrentLanguage()
 568:      *
 569:      * @author Oleg Schildt
 570:      */
 571:     public function setCurrentLanguage(string $language): void
 572:     {
 573:         if (empty(self::$supported_languages[$language])) {
 574:             return;
 575:         }
 576: 
 577:         self::$current_language[self::$context] = $language;
 578: 
 579:         if ($this->use_cookie) {
 580:             set_cookie(self::$context . "_language", $language, time() + 365 * 24 * 3600, ["samesite" => "strict", "path" => $this->cookie_path]);
 581:         }
 582:     } // setCurrentLanguage
 583: 
 584:     /**
 585:      * Returns the fallback language. If set and a translation
 586:      * is missing on a language, the translation on this language will be used.
 587:      *
 588:      * @return string
 589:      * Returns the fallback language.
 590:      *
 591:      * @author Oleg Schildt
 592:      */
 593:     public function getFallbackLanguage(): string
 594:     {
 595:         return $this->use_fallback_language;
 596:     } // getFallbackLanguage
 597: 
 598:     /**
 599:      * Returns the current language.
 600:      *
 601:      * @return string
 602:      * Returns the current language ISO code.
 603:      *
 604:      * @see LanguageManager::setCurrentLanguage()
 605:      *
 606:      * @author Oleg Schildt
 607:      */
 608:     public function getCurrentLanguage(): string
 609:     {
 610:         if (empty(self::$current_language[self::$context])) {
 611:             foreach (self::$supported_languages as $lng) {
 612:                 self::$current_language[self::$context] = $lng;
 613:                 break;
 614:             }
 615:         }
 616: 
 617:         return self::$current_language[self::$context];
 618:     } // getCurrentLanguage
 619: 
 620:     /**
 621:      * Provides the text translation for the text ID for the given language.
 622:      *
 623:      * @param string $text_id
 624:      * Text ID
 625:      *
 626:      * @param string $lng
 627:      * The language. If it is not specified,
 628:      * the default language is used.
 629:      *
 630:      * @param string $default_text
 631:      * The default text to be used if there is no translation.
 632:      *
 633:      * @return string
 634:      * Returns the translation text or the $default_text/$text_id if no translation
 635:      * is found.
 636:      *
 637:      * @throws \Exception
 638:      * It might throw exceptions in the case of any errors.
 639:      *
 640:      * @author Oleg Schildt
 641:      */
 642:     public function text(string $text_id, string $lng = "", string $default_text = ""): string
 643:     {
 644:         if (empty($lng)) {
 645:             $lng = $this->getCurrentLanguage();
 646:         }
 647: 
 648:         if (!$this->hasTranslation($text_id, $lng)) {
 649:             if ($this->warn_missing) {
 650:                 trigger_error("No translation for the text '$text_id' in the language [$lng]!", E_USER_WARNING);
 651:             }
 652: 
 653:             if (empty($default_text)) {
 654:                 if (!empty($this->use_fallback_language)) {
 655:                     return $this->try_text($text_id, $this->use_fallback_language);
 656:                 }
 657:               
 658:                 return $text_id;
 659:             } else {
 660:                 return $default_text;
 661:             }
 662:         }
 663: 
 664:         return self::$texts[$lng][$text_id];
 665:     } // text
 666: 
 667:     /**
 668:      * Provides the text translation for the text ID for the given language if the translation exists.
 669:      * Otherwise, it returns the text ID and emits no warning.
 670:      *
 671:      * @param string $text_id
 672:      * Text ID
 673:      *
 674:      * @param string $lng
 675:      * The language. If it is not specified,
 676:      * the default language is used.
 677:      *
 678:      * @return string
 679:      * Returns the translation text or the $default_text/$text_id if no translation
 680:      * is found.
 681:      *
 682:      * @throws \Exception
 683:      * It might throw exceptions in the case of any errors.
 684:      *
 685:      * @author Oleg Schildt
 686:      */
 687:     public function try_text(string $text_id, string $lng = ""): string
 688:     {
 689:         if (!$this->hasTranslation($text_id, $lng)) {
 690:             if (!empty($this->use_fallback_language) && $this->hasTranslation($text_id, $this->use_fallback_language)) {
 691:                 return text($text_id, $this->use_fallback_language);
 692:             }
 693:             
 694:             return $text_id;
 695:         }
 696:         
 697:         return text($text_id, $lng);
 698:     } // try_text
 699: 
 700:     /**
 701:      * Checks whether the text translation for the text ID for the given language exists.
 702:      *
 703:      * @param string $text_id
 704:      * Text ID
 705:      *
 706:      * @param string $lng
 707:      * The language. If it is not specified,
 708:      * the default language is used.
 709:      *
 710:      * @return bool
 711:      * Returns true if the translation exists, otherwise false.
 712:      *
 713:      * @author Oleg Schildt
 714:      */
 715:     public function hasTranslation(string $text_id, string $lng = ""): bool
 716:     {
 717:         if (empty($lng)) {
 718:             $lng = $this->getCurrentLanguage();
 719:         }
 720: 
 721:         return !empty(self::$texts[$lng][$text_id]);
 722:     } // hasTranslation
 723: 
 724:     /**
 725:      * Provides the text translation for the language name by the code
 726:      * for the given language.
 727:      *
 728:      * @param string $code
 729:      * Language ISO code (lowercase, e.g. en, de, fr).
 730:      *
 731:      * @param string $lng
 732:      * The language. If it is not specified,
 733:      * the default language is used.
 734:      *
 735:      * @return string
 736:      * Returns the translation text for the language name or the $code if no translation
 737:      * is found.
 738:      *
 739:      * @see LanguageManager::getLanguageCode()
 740:      * @see LanguageManager::validateLanguageCode()
 741:      * @see LanguageManager::getLanguageList()
 742:      * @see LanguageManager::getCountryName()
 743:      *
 744:      * @author Oleg Schildt
 745:      */
 746:     public function getLanguageName(string $code, string $lng = ""): string
 747:     {
 748:         if (empty($lng)) {
 749:             $lng = $this->getCurrentLanguage();
 750:         }
 751: 
 752:         if (empty(self::$languages[$lng][$code])) {
 753:             if ($this->warn_missing) {
 754:                 trigger_error("No translation for the language name [$code] in the language [$lng]!", E_USER_WARNING);
 755:             }
 756:             return $code;
 757:         }
 758: 
 759:         return self::$languages[$lng][$code];
 760:     } // getLanguageName
 761: 
 762:     /**
 763:      * Tries to find the language code by the given name.
 764:      *
 765:      * @param string $lang_name
 766:      * The name of the language in any supported language.
 767:      *
 768:      * @return string
 769:      * Returns the language code if it could be found, otherwise an empty string.
 770:      *
 771:      * @see LanguageManager::getLanguageName()
 772:      * @see LanguageManager::validateLanguageCode()
 773:      * @see LanguageManager::getLanguageList()
 774:      * @see LanguageManager::getCountryCode()
 775:      *
 776:      * @author Oleg Schildt
 777:      */
 778:     public function getLanguageCode(string $lang_name): string
 779:     {
 780:         foreach (self::$supported_languages as $lng) {
 781:             if (empty(self::$languages[$lng])) {
 782:                 continue;
 783:             }
 784: 
 785:             foreach (self::$languages[$lng] as $code => $translation) {
 786:                 if (strcasecmp($lang_name, $translation) == 0) {
 787:                     return $code;
 788:                 }
 789:             } // foreach
 790:         } // foreach
 791: 
 792:         return "";
 793:     } // getLanguageCode
 794: 
 795:     /**
 796:      * Checks whether the language code is valid (has translation).
 797:      *
 798:      * @param string $code
 799:      * Language ISO code (lowercase, e.g. en, de, fr).
 800:      *
 801:      * @param string $lng
 802:      * The language. If it is not specified,
 803:      * the default language is used.
 804:      *
 805:      * @return bool
 806:      * Returns true if the language code is valid (has translation), otherwise false.
 807:      *
 808:      * @see LanguageManager::getLanguageName()
 809:      * @see LanguageManager::getLanguageCode()
 810:      * @see LanguageManager::getLanguageList()
 811:      * @see LanguageManager::validateCountryCode()
 812:      *
 813:      * @author Oleg Schildt
 814:      */
 815:     public function validateLanguageCode(string $code, string $lng = ""): bool
 816:     {
 817:         if (empty($lng)) {
 818:             $lng = $this->getCurrentLanguage();
 819:         }
 820: 
 821:         return !empty(self::$languages[$lng][$code]);
 822:     } // validateLanguageCode
 823: 
 824:     /**
 825:      * Provides the list of languages for the given language in the form "code" => "translation".
 826:      *
 827:      * @param array &$language_list
 828:      * Target array where the language list should be loaded.
 829:      *
 830:      * @param string $lng
 831:      * The language. If it is not specified,
 832:      * the default language is used.
 833:      *
 834:      * @param array $display_first
 835:      * List of the language codes to be displayed first in the order, they appear in the list.
 836:      *
 837:      * @return bool
 838:      * Returns true if the language list is successfully retrieved, otherwise false.
 839:      *
 840:      * @see LanguageManager::getLanguageName()
 841:      * @see LanguageManager::getLanguageCode()
 842:      * @see LanguageManager::validateLanguageCode()
 843:      * @see LanguageManager::getCountryList()
 844:      *
 845:      * @author Oleg Schildt
 846:      */
 847:     public function getLanguageList(array &$language_list, string $lng = "", array $display_first = []): bool
 848:     {
 849:         if (empty($lng)) {
 850:             $lng = $this->getCurrentLanguage();
 851:         }
 852: 
 853:         if (empty(self::$languages[$lng])) {
 854:             return false;
 855:         }
 856: 
 857:         $language_list = array_flip($display_first);
 858: 
 859:         asort(self::$languages[$lng], SORT_LOCALE_STRING);
 860: 
 861:         foreach (self::$languages[$lng] as $code => $name) {
 862:             $language_list[$code] = $name;
 863:         }
 864: 
 865:         return true;
 866:     } // getLanguageList
 867: 
 868:     /**
 869:      * Provides the text translation for the country name by the code
 870:      * for the given language.
 871:      *
 872:      * @param string $code
 873:      * Country ISO code (uppercase, e.g. US, DE, FR).
 874:      *
 875:      * @param string $lng
 876:      * The language. If it is not specified,
 877:      * the default language is used.
 878:      *
 879:      * @return string
 880:      * Returns the translation text for the country name or the $code if no translation
 881:      * is found.
 882:      *
 883:      * @see LanguageManager::getCountryCode()
 884:      * @see LanguageManager::validateCountryCode()
 885:      * @see LanguageManager::getCountryList()
 886:      * @see LanguageManager::getLanguageName()
 887:      *
 888:      * @author Oleg Schildt
 889:      */
 890:     public function getCountryName(string $code, string $lng = ""): string
 891:     {
 892:         if (empty($lng)) {
 893:             $lng = $this->getCurrentLanguage();
 894:         }
 895: 
 896:         if (empty(self::$countries[$lng][$code])) {
 897:             if ($this->warn_missing) {
 898:                 trigger_error("No translation for the country name [$code] in the language [$lng]!", E_USER_WARNING);
 899:             }
 900:             return $code;
 901:         }
 902: 
 903:         return self::$countries[$lng][$code];
 904:     } // getCountryName
 905: 
 906:     /**
 907:      * Tries to find the country code by the given name.
 908:      *
 909:      * @param string $country_name
 910:      * The name of the country in any supported language.
 911:      *
 912:      * @return string
 913:      * Returns the country code if it could be found, otherwise an empty string.
 914:      *
 915:      * @see LanguageManager::getCountryName()
 916:      * @see LanguageManager::validateCountryCode()
 917:      * @see LanguageManager::getCountryList()
 918:      * @see LanguageManager::getLanguageCode()
 919:      *
 920:      * @author Oleg Schildt
 921:      */
 922:     public function getCountryCode(string $country_name): string
 923:     {
 924:         foreach (self::$supported_languages as $lng) {
 925:             if (empty(self::$countries[$lng])) {
 926:                 continue;
 927:             }
 928: 
 929:             foreach (self::$countries[$lng] as $code => $translation) {
 930:                 if (strcasecmp($country_name, $translation) == 0) {
 931:                     return $code;
 932:                 }
 933:             } // foreach
 934:         } // foreach
 935: 
 936:         return "";
 937:     } // getCountryCode
 938: 
 939:     /**
 940:      * Checks whether the country code is valid (has translation).
 941:      *
 942:      * @param string $code
 943:      * Country ISO code (uppercase, e.g. US, DE, FR).
 944:      *
 945:      * @param string $lng
 946:      * The language. If it is not specified,
 947:      * the default language is used.
 948:      *
 949:      * @return bool
 950:      * Returns true if the country code is valid (has translation), otherwise false.
 951:      *
 952:      * @see LanguageManager::getCountryName()
 953:      * @see LanguageManager::getCountryCode()
 954:      * @see LanguageManager::getCountryList()
 955:      * @see LanguageManager::validateLanguageCode()
 956:      *
 957:      * @author Oleg Schildt
 958:      */
 959:     public function validateCountryCode(string $code, string $lng = ""): bool
 960:     {
 961:         if (empty($lng)) {
 962:             $lng = $this->getCurrentLanguage();
 963:         }
 964: 
 965:         return !empty(self::$countries[$lng][$code]);
 966:     } // validateCountryCode
 967: 
 968:     /**
 969:      * Provides the list of countries for the given language in the form "code" => "translation".
 970:      *
 971:      * @param array &$country_list
 972:      * Target array where the country list should be loaded.
 973:      *
 974:      * @param string $lng
 975:      * The language. If it is not specified,
 976:      * the default language is used.
 977:      *
 978:      * @param array $display_first
 979:      * List of the country codes to be displayed first in the order, they appear in the list.
 980:      *
 981:      * @return bool
 982:      * Returns true if the country list is successfully retrieved, otherwise false.
 983:      *
 984:      * @see LanguageManager::getCountryName()
 985:      * @see LanguageManager::getCountryCode()
 986:      * @see LanguageManager::validateCountryCode()
 987:      * @see LanguageManager::getLanguageList()
 988:      *
 989:      * @author Oleg Schildt
 990:      */
 991:     public function getCountryList(array &$country_list, string $lng = "", array $display_first = []): bool
 992:     {
 993:         if (empty($lng)) {
 994:             $lng = $this->getCurrentLanguage();
 995:         }
 996: 
 997:         if (empty(self::$countries[$lng])) {
 998:             return false;
 999:         }
1000: 
1001:         $country_list = array_flip($display_first);
1002: 
1003:         asort(self::$countries[$lng], SORT_LOCALE_STRING);
1004: 
1005:         foreach (self::$countries[$lng] as $code => $name) {
1006:             $country_list[$code] = $name;
1007:         }
1008: 
1009:         return true;
1010:     } // getCountryList
1011: } // LanguageManager