src/Core/Framework/Adapter/Translation/Translator.php line 147

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