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

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