vendor/symfony/translation/Translator.php line 207

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