vendor/symfony/translation/Translator.php line 202

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