vendor/shopware/core/Framework/Adapter/Translation/Translator.php line 120

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Adapter\Translation;
  3. use Doctrine\DBAL\Exception\ConnectionException;
  4. use Shopware\Core\Defaults;
  5. use Shopware\Core\Framework\Context;
  6. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  9. use Shopware\Core\Framework\Log\Package;
  10. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  11. use Shopware\Core\PlatformRequest;
  12. use Shopware\Core\SalesChannelRequest;
  13. use Shopware\Core\System\Locale\LanguageLocaleCodeProvider;
  14. use Shopware\Core\System\Snippet\SnippetService;
  15. use Symfony\Component\HttpFoundation\RequestStack;
  16. use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
  17. use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
  18. use Symfony\Component\Translation\MessageCatalogueInterface;
  19. use Symfony\Component\Translation\Translator as SymfonyTranslator;
  20. use Symfony\Component\Translation\TranslatorBagInterface;
  21. use Symfony\Contracts\Cache\CacheInterface;
  22. use Symfony\Contracts\Cache\ItemInterface;
  23. use Symfony\Contracts\Translation\LocaleAwareInterface;
  24. use Symfony\Contracts\Translation\TranslatorInterface;
  25. use Symfony\Contracts\Translation\TranslatorTrait;
  26. #[Package('core')]
  27. class Translator extends AbstractTranslator
  28. {
  29.     use TranslatorTrait;
  30.     /**
  31.      * @var TranslatorInterface|TranslatorBagInterface|WarmableInterface
  32.      */
  33.     private $translator;
  34.     private RequestStack $requestStack;
  35.     private CacheInterface $cache;
  36.     /**
  37.      * @var array<string, MessageCatalogueInterface>
  38.      */
  39.     private array $isCustomized = [];
  40.     private MessageFormatterInterface $formatter;
  41.     private SnippetService $snippetService;
  42.     private ?string $snippetSetId null;
  43.     private ?string $salesChannelId null;
  44.     private ?string $localeBeforeInject null;
  45.     private string $environment;
  46.     /**
  47.      * @var array<string, bool>
  48.      */
  49.     private array $keys = ['all' => true];
  50.     /**
  51.      * @var array<string, array<string, bool>>
  52.      */
  53.     private array $traces = [];
  54.     private EntityRepositoryInterface $snippetSetRepository;
  55.     /**
  56.      * @var array<string, string>
  57.      */
  58.     private array $snippets = [];
  59.     private LanguageLocaleCodeProvider $languageLocaleProvider;
  60.     /**
  61.      * @internal
  62.      */
  63.     public function __construct(
  64.         TranslatorInterface $translator,
  65.         RequestStack $requestStack,
  66.         CacheInterface $cache,
  67.         MessageFormatterInterface $formatter,
  68.         SnippetService $snippetService,
  69.         string $environment,
  70.         EntityRepositoryInterface $snippetSetRepository,
  71.         LanguageLocaleCodeProvider $languageLocaleProvider
  72.     ) {
  73.         $this->translator $translator;
  74.         $this->requestStack $requestStack;
  75.         $this->cache $cache;
  76.         $this->formatter $formatter;
  77.         $this->snippetService $snippetService;
  78.         $this->environment $environment;
  79.         $this->snippetSetRepository $snippetSetRepository;
  80.         $this->languageLocaleProvider $languageLocaleProvider;
  81.     }
  82.     public static function buildName(string $id): string
  83.     {
  84.         return 'translator.' $id;
  85.     }
  86.     public function getDecorated(): AbstractTranslator
  87.     {
  88.         throw new DecorationPatternException(self::class);
  89.     }
  90.     /**
  91.      * @return mixed|null All kind of data could be cached
  92.      */
  93.     public function trace(string $key, \Closure $param)
  94.     {
  95.         $this->traces[$key] = [];
  96.         $this->keys[$key] = true;
  97.         $result $param();
  98.         unset($this->keys[$key]);
  99.         return $result;
  100.     }
  101.     /**
  102.      * @return array<int, string>
  103.      */
  104.     public function getTrace(string $key): array
  105.     {
  106.         $trace = isset($this->traces[$key]) ? array_keys($this->traces[$key]) : [];
  107.         unset($this->traces[$key]);
  108.         return $trace;
  109.     }
  110.     /**
  111.      * {@inheritdoc}
  112.      */
  113.     public function getCatalogue(?string $locale null): MessageCatalogueInterface
  114.     {
  115.         \assert($this->translator instanceof TranslatorBagInterface);
  116.         $catalog $this->translator->getCatalogue($locale);
  117.         $fallbackLocale $this->getFallbackLocale();
  118.         $localization mb_substr($fallbackLocale02);
  119.         if ($this->isShopwareLocaleCatalogue($catalog) && !$this->isFallbackLocaleCatalogue($catalog$localization)) {
  120.             $catalog->addFallbackCatalogue($this->translator->getCatalogue($localization));
  121.         } else {
  122.             //fallback locale and current locale has the same localization -> reset fallback
  123.             // or locale is symfony style locale so we shouldn't add shopware fallbacks as it may lead to circular references
  124.             $fallbackLocale null;
  125.         }
  126.         // disable fallback logic to display symfony warnings
  127.         if ($this->environment !== 'prod') {
  128.             $fallbackLocale null;
  129.         }
  130.         return $this->getCustomizedCatalog($catalog$fallbackLocale$locale);
  131.     }
  132.     /**
  133.      * @param array<string, string> $parameters
  134.      */
  135.     public function trans($id, array $parameters = [], ?string $domain null, ?string $locale null): string
  136.     {
  137.         if ($domain === null) {
  138.             $domain 'messages';
  139.         }
  140.         foreach (array_keys($this->keys) as $trace) {
  141.             $this->traces[$trace][self::buildName($id)] = true;
  142.         }
  143.         return $this->formatter->format($this->getCatalogue($locale)->get($id$domain), $locale ?? $this->getFallbackLocale(), $parameters);
  144.     }
  145.     /**
  146.      * {@inheritdoc}
  147.      */
  148.     public function setLocale($locale): void
  149.     {
  150.         \assert($this->translator instanceof LocaleAwareInterface);
  151.         $this->translator->setLocale($locale);
  152.     }
  153.     /**
  154.      * {@inheritdoc}
  155.      */
  156.     public function getLocale(): string
  157.     {
  158.         \assert($this->translator instanceof LocaleAwareInterface);
  159.         return $this->translator->getLocale();
  160.     }
  161.     /**
  162.      * @param string $cacheDir
  163.      */
  164.     public function warmUp($cacheDir): void
  165.     {
  166.         if ($this->translator instanceof WarmableInterface) {
  167.             $this->translator->warmUp($cacheDir);
  168.         }
  169.     }
  170.     public function resetInMemoryCache(): void
  171.     {
  172.         $this->isCustomized = [];
  173.         $this->snippetSetId null;
  174.         if ($this->translator instanceof SymfonyTranslator) {
  175.             // Reset FallbackLocale in memory cache of symfony implementation
  176.             // set fallback values from Framework/Resources/config/translation.yaml
  177.             $this->translator->setFallbackLocales(['en_GB''en']);
  178.         }
  179.     }
  180.     /**
  181.      * Injects temporary settings for translation which differ from Context.
  182.      * Call resetInjection() when specific translation is done
  183.      */
  184.     public function injectSettings(string $salesChannelIdstring $languageIdstring $localeContext $context): void
  185.     {
  186.         $this->localeBeforeInject $this->getLocale();
  187.         $this->salesChannelId $salesChannelId;
  188.         $this->setLocale($locale);
  189.         $this->resolveSnippetSetId($salesChannelId$languageId$locale$context);
  190.         $this->getCatalogue($locale);
  191.     }
  192.     public function resetInjection(): void
  193.     {
  194.         \assert($this->localeBeforeInject !== null);
  195.         $this->setLocale($this->localeBeforeInject);
  196.         $this->snippetSetId null;
  197.         $this->salesChannelId null;
  198.     }
  199.     public function getSnippetSetId(?string $locale null): ?string
  200.     {
  201.         if ($locale !== null) {
  202.             if (\array_key_exists($locale$this->snippets)) {
  203.                 return $this->snippets[$locale];
  204.             }
  205.             $criteria = new Criteria();
  206.             $criteria->addFilter(new EqualsFilter('iso'$locale));
  207.             $snippetSetId $this->snippetSetRepository->searchIds($criteriaContext::createDefaultContext())->firstId();
  208.             if ($snippetSetId !== null) {
  209.                 return $this->snippets[$locale] = $snippetSetId;
  210.             }
  211.         }
  212.         if ($this->snippetSetId !== null) {
  213.             return $this->snippetSetId;
  214.         }
  215.         $request $this->requestStack->getCurrentRequest();
  216.         if (!$request) {
  217.             return null;
  218.         }
  219.         $this->snippetSetId $request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_SNIPPET_SET_ID);
  220.         return $this->snippetSetId;
  221.     }
  222.     /**
  223.      * @return array<int, MessageCatalogueInterface>
  224.      */
  225.     public function getCatalogues(): array
  226.     {
  227.         return array_values($this->isCustomized);
  228.     }
  229.     private function isFallbackLocaleCatalogue(MessageCatalogueInterface $catalogstring $fallbackLocale): bool
  230.     {
  231.         return mb_strpos($catalog->getLocale(), $fallbackLocale) === 0;
  232.     }
  233.     /**
  234.      * Shopware uses dashes in all locales
  235.      * if the catalogue does not contain any dashes it means it is a symfony fallback catalogue
  236.      * in that case we should not add the shopware fallback catalogue as it would result in circular references
  237.      */
  238.     private function isShopwareLocaleCatalogue(MessageCatalogueInterface $catalog): bool
  239.     {
  240.         return mb_strpos($catalog->getLocale(), '-') !== false;
  241.     }
  242.     private function resolveSnippetSetId(string $salesChannelIdstring $languageIdstring $localeContext $context): void
  243.     {
  244.         $snippetSet $this->snippetService->getSnippetSet($salesChannelId$languageId$locale$context);
  245.         if ($snippetSet === null) {
  246.             $this->snippetSetId null;
  247.         } else {
  248.             $this->snippetSetId $snippetSet->getId();
  249.         }
  250.     }
  251.     /**
  252.      * Add language specific snippets provided by the admin
  253.      */
  254.     private function getCustomizedCatalog(MessageCatalogueInterface $catalog, ?string $fallbackLocale, ?string $locale null): MessageCatalogueInterface
  255.     {
  256.         $snippetSetId $this->getSnippetSetId($locale);
  257.         if (!$snippetSetId) {
  258.             return $catalog;
  259.         }
  260.         if (\array_key_exists($snippetSetId$this->isCustomized)) {
  261.             return $this->isCustomized[$snippetSetId];
  262.         }
  263.         $snippets $this->loadSnippets($catalog$snippetSetId$fallbackLocale);
  264.         $newCatalog = clone $catalog;
  265.         $newCatalog->add($snippets);
  266.         return $this->isCustomized[$snippetSetId] = $newCatalog;
  267.     }
  268.     /**
  269.      * @return array<string, string>
  270.      */
  271.     private function loadSnippets(MessageCatalogueInterface $catalogstring $snippetSetId, ?string $fallbackLocale): array
  272.     {
  273.         $this->resolveSalesChannelId();
  274.         $key sprintf('translation.catalog.%s.%s'$this->salesChannelId ?: 'DEFAULT'$snippetSetId);
  275.         return $this->cache->get($key, function (ItemInterface $item) use ($catalog$snippetSetId$fallbackLocale) {
  276.             $item->tag('translation.catalog.' $snippetSetId);
  277.             $item->tag(sprintf('translation.catalog.%s'$this->salesChannelId ?: 'DEFAULT'));
  278.             return $this->snippetService->getStorefrontSnippets($catalog$snippetSetId$fallbackLocale$this->salesChannelId);
  279.         });
  280.     }
  281.     private function getFallbackLocale(): string
  282.     {
  283.         try {
  284.             return $this->languageLocaleProvider->getLocaleForLanguageId(Defaults::LANGUAGE_SYSTEM);
  285.         } catch (ConnectionException $_) {
  286.             // this allows us to use the translator even if there's no db connection yet
  287.             return 'en-GB';
  288.         }
  289.     }
  290.     private function resolveSalesChannelId(): void
  291.     {
  292.         if ($this->salesChannelId !== null) {
  293.             return;
  294.         }
  295.         $request $this->requestStack->getCurrentRequest();
  296.         if (!$request) {
  297.             return;
  298.         }
  299.         $this->salesChannelId $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
  300.     }
  301. }