vendor/symfony/translation/Translator.php line 274

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Translation;
  11. use Symfony\Component\Config\ConfigCacheFactory;
  12. use Symfony\Component\Config\ConfigCacheFactoryInterface;
  13. use Symfony\Component\Config\ConfigCacheInterface;
  14. use Symfony\Component\Translation\Exception\InvalidArgumentException;
  15. use Symfony\Component\Translation\Exception\NotFoundResourceException;
  16. use Symfony\Component\Translation\Exception\RuntimeException;
  17. use Symfony\Component\Translation\Formatter\IntlFormatterInterface;
  18. use Symfony\Component\Translation\Formatter\MessageFormatter;
  19. use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
  20. use Symfony\Component\Translation\Loader\LoaderInterface;
  21. use Symfony\Contracts\Translation\LocaleAwareInterface;
  22. use Symfony\Contracts\Translation\TranslatableInterface;
  23. use Symfony\Contracts\Translation\TranslatorInterface;
  24. // Help opcache.preload discover always-needed symbols
  25. class_exists(MessageCatalogue::class);
  26. /**
  27.  * @author Fabien Potencier <fabien@symfony.com>
  28.  */
  29. class Translator implements TranslatorInterfaceTranslatorBagInterfaceLocaleAwareInterface
  30. {
  31.     /**
  32.      * @var MessageCatalogueInterface[]
  33.      */
  34.     protected $catalogues = [];
  35.     private string $locale;
  36.     /**
  37.      * @var string[]
  38.      */
  39.     private array $fallbackLocales = [];
  40.     /**
  41.      * @var LoaderInterface[]
  42.      */
  43.     private array $loaders = [];
  44.     private array $resources = [];
  45.     private MessageFormatterInterface $formatter;
  46.     private ?string $cacheDir;
  47.     private bool $debug;
  48.     private array $cacheVary;
  49.     private ?ConfigCacheFactoryInterface $configCacheFactory;
  50.     private array $parentLocales;
  51.     private bool $hasIntlFormatter;
  52.     /**
  53.      * @throws InvalidArgumentException If a locale contains invalid characters
  54.      */
  55.     public function __construct(string $localeMessageFormatterInterface $formatter nullstring $cacheDir nullbool $debug false, array $cacheVary = [])
  56.     {
  57.         $this->setLocale($locale);
  58.         $this->formatter $formatter ??= new MessageFormatter();
  59.         $this->cacheDir $cacheDir;
  60.         $this->debug $debug;
  61.         $this->cacheVary $cacheVary;
  62.         $this->hasIntlFormatter $formatter instanceof IntlFormatterInterface;
  63.     }
  64.     public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
  65.     {
  66.         $this->configCacheFactory $configCacheFactory;
  67.     }
  68.     /**
  69.      * Adds a Loader.
  70.      *
  71.      * @param string $format The name of the loader (@see addResource())
  72.      */
  73.     public function addLoader(string $formatLoaderInterface $loader)
  74.     {
  75.         $this->loaders[$format] = $loader;
  76.     }
  77.     /**
  78.      * Adds a Resource.
  79.      *
  80.      * @param string $format   The name of the loader (@see addLoader())
  81.      * @param mixed  $resource The resource name
  82.      *
  83.      * @throws InvalidArgumentException If the locale contains invalid characters
  84.      */
  85.     public function addResource(string $formatmixed $resourcestring $localestring $domain null)
  86.     {
  87.         $domain ??= 'messages';
  88.         $this->assertValidLocale($locale);
  89.         $locale ?: $locale class_exists(\Locale::class) ? \Locale::getDefault() : 'en';
  90.         $this->resources[$locale][] = [$format$resource$domain];
  91.         if (\in_array($locale$this->fallbackLocales)) {
  92.             $this->catalogues = [];
  93.         } else {
  94.             unset($this->catalogues[$locale]);
  95.         }
  96.     }
  97.     public function setLocale(string $locale)
  98.     {
  99.         $this->assertValidLocale($locale);
  100.         $this->locale $locale;
  101.     }
  102.     public function getLocale(): string
  103.     {
  104.         return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en');
  105.     }
  106.     /**
  107.      * Sets the fallback locales.
  108.      *
  109.      * @param string[] $locales
  110.      *
  111.      * @throws InvalidArgumentException If a locale contains invalid characters
  112.      */
  113.     public function setFallbackLocales(array $locales)
  114.     {
  115.         // needed as the fallback locales are linked to the already loaded catalogues
  116.         $this->catalogues = [];
  117.         foreach ($locales as $locale) {
  118.             $this->assertValidLocale($locale);
  119.         }
  120.         $this->fallbackLocales $this->cacheVary['fallback_locales'] = $locales;
  121.     }
  122.     /**
  123.      * Gets the fallback locales.
  124.      *
  125.      * @internal
  126.      */
  127.     public function getFallbackLocales(): array
  128.     {
  129.         return $this->fallbackLocales;
  130.     }
  131.     public function trans(?string $id, array $parameters = [], string $domain nullstring $locale null): string
  132.     {
  133.         if (null === $id || '' === $id) {
  134.             return '';
  135.         }
  136.         $domain ??= 'messages';
  137.         $catalogue $this->getCatalogue($locale);
  138.         $locale $catalogue->getLocale();
  139.         while (!$catalogue->defines($id$domain)) {
  140.             if ($cat $catalogue->getFallbackCatalogue()) {
  141.                 $catalogue $cat;
  142.                 $locale $catalogue->getLocale();
  143.             } else {
  144.                 break;
  145.             }
  146.         }
  147.         $parameters array_map(function ($parameter) use ($locale) {
  148.             return $parameter instanceof TranslatableInterface $parameter->trans($this$locale) : $parameter;
  149.         }, $parameters);
  150.         $len \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX);
  151.         if ($this->hasIntlFormatter
  152.             && ($catalogue->defines($id$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)
  153.             || (\strlen($domain) > $len && === substr_compare($domainMessageCatalogue::INTL_DOMAIN_SUFFIX, -$len$len)))
  154.         ) {
  155.             return $this->formatter->formatIntl($catalogue->get($id$domain), $locale$parameters);
  156.         }
  157.         return $this->formatter->format($catalogue->get($id$domain), $locale$parameters);
  158.     }
  159.     public function getCatalogue(string $locale null): MessageCatalogueInterface
  160.     {
  161.         if (!$locale) {
  162.             $locale $this->getLocale();
  163.         } else {
  164.             $this->assertValidLocale($locale);
  165.         }
  166.         if (!isset($this->catalogues[$locale])) {
  167.             $this->loadCatalogue($locale);
  168.         }
  169.         return $this->catalogues[$locale];
  170.     }
  171.     public function getCatalogues(): array
  172.     {
  173.         return array_values($this->catalogues);
  174.     }
  175.     /**
  176.      * Gets the loaders.
  177.      *
  178.      * @return LoaderInterface[]
  179.      */
  180.     protected function getLoaders(): array
  181.     {
  182.         return $this->loaders;
  183.     }
  184.     protected function loadCatalogue(string $locale)
  185.     {
  186.         if (null === $this->cacheDir) {
  187.             $this->initializeCatalogue($locale);
  188.         } else {
  189.             $this->initializeCacheCatalogue($locale);
  190.         }
  191.     }
  192.     protected function initializeCatalogue(string $locale)
  193.     {
  194.         $this->assertValidLocale($locale);
  195.         try {
  196.             $this->doLoadCatalogue($locale);
  197.         } catch (NotFoundResourceException $e) {
  198.             if (!$this->computeFallbackLocales($locale)) {
  199.                 throw $e;
  200.             }
  201.         }
  202.         $this->loadFallbackCatalogues($locale);
  203.     }
  204.     private function initializeCacheCatalogue(string $locale): void
  205.     {
  206.         if (isset($this->catalogues[$locale])) {
  207.             /* Catalogue already initialized. */
  208.             return;
  209.         }
  210.         $this->assertValidLocale($locale);
  211.         $cache $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale),
  212.             function (ConfigCacheInterface $cache) use ($locale) {
  213.                 $this->dumpCatalogue($locale$cache);
  214.             }
  215.         );
  216.         if (isset($this->catalogues[$locale])) {
  217.             /* Catalogue has been initialized as it was written out to cache. */
  218.             return;
  219.         }
  220.         /* Read catalogue from cache. */
  221.         $this->catalogues[$locale] = include $cache->getPath();
  222.     }
  223.     private function dumpCatalogue(string $localeConfigCacheInterface $cache): void
  224.     {
  225.         $this->initializeCatalogue($locale);
  226.         $fallbackContent $this->getFallbackContent($this->catalogues[$locale]);
  227.         $content sprintf(<<<EOF
  228. <?php
  229. use Symfony\Component\Translation\MessageCatalogue;
  230. \$catalogue = new MessageCatalogue('%s', %s);
  231. %s
  232. return \$catalogue;
  233. EOF
  234.             ,
  235.             $locale,
  236.             var_export($this->getAllMessages($this->catalogues[$locale]), true),
  237.             $fallbackContent
  238.         );
  239.         $cache->write($content$this->catalogues[$locale]->getResources());
  240.     }
  241.     private function getFallbackContent(MessageCatalogue $catalogue): string
  242.     {
  243.         $fallbackContent '';
  244.         $current '';
  245.         $replacementPattern '/[^a-z0-9_]/i';
  246.         $fallbackCatalogue $catalogue->getFallbackCatalogue();
  247.         while ($fallbackCatalogue) {
  248.             $fallback $fallbackCatalogue->getLocale();
  249.             $fallbackSuffix ucfirst(preg_replace($replacementPattern'_'$fallback));
  250.             $currentSuffix ucfirst(preg_replace($replacementPattern'_'$current));
  251.             $fallbackContent .= sprintf(<<<'EOF'
  252. $catalogue%s = new MessageCatalogue('%s', %s);
  253. $catalogue%s->addFallbackCatalogue($catalogue%s);
  254. EOF
  255.                 ,
  256.                 $fallbackSuffix,
  257.                 $fallback,
  258.                 var_export($this->getAllMessages($fallbackCatalogue), true),
  259.                 $currentSuffix,
  260.                 $fallbackSuffix
  261.             );
  262.             $current $fallbackCatalogue->getLocale();
  263.             $fallbackCatalogue $fallbackCatalogue->getFallbackCatalogue();
  264.         }
  265.         return $fallbackContent;
  266.     }
  267.     private function getCatalogueCachePath(string $locale): string
  268.     {
  269.         return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256'serialize($this->cacheVary), true)), 07), '/''_').'.php';
  270.     }
  271.     /**
  272.      * @internal
  273.      */
  274.     protected function doLoadCatalogue(string $locale): void
  275.     {
  276.         $this->catalogues[$locale] = new MessageCatalogue($locale);
  277.         if (isset($this->resources[$locale])) {
  278.             foreach ($this->resources[$locale] as $resource) {
  279.                 if (!isset($this->loaders[$resource[0]])) {
  280.                     if (\is_string($resource[1])) {
  281.                         throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.'$resource[0], $resource[1]));
  282.                     }
  283.                     throw new RuntimeException(sprintf('No loader is registered for the "%s" format.'$resource[0]));
  284.                 }
  285.                 $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale$resource[2]));
  286.             }
  287.         }
  288.     }
  289.     private function loadFallbackCatalogues(string $locale): void
  290.     {
  291.         $current $this->catalogues[$locale];
  292.         foreach ($this->computeFallbackLocales($locale) as $fallback) {
  293.             if (!isset($this->catalogues[$fallback])) {
  294.                 $this->initializeCatalogue($fallback);
  295.             }
  296.             $fallbackCatalogue = new MessageCatalogue($fallback$this->getAllMessages($this->catalogues[$fallback]));
  297.             foreach ($this->catalogues[$fallback]->getResources() as $resource) {
  298.                 $fallbackCatalogue->addResource($resource);
  299.             }
  300.             $current->addFallbackCatalogue($fallbackCatalogue);
  301.             $current $fallbackCatalogue;
  302.         }
  303.     }
  304.     protected function computeFallbackLocales(string $locale)
  305.     {
  306.         $this->parentLocales ??= json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true);
  307.         $originLocale $locale;
  308.         $locales = [];
  309.         while ($locale) {
  310.             $parent $this->parentLocales[$locale] ?? null;
  311.             if ($parent) {
  312.                 $locale 'root' !== $parent $parent null;
  313.             } elseif (\function_exists('locale_parse')) {
  314.                 $localeSubTags locale_parse($locale);
  315.                 $locale null;
  316.                 if (\count($localeSubTags)) {
  317.                     array_pop($localeSubTags);
  318.                     $locale locale_compose($localeSubTags) ?: null;
  319.                 }
  320.             } elseif ($i strrpos($locale'_') ?: strrpos($locale'-')) {
  321.                 $locale substr($locale0$i);
  322.             } else {
  323.                 $locale null;
  324.             }
  325.             if (null !== $locale) {
  326.                 $locales[] = $locale;
  327.             }
  328.         }
  329.         foreach ($this->fallbackLocales as $fallback) {
  330.             if ($fallback === $originLocale) {
  331.                 continue;
  332.             }
  333.             $locales[] = $fallback;
  334.         }
  335.         return array_unique($locales);
  336.     }
  337.     /**
  338.      * Asserts that the locale is valid, throws an Exception if not.
  339.      *
  340.      * @throws InvalidArgumentException If the locale contains invalid characters
  341.      */
  342.     protected function assertValidLocale(string $locale)
  343.     {
  344.         if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i'$locale)) {
  345.             throw new InvalidArgumentException(sprintf('Invalid "%s" locale.'$locale));
  346.         }
  347.     }
  348.     /**
  349.      * Provides the ConfigCache factory implementation, falling back to a
  350.      * default implementation if necessary.
  351.      */
  352.     private function getConfigCacheFactory(): ConfigCacheFactoryInterface
  353.     {
  354.         $this->configCacheFactory ??= new ConfigCacheFactory($this->debug);
  355.         return $this->configCacheFactory;
  356.     }
  357.     private function getAllMessages(MessageCatalogueInterface $catalogue): array
  358.     {
  359.         $allMessages = [];
  360.         foreach ($catalogue->all() as $domain => $messages) {
  361.             if ($intlMessages $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
  362.                 $allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages;
  363.                 $messages array_diff_key($messages$intlMessages);
  364.             }
  365.             if ($messages) {
  366.                 $allMessages[$domain] = $messages;
  367.             }
  368.         }
  369.         return $allMessages;
  370.     }
  371. }