vendor/symfony/translation/Translator.php line 130

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