Translate.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\Framework;
  8. use Magento\Framework\App\Filesystem\DirectoryList;
  9. use Magento\Framework\App\ObjectManager;
  10. use Magento\Framework\Filesystem\Driver\File;
  11. use Magento\Framework\Filesystem\DriverInterface;
  12. /**
  13. * Translate library
  14. *
  15. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  16. * @SuppressWarnings(PHPMD.TooManyFields)
  17. */
  18. class Translate implements \Magento\Framework\TranslateInterface
  19. {
  20. const CONFIG_AREA_KEY = 'area';
  21. const CONFIG_LOCALE_KEY = 'locale';
  22. const CONFIG_SCOPE_KEY = 'scope';
  23. const CONFIG_THEME_KEY = 'theme';
  24. const CONFIG_MODULE_KEY = 'module';
  25. /**
  26. * Locale code
  27. *
  28. * @var string
  29. */
  30. protected $_localeCode;
  31. /**
  32. * Translator configuration array
  33. *
  34. * @var array
  35. */
  36. protected $_config;
  37. /**
  38. * Cache identifier
  39. *
  40. * @var string
  41. */
  42. protected $_cacheId;
  43. /**
  44. * Translation data
  45. *
  46. * @var []
  47. */
  48. protected $_data = [];
  49. /**
  50. * @var \Magento\Framework\View\DesignInterface
  51. */
  52. protected $_viewDesign;
  53. /**
  54. * @var \Magento\Framework\Cache\FrontendInterface
  55. */
  56. protected $_cache;
  57. /**
  58. * @var \Magento\Framework\View\FileSystem
  59. */
  60. protected $_viewFileSystem;
  61. /**
  62. * @var \Magento\Framework\Module\ModuleList
  63. */
  64. protected $_moduleList;
  65. /**
  66. * @var \Magento\Framework\Module\Dir\Reader
  67. */
  68. protected $_modulesReader;
  69. /**
  70. * @var \Magento\Framework\App\ScopeResolverInterface
  71. */
  72. protected $_scopeResolver;
  73. /**
  74. * @var \Magento\Framework\Translate\ResourceInterface
  75. */
  76. protected $_translateResource;
  77. /**
  78. * @var \Magento\Framework\Locale\ResolverInterface
  79. */
  80. protected $_locale;
  81. /**
  82. * @var \Magento\Framework\App\State
  83. */
  84. protected $_appState;
  85. /**
  86. * @var \Magento\Framework\Filesystem\Directory\Read
  87. */
  88. protected $directory;
  89. /**
  90. * @var \Magento\Framework\App\RequestInterface
  91. */
  92. protected $request;
  93. /**
  94. * @var \Magento\Framework\File\Csv
  95. */
  96. protected $_csvParser;
  97. /**
  98. * @var \Magento\Framework\App\Language\Dictionary
  99. */
  100. protected $packDictionary;
  101. /**
  102. * @var \Magento\Framework\Serialize\SerializerInterface
  103. */
  104. private $serializer;
  105. /**
  106. * @var DriverInterface
  107. */
  108. private $fileDriver;
  109. /**
  110. * @param \Magento\Framework\View\DesignInterface $viewDesign
  111. * @param \Magento\Framework\Cache\FrontendInterface $cache
  112. * @param \Magento\Framework\View\FileSystem $viewFileSystem
  113. * @param \Magento\Framework\Module\ModuleList $moduleList
  114. * @param \Magento\Framework\Module\Dir\Reader $modulesReader
  115. * @param \Magento\Framework\App\ScopeResolverInterface $scopeResolver
  116. * @param \Magento\Framework\Translate\ResourceInterface $translate
  117. * @param \Magento\Framework\Locale\ResolverInterface $locale
  118. * @param \Magento\Framework\App\State $appState
  119. * @param \Magento\Framework\Filesystem $filesystem
  120. * @param \Magento\Framework\App\RequestInterface $request
  121. * @param \Magento\Framework\File\Csv $csvParser
  122. * @param \Magento\Framework\App\Language\Dictionary $packDictionary
  123. * @param DriverInterface|null $fileDriver
  124. *
  125. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  126. */
  127. public function __construct(
  128. \Magento\Framework\View\DesignInterface $viewDesign,
  129. \Magento\Framework\Cache\FrontendInterface $cache,
  130. \Magento\Framework\View\FileSystem $viewFileSystem,
  131. \Magento\Framework\Module\ModuleList $moduleList,
  132. \Magento\Framework\Module\Dir\Reader $modulesReader,
  133. \Magento\Framework\App\ScopeResolverInterface $scopeResolver,
  134. \Magento\Framework\Translate\ResourceInterface $translate,
  135. \Magento\Framework\Locale\ResolverInterface $locale,
  136. \Magento\Framework\App\State $appState,
  137. \Magento\Framework\Filesystem $filesystem,
  138. \Magento\Framework\App\RequestInterface $request,
  139. \Magento\Framework\File\Csv $csvParser,
  140. \Magento\Framework\App\Language\Dictionary $packDictionary,
  141. DriverInterface $fileDriver = null
  142. ) {
  143. $this->_viewDesign = $viewDesign;
  144. $this->_cache = $cache;
  145. $this->_viewFileSystem = $viewFileSystem;
  146. $this->_moduleList = $moduleList;
  147. $this->_modulesReader = $modulesReader;
  148. $this->_scopeResolver = $scopeResolver;
  149. $this->_translateResource = $translate;
  150. $this->_locale = $locale;
  151. $this->_appState = $appState;
  152. $this->request = $request;
  153. $this->directory = $filesystem->getDirectoryRead(DirectoryList::ROOT);
  154. $this->_csvParser = $csvParser;
  155. $this->packDictionary = $packDictionary;
  156. $this->fileDriver = $fileDriver
  157. ?? ObjectManager::getInstance()->get(File::class);
  158. $this->_config = [
  159. self::CONFIG_AREA_KEY => null,
  160. self::CONFIG_LOCALE_KEY => null,
  161. self::CONFIG_SCOPE_KEY => null,
  162. self::CONFIG_THEME_KEY => null,
  163. self::CONFIG_MODULE_KEY => null,
  164. ];
  165. }
  166. /**
  167. * Initialize translation data
  168. *
  169. * @param string|null $area
  170. * @param bool $forceReload
  171. * @return $this
  172. */
  173. public function loadData($area = null, $forceReload = false)
  174. {
  175. $this->_data = [];
  176. if ($area === null) {
  177. $area = $this->_appState->getAreaCode();
  178. }
  179. $this->setConfig(
  180. [
  181. self::CONFIG_AREA_KEY => $area,
  182. ]
  183. );
  184. if (!$forceReload) {
  185. $data = $this->_loadCache();
  186. if (false !== $data) {
  187. $this->_data = $data;
  188. return $this;
  189. }
  190. }
  191. $this->_loadModuleTranslation();
  192. $this->_loadPackTranslation();
  193. $this->_loadThemeTranslation();
  194. $this->_loadDbTranslation();
  195. if (!$forceReload) {
  196. $this->_saveCache();
  197. }
  198. return $this;
  199. }
  200. /**
  201. * Initialize configuration
  202. *
  203. * @param array $config
  204. * @return $this
  205. */
  206. protected function setConfig($config)
  207. {
  208. $this->_config = $config;
  209. if (!isset($this->_config[self::CONFIG_LOCALE_KEY])) {
  210. $this->_config[self::CONFIG_LOCALE_KEY] = $this->getLocale();
  211. }
  212. if (!isset($this->_config[self::CONFIG_SCOPE_KEY])) {
  213. $this->_config[self::CONFIG_SCOPE_KEY] = $this->getScope();
  214. }
  215. if (!isset($this->_config[self::CONFIG_THEME_KEY])) {
  216. $this->_config[self::CONFIG_THEME_KEY] = $this->_viewDesign->getDesignTheme()->getThemePath();
  217. }
  218. if (!isset($this->_config[self::CONFIG_MODULE_KEY])) {
  219. $this->_config[self::CONFIG_MODULE_KEY] = $this->getControllerModuleName();
  220. }
  221. return $this;
  222. }
  223. /**
  224. * Retrieve scope code
  225. *
  226. * @return string
  227. */
  228. protected function getScope()
  229. {
  230. $scope = ($this->getConfig(self::CONFIG_AREA_KEY) === 'adminhtml') ? 'admin' : null;
  231. return $this->_scopeResolver->getScope($scope)->getCode();
  232. }
  233. /**
  234. * Retrieve config value by key
  235. *
  236. * @param string $key
  237. * @return mixed
  238. */
  239. protected function getConfig($key)
  240. {
  241. if (isset($this->_config[$key])) {
  242. return $this->_config[$key];
  243. }
  244. return null;
  245. }
  246. /**
  247. * Retrieve name of the current module
  248. *
  249. * @return mixed
  250. */
  251. protected function getControllerModuleName()
  252. {
  253. return $this->request->getControllerModule();
  254. }
  255. /**
  256. * Load data from module translation files
  257. *
  258. * @return $this
  259. */
  260. protected function _loadModuleTranslation()
  261. {
  262. $currentModule = $this->getControllerModuleName();
  263. $allModulesExceptCurrent = array_diff($this->_moduleList->getNames(), [$currentModule]);
  264. $this->loadModuleTranslationByModulesList($allModulesExceptCurrent);
  265. $this->loadModuleTranslationByModulesList([$currentModule]);
  266. return $this;
  267. }
  268. /**
  269. * Load data from module translation files by list of modules
  270. *
  271. * @param array $modules
  272. * @return $this
  273. */
  274. protected function loadModuleTranslationByModulesList(array $modules)
  275. {
  276. foreach ($modules as $module) {
  277. $moduleFilePath = $this->_getModuleTranslationFile($module, $this->getLocale());
  278. $this->_addData($this->_getFileData($moduleFilePath));
  279. }
  280. return $this;
  281. }
  282. /**
  283. * Adding translation data
  284. *
  285. * @param array $data
  286. * @return $this
  287. */
  288. protected function _addData($data)
  289. {
  290. foreach ($data as $key => $value) {
  291. if ($key === $value) {
  292. if (isset($this->_data[$key])) {
  293. unset($this->_data[$key]);
  294. }
  295. continue;
  296. }
  297. $key = str_replace('""', '"', $key);
  298. $value = str_replace('""', '"', $value);
  299. $this->_data[$key] = $value;
  300. }
  301. return $this;
  302. }
  303. /**
  304. * Load current theme translation according to fallback
  305. *
  306. * @return $this
  307. */
  308. protected function _loadThemeTranslation()
  309. {
  310. $themeFiles = $this->getThemeTranslationFilesList($this->getLocale());
  311. /** @var string $file */
  312. foreach ($themeFiles as $file) {
  313. if ($file) {
  314. $this->_addData($this->_getFileData($file));
  315. }
  316. }
  317. return $this;
  318. }
  319. /**
  320. * Load translation dictionary from language packages
  321. *
  322. * @return void
  323. */
  324. protected function _loadPackTranslation()
  325. {
  326. $data = $this->packDictionary->getDictionary($this->getLocale());
  327. $this->_addData($data);
  328. }
  329. /**
  330. * Loading current translation from DB
  331. *
  332. * @return $this
  333. */
  334. protected function _loadDbTranslation()
  335. {
  336. $data = $this->_translateResource->getTranslationArray(null, $this->getLocale());
  337. $this->_addData(array_map('htmlspecialchars_decode', $data));
  338. return $this;
  339. }
  340. /**
  341. * Retrieve translation file for module
  342. *
  343. * @param string $moduleName
  344. * @param string $locale
  345. * @return string
  346. */
  347. protected function _getModuleTranslationFile($moduleName, $locale)
  348. {
  349. $file = $this->_modulesReader->getModuleDir(Module\Dir::MODULE_I18N_DIR, $moduleName);
  350. $file .= '/' . $locale . '.csv';
  351. return $file;
  352. }
  353. /**
  354. * Get theme translation locale file name
  355. *
  356. * @param string|null $locale
  357. * @param array $config
  358. * @return string|null
  359. */
  360. private function getThemeTranslationFileName(?string $locale, array $config): ?string
  361. {
  362. $fileName = $this->_viewFileSystem->getLocaleFileName(
  363. 'i18n' . '/' . $locale . '.csv',
  364. $config
  365. );
  366. return $fileName ? $fileName : null;
  367. }
  368. /**
  369. * Get parent themes for the current theme in fallback order
  370. *
  371. * @return array
  372. */
  373. private function getParentThemesList(): array
  374. {
  375. $themes = [];
  376. $parentTheme = $this->_viewDesign->getDesignTheme()->getParentTheme();
  377. while ($parentTheme) {
  378. $themes[] = $parentTheme;
  379. $parentTheme = $parentTheme->getParentTheme();
  380. }
  381. $themes = array_reverse($themes);
  382. return $themes;
  383. }
  384. /**
  385. * Retrieve translation files for themes according to fallback
  386. *
  387. * @param string $locale
  388. *
  389. * @return array
  390. */
  391. private function getThemeTranslationFilesList($locale): array
  392. {
  393. $translationFiles = [];
  394. /** @var \Magento\Framework\View\Design\ThemeInterface $theme */
  395. foreach ($this->getParentThemesList() as $theme) {
  396. $config = $this->_config;
  397. $config['theme'] = $theme->getCode();
  398. $translationFiles[] = $this->getThemeTranslationFileName($locale, $config);
  399. }
  400. $translationFiles[] = $this->getThemeTranslationFileName($locale, $this->_config);
  401. return $translationFiles;
  402. }
  403. /**
  404. * Retrieve translation file for theme
  405. *
  406. * @param string $locale
  407. * @return string
  408. *
  409. * @deprecated 102.0.1
  410. *
  411. * @see \Magento\Framework\Translate::getThemeTranslationFilesList
  412. */
  413. protected function _getThemeTranslationFile($locale)
  414. {
  415. return $this->_viewFileSystem->getLocaleFileName(
  416. 'i18n' . '/' . $locale . '.csv',
  417. $this->_config
  418. );
  419. }
  420. /**
  421. * Retrieve data from file
  422. *
  423. * @param string $file
  424. * @return array
  425. */
  426. protected function _getFileData($file)
  427. {
  428. $data = [];
  429. if ($this->fileDriver->isExists($file)) {
  430. $this->_csvParser->setDelimiter(',');
  431. $data = $this->_csvParser->getDataPairs($file);
  432. }
  433. return $data;
  434. }
  435. /**
  436. * Retrieve translation data
  437. *
  438. * @return array
  439. */
  440. public function getData()
  441. {
  442. if ($this->_data === null) {
  443. return [];
  444. }
  445. return $this->_data;
  446. }
  447. /**
  448. * Retrieve locale
  449. *
  450. * @return string
  451. */
  452. public function getLocale()
  453. {
  454. if (null === $this->_localeCode) {
  455. $this->_localeCode = $this->_locale->getLocale();
  456. }
  457. return $this->_localeCode;
  458. }
  459. /**
  460. * Set locale
  461. *
  462. * @param string $locale
  463. * @return \Magento\Framework\TranslateInterface
  464. */
  465. public function setLocale($locale)
  466. {
  467. $this->_localeCode = $locale;
  468. $this->_config[self::CONFIG_LOCALE_KEY] = $locale;
  469. return $this;
  470. }
  471. /**
  472. * Retrieve theme code
  473. *
  474. * @return string
  475. */
  476. public function getTheme()
  477. {
  478. $theme = $this->request->getParam(self::CONFIG_THEME_KEY);
  479. if (empty($theme)) {
  480. return self::CONFIG_THEME_KEY . $this->getConfig(self::CONFIG_THEME_KEY);
  481. }
  482. return self::CONFIG_THEME_KEY . $theme['theme_title'];
  483. }
  484. /**
  485. * Retrieve cache identifier
  486. *
  487. * @return string
  488. */
  489. protected function getCacheId()
  490. {
  491. $_cacheId = \Magento\Framework\App\Cache\Type\Translate::TYPE_IDENTIFIER;
  492. $_cacheId .= '_' . $this->_config[self::CONFIG_LOCALE_KEY];
  493. $_cacheId .= '_' . $this->_config[self::CONFIG_AREA_KEY];
  494. $_cacheId .= '_' . $this->_config[self::CONFIG_SCOPE_KEY];
  495. $_cacheId .= '_' . $this->_config[self::CONFIG_THEME_KEY];
  496. $_cacheId .= '_' . $this->_config[self::CONFIG_MODULE_KEY];
  497. $this->_cacheId = $_cacheId;
  498. return $_cacheId;
  499. }
  500. /**
  501. * Loading data cache
  502. *
  503. * @return array|bool
  504. */
  505. protected function _loadCache()
  506. {
  507. $data = $this->_cache->load($this->getCacheId());
  508. if ($data) {
  509. $data = $this->getSerializer()->unserialize($data);
  510. }
  511. return $data;
  512. }
  513. /**
  514. * Saving data cache
  515. *
  516. * @return $this
  517. */
  518. protected function _saveCache()
  519. {
  520. $this->_cache->save($this->getSerializer()->serialize($this->getData()), $this->getCacheId(), [], false);
  521. return $this;
  522. }
  523. /**
  524. * Get serializer
  525. *
  526. * @return \Magento\Framework\Serialize\SerializerInterface
  527. * @deprecated 101.0.0
  528. */
  529. private function getSerializer()
  530. {
  531. if ($this->serializer === null) {
  532. $this->serializer = \Magento\Framework\App\ObjectManager::getInstance()
  533. ->get(Serialize\SerializerInterface::class);
  534. }
  535. return $this->serializer;
  536. }
  537. }