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

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