vendor/pimcore/pimcore/lib/Translation/Translator.php line 180

Open in your IDE?
  1. <?php
  2. /**
  3. * Pimcore
  4. *
  5. * This source file is available under two different licenses:
  6. * - GNU General Public License version 3 (GPLv3)
  7. * - Pimcore Commercial License (PCL)
  8. * Full copyright and license information is available in
  9. * LICENSE.md which is distributed with this source code.
  10. *
  11. * @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12. * @license http://www.pimcore.org/license GPLv3 and PCL
  13. */
  14. namespace Pimcore\Translation;
  15. use Pimcore\Cache;
  16. use Pimcore\Model\Translation;
  17. use Pimcore\Tool;
  18. use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
  19. use Symfony\Component\HttpKernel\Kernel;
  20. use Symfony\Component\Translation\Exception\InvalidArgumentException;
  21. use Symfony\Component\Translation\MessageCatalogue;
  22. use Symfony\Component\Translation\MessageCatalogueInterface;
  23. use Symfony\Component\Translation\TranslatorBagInterface;
  24. use Symfony\Contracts\Translation\LocaleAwareInterface;
  25. use Symfony\Contracts\Translation\TranslatorInterface;
  26. class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface
  27. {
  28. /**
  29. * @var TranslatorInterface|TranslatorBagInterface|WarmableInterface
  30. */
  31. protected $translator;
  32. /**
  33. * @var array
  34. */
  35. protected $initializedCatalogues = [];
  36. /**
  37. * @var string
  38. */
  39. protected $adminPath = '';
  40. /**
  41. * @var array
  42. */
  43. protected $adminTranslationMapping = [];
  44. /**
  45. * If true, the translator will just return the translation key instead of actually translating
  46. * the message. Can be useful for debugging and to get an overview over used translation keys on
  47. * a page.
  48. *
  49. * @var bool
  50. */
  51. protected $disableTranslations = false;
  52. /**
  53. * @var Kernel
  54. */
  55. protected $kernel;
  56. /**
  57. * @param TranslatorInterface $translator
  58. */
  59. public function __construct(TranslatorInterface $translator)
  60. {
  61. if (!$translator instanceof TranslatorBagInterface) {
  62. throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', get_class($translator)));
  63. }
  64. $this->translator = $translator;
  65. }
  66. /**
  67. * {@inheritdoc}
  68. *
  69. * @return string
  70. */
  71. public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null)//: string
  72. {
  73. $id = trim($id);
  74. if ($this->disableTranslations) {
  75. return $id;
  76. }
  77. if (null === $domain) {
  78. $domain = Translation::DOMAIN_DEFAULT;
  79. }
  80. if ($domain === Translation::DOMAIN_ADMIN && !empty($this->adminTranslationMapping)) {
  81. if (null === $locale) {
  82. $locale = $this->getLocale();
  83. }
  84. if (array_key_exists($locale, $this->adminTranslationMapping)) {
  85. $locale = $this->adminTranslationMapping[$locale];
  86. }
  87. }
  88. $catalogue = $this->getCatalogue($locale);
  89. $locale = $catalogue->getLocale();
  90. $this->lazyInitialize($domain, $locale);
  91. $originalId = $id;
  92. $term = $this->translator->trans($id, $parameters, $domain, $locale);
  93. // only check for empty translation on original ID - we don't want to create empty
  94. // translations for normalized IDs when case insensitive
  95. $term = $this->checkForEmptyTranslation($originalId, $term, $parameters, $domain, $locale);
  96. // check for an indexed array, that used the ZF1 vsprintf() notation for parameters
  97. if (isset($parameters[0])) {
  98. $term = vsprintf($term, $parameters);
  99. }
  100. $term = $this->updateLinks($term);
  101. return $term;
  102. }
  103. /**
  104. * {@inheritdoc}
  105. */
  106. public function setLocale(string $locale)
  107. {
  108. if ($this->translator instanceof LocaleAwareInterface) {
  109. $this->translator->setLocale($locale);
  110. }
  111. }
  112. /**
  113. * {@inheritdoc}
  114. *
  115. * @return string
  116. */
  117. public function getLocale()//: string
  118. {
  119. if ($this->translator instanceof LocaleAwareInterface) {
  120. return $this->translator->getLocale();
  121. }
  122. return \Pimcore\Tool::getDefaultLanguage();
  123. }
  124. /**
  125. * {@inheritdoc}
  126. *
  127. * @return MessageCatalogueInterface
  128. */
  129. public function getCatalogue(string $locale = null)// : MessageCatalogueInterface
  130. {
  131. return $this->translator->getCatalogue($locale);
  132. }
  133. /**
  134. * @internal
  135. *
  136. * @param string $domain
  137. * @param string $locale
  138. */
  139. public function lazyInitialize(string $domain, string $locale)
  140. {
  141. $cacheKey = $this->getCacheKey($domain, $locale);
  142. if (isset($this->initializedCatalogues[$cacheKey])) {
  143. return;
  144. }
  145. $this->initializedCatalogues[$cacheKey] = true;
  146. if (Translation::isAValidDomain($domain)) {
  147. $catalogue = null;
  148. if (!$catalogue = Cache::load($cacheKey)) {
  149. $data = ['__pimcore_dummy' => 'only_a_dummy'];
  150. $dataIntl = ['__pimcore_dummy' => 'only_a_dummy'];
  151. if ($domain == 'admin') {
  152. $jsonFiles = [
  153. $locale . '.json' => 'en.json',
  154. $locale . '.extended.json' => 'en.extended.json',
  155. ];
  156. foreach ($jsonFiles as $sourceFile => $fallbackFile) {
  157. try {
  158. $jsonPath = $this->getKernel()->locateResource($this->getAdminPath() . '/' . $sourceFile);
  159. } catch (\Exception $e) {
  160. $jsonPath = $this->getKernel()->locateResource($this->getAdminPath() . '/' . $fallbackFile);
  161. }
  162. $jsonTranslations = json_decode(file_get_contents($jsonPath), true);
  163. if (is_array($jsonTranslations)) {
  164. $defaultCatalog = $this->getCatalogue($locale);
  165. foreach ($jsonTranslations as $translationKey => $translationValue) {
  166. if (!$defaultCatalog->has($translationKey, 'admin')) {
  167. $data[$translationKey] = $translationValue;
  168. }
  169. }
  170. }
  171. }
  172. }
  173. $list = new Translation\Listing();
  174. $list->setDomain($domain);
  175. $debugAdminTranslations = \Pimcore\Config::getSystemConfiguration('general')['debug_admin_translations'] ?? false;
  176. $list->setCondition('language = ?', [$locale]);
  177. $translations = $list->loadRaw();
  178. foreach ($translations as $translation) {
  179. $translationTerm = Tool\Text::removeLineBreaks($translation['text']);
  180. if (
  181. (!isset($data[$translation['key']]) && !$this->getCatalogue($locale)->has($translation['key'], $domain)) ||
  182. !empty($translationTerm)) {
  183. $translationKey = $translation['key'];
  184. if (empty($translationTerm) && $debugAdminTranslations) {
  185. //wrap non-translated keys with "+", if debug admin translations is enabled
  186. $translationTerm = '+' . $translationKey. '+';
  187. }
  188. if (empty($translation['type']) || $translation['type'] === 'simple') {
  189. $data[$translationKey] = $translationTerm;
  190. } else {
  191. $dataIntl[$translationKey] = $translationTerm;
  192. }
  193. }
  194. }
  195. // aliases support
  196. if ($domain == 'admin') {
  197. $aliasesPath = $this->getKernel()->locateResource($this->getAdminPath() . '/aliases.json');
  198. $aliases = json_decode(file_get_contents($aliasesPath), true);
  199. foreach ($aliases as $aliasTarget => $aliasSource) {
  200. if (isset($data[$aliasSource]) && (!isset($data[$aliasTarget]) || empty($data[$aliasTarget]))) {
  201. $data[$aliasTarget] = $data[$aliasSource];
  202. }
  203. }
  204. }
  205. $data = [
  206. $domain => $data,
  207. $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX => $dataIntl,
  208. ];
  209. $catalogue = new MessageCatalogue($locale, $data);
  210. Cache::save($catalogue, $cacheKey, ['translator', 'translator_website', 'translate'], null, 999);
  211. }
  212. if ($catalogue) {
  213. $this->getCatalogue($locale)->addCatalogue($catalogue);
  214. }
  215. }
  216. }
  217. /**
  218. * Resets the initialization of a specific catalogue
  219. *
  220. * @param string $domain
  221. * @param string $locale
  222. *
  223. * @return void
  224. */
  225. public function resetInitialization(string $domain, string $locale): void
  226. {
  227. $cacheKey = $this->getCacheKey($domain, $locale);
  228. unset($this->initializedCatalogues[$cacheKey]);
  229. }
  230. /**
  231. * Reset Catalogues initialization
  232. */
  233. public function resetCache()
  234. {
  235. $this->initializedCatalogues = [];
  236. }
  237. /**
  238. * @param string $id
  239. * @param string $translated
  240. * @param array $parameters
  241. * @param string $domain
  242. * @param string $locale
  243. *
  244. * @return string
  245. *
  246. * @throws \Exception
  247. */
  248. private function checkForEmptyTranslation($id, $translated, $parameters, $domain, $locale)
  249. {
  250. if (empty($id)) {
  251. return $translated;
  252. }
  253. $normalizedId = $id;
  254. //translate only plural form(seperated by pipe "|") with count param
  255. if (isset($parameters['%count%']) && $translated && strpos($normalizedId, '|') !== false) {
  256. $normalizedId = $id = $translated;
  257. $translated = $this->translator->trans($normalizedId, $parameters, $domain, $locale);
  258. }
  259. $lookForFallback = empty($translated);
  260. if ($normalizedId != $translated && $translated) {
  261. return $translated;
  262. } elseif ($normalizedId == $translated) {
  263. if ($this->getCatalogue($locale)->has($normalizedId, $domain)) {
  264. $translated = $this->getCatalogue($locale)->get($normalizedId, $domain);
  265. if ($normalizedId != $translated && $translated) {
  266. return $translated;
  267. }
  268. } elseif (Translation::isAValidDomain($domain)) {
  269. if (strlen($id) > 190) {
  270. throw new \Exception("Message ID's longer than 190 characters are invalid!");
  271. }
  272. // no translation found create key
  273. if (Translation::IsAValidLanguage($domain, $locale)) {
  274. $t = Translation::getByKey($id, $domain);
  275. if ($t) {
  276. if (!$t->hasTranslation($locale)) {
  277. $t->addTranslation($locale, '');
  278. } else {
  279. // return the original not lowercased ID
  280. return $id;
  281. }
  282. } else {
  283. $t = new Translation();
  284. $t->setDomain($domain);
  285. $t->setKey($id);
  286. // add all available languages
  287. $availableLanguages = (array)Translation::getValidLanguages();
  288. foreach ($availableLanguages as $language) {
  289. $t->addTranslation($language, '');
  290. }
  291. }
  292. TranslationEntriesDumper::addToSaveQueue($t);
  293. }
  294. // put it into the catalogue, otherwise when there are more calls to the same key during one process
  295. // the key would be inserted/updated several times, what would be redundant
  296. $this->getCatalogue($locale)->set($normalizedId, $id, $domain);
  297. }
  298. }
  299. // now check for custom fallback locales, only for shared translations
  300. if ($lookForFallback && $domain == 'messages') {
  301. foreach (Tool::getFallbackLanguagesFor($locale) as $fallbackLanguage) {
  302. $this->lazyInitialize($domain, $fallbackLanguage);
  303. $catalogue = $this->getCatalogue($fallbackLanguage);
  304. $fallbackValue = '';
  305. if ($catalogue->has($normalizedId, $domain)) {
  306. $fallbackValue = $catalogue->get($normalizedId, $domain);
  307. }
  308. if ($fallbackValue && $normalizedId != $fallbackValue) {
  309. // update fallback value in original catalogue otherwise multiple calls to the same id will not work
  310. $this->getCatalogue($locale)->set($normalizedId, $fallbackValue, $domain);
  311. return strtr($fallbackValue, $parameters);
  312. }
  313. }
  314. }
  315. return !empty($translated) ? $translated : $id;
  316. }
  317. /**
  318. * @internal
  319. *
  320. * @return string
  321. */
  322. public function getAdminPath()
  323. {
  324. return $this->adminPath;
  325. }
  326. /**
  327. * @internal
  328. *
  329. * @param string $adminPath
  330. */
  331. public function setAdminPath($adminPath)
  332. {
  333. $this->adminPath = $adminPath;
  334. }
  335. /**
  336. * @internal
  337. *
  338. * @return array
  339. */
  340. public function getAdminTranslationMapping(): array
  341. {
  342. return $this->adminTranslationMapping;
  343. }
  344. /**
  345. * @internal
  346. *
  347. * @param array $adminTranslationMapping
  348. */
  349. public function setAdminTranslationMapping(array $adminTranslationMapping): void
  350. {
  351. $this->adminTranslationMapping = $adminTranslationMapping;
  352. }
  353. /**
  354. * @internal
  355. *
  356. * @return Kernel
  357. */
  358. public function getKernel()
  359. {
  360. return $this->kernel;
  361. }
  362. /**
  363. * @internal
  364. *
  365. * @param Kernel $kernel
  366. */
  367. public function setKernel($kernel)
  368. {
  369. $this->kernel = $kernel;
  370. }
  371. public function getDisableTranslations(): bool
  372. {
  373. return $this->disableTranslations;
  374. }
  375. /**
  376. * @param bool $disableTranslations
  377. */
  378. public function setDisableTranslations(bool $disableTranslations)
  379. {
  380. $this->disableTranslations = $disableTranslations;
  381. }
  382. /**
  383. * @param string $text
  384. *
  385. * @return string
  386. */
  387. private function updateLinks(string $text): string
  388. {
  389. if (strpos($text, 'pimcore_id')) {
  390. $text = Tool\Text::wysiwygText($text);
  391. }
  392. return $text;
  393. }
  394. /**
  395. * Passes through all unknown calls onto the translator object.
  396. *
  397. * @param string $method
  398. * @param array $args
  399. *
  400. * @return mixed
  401. */
  402. public function __call($method, $args)
  403. {
  404. return call_user_func_array([$this->translator, $method], $args);
  405. }
  406. /**
  407. * @param string $domain
  408. * @param string $locale
  409. *
  410. * @return string
  411. */
  412. private function getCacheKey(string $domain, string $locale): string
  413. {
  414. return 'translation_data_' . md5($domain . '_' . $locale);
  415. }
  416. /**
  417. * @param string $cacheDir
  418. *
  419. * @return string[]
  420. */
  421. public function warmUp(string $cacheDir): array
  422. {
  423. return $this->translator->warmUp($cacheDir);
  424. }
  425. }