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

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($fallbackLocale, 0, 2);
  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 $salesChannelId, string $languageId, string $locale, Context $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($criteria, Context::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 $catalog, string $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 $salesChannelId, string $languageId, string $locale, Context $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 $catalog, string $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. }