Просмотр файла vendor/symfony/translation/Translator.php

Размер файла: 14.17Kb
  1. <?php
  2.  
  3. /*
  4. * This file is part of the Symfony package.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11.  
  12. namespace Symfony\Component\Translation;
  13.  
  14. use Symfony\Component\Config\ConfigCacheFactory;
  15. use Symfony\Component\Config\ConfigCacheFactoryInterface;
  16. use Symfony\Component\Config\ConfigCacheInterface;
  17. use Symfony\Component\Translation\Exception\InvalidArgumentException;
  18. use Symfony\Component\Translation\Exception\NotFoundResourceException;
  19. use Symfony\Component\Translation\Exception\RuntimeException;
  20. use Symfony\Component\Translation\Formatter\IntlFormatterInterface;
  21. use Symfony\Component\Translation\Formatter\MessageFormatter;
  22. use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
  23. use Symfony\Component\Translation\Loader\LoaderInterface;
  24. use Symfony\Contracts\Translation\LocaleAwareInterface;
  25. use Symfony\Contracts\Translation\TranslatorInterface;
  26.  
  27. // Help opcache.preload discover always-needed symbols
  28. class_exists(MessageCatalogue::class);
  29.  
  30. /**
  31. * @author Fabien Potencier <fabien@symfony.com>
  32. */
  33. class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
  34. {
  35. /**
  36. * @var MessageCatalogueInterface[]
  37. */
  38. protected $catalogues = [];
  39.  
  40. /**
  41. * @var string
  42. */
  43. private $locale;
  44.  
  45. /**
  46. * @var string[]
  47. */
  48. private $fallbackLocales = [];
  49.  
  50. /**
  51. * @var LoaderInterface[]
  52. */
  53. private $loaders = [];
  54.  
  55. /**
  56. * @var array
  57. */
  58. private $resources = [];
  59.  
  60. /**
  61. * @var MessageFormatterInterface
  62. */
  63. private $formatter;
  64.  
  65. /**
  66. * @var string
  67. */
  68. private $cacheDir;
  69.  
  70. /**
  71. * @var bool
  72. */
  73. private $debug;
  74.  
  75. private $cacheVary;
  76.  
  77. /**
  78. * @var ConfigCacheFactoryInterface|null
  79. */
  80. private $configCacheFactory;
  81.  
  82. /**
  83. * @var array|null
  84. */
  85. private $parentLocales;
  86.  
  87. private $hasIntlFormatter;
  88.  
  89. /**
  90. * @throws InvalidArgumentException If a locale contains invalid characters
  91. */
  92. public function __construct(string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = [])
  93. {
  94. $this->setLocale($locale);
  95.  
  96. if (null === $formatter) {
  97. $formatter = new MessageFormatter();
  98. }
  99.  
  100. $this->formatter = $formatter;
  101. $this->cacheDir = $cacheDir;
  102. $this->debug = $debug;
  103. $this->cacheVary = $cacheVary;
  104. $this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface;
  105. }
  106.  
  107. public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
  108. {
  109. $this->configCacheFactory = $configCacheFactory;
  110. }
  111.  
  112. /**
  113. * Adds a Loader.
  114. *
  115. * @param string $format The name of the loader (@see addResource())
  116. */
  117. public function addLoader(string $format, LoaderInterface $loader)
  118. {
  119. $this->loaders[$format] = $loader;
  120. }
  121.  
  122. /**
  123. * Adds a Resource.
  124. *
  125. * @param string $format The name of the loader (@see addLoader())
  126. * @param mixed $resource The resource name
  127. *
  128. * @throws InvalidArgumentException If the locale contains invalid characters
  129. */
  130. public function addResource(string $format, $resource, string $locale, string $domain = null)
  131. {
  132. if (null === $domain) {
  133. $domain = 'messages';
  134. }
  135.  
  136. $this->assertValidLocale($locale);
  137. $locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en';
  138.  
  139. $this->resources[$locale][] = [$format, $resource, $domain];
  140.  
  141. if (\in_array($locale, $this->fallbackLocales)) {
  142. $this->catalogues = [];
  143. } else {
  144. unset($this->catalogues[$locale]);
  145. }
  146. }
  147.  
  148. /**
  149. * {@inheritdoc}
  150. */
  151. public function setLocale(string $locale)
  152. {
  153. $this->assertValidLocale($locale);
  154. $this->locale = $locale;
  155. }
  156.  
  157. /**
  158. * {@inheritdoc}
  159. */
  160. public function getLocale()
  161. {
  162. return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en');
  163. }
  164.  
  165. /**
  166. * Sets the fallback locales.
  167. *
  168. * @param string[] $locales
  169. *
  170. * @throws InvalidArgumentException If a locale contains invalid characters
  171. */
  172. public function setFallbackLocales(array $locales)
  173. {
  174. // needed as the fallback locales are linked to the already loaded catalogues
  175. $this->catalogues = [];
  176.  
  177. foreach ($locales as $locale) {
  178. $this->assertValidLocale($locale);
  179. }
  180.  
  181. $this->fallbackLocales = $this->cacheVary['fallback_locales'] = $locales;
  182. }
  183.  
  184. /**
  185. * Gets the fallback locales.
  186. *
  187. * @internal
  188. */
  189. public function getFallbackLocales(): array
  190. {
  191. return $this->fallbackLocales;
  192. }
  193.  
  194. /**
  195. * {@inheritdoc}
  196. */
  197. public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
  198. {
  199. if (null === $id || '' === $id) {
  200. return '';
  201. }
  202.  
  203. if (null === $domain) {
  204. $domain = 'messages';
  205. }
  206.  
  207. $catalogue = $this->getCatalogue($locale);
  208. $locale = $catalogue->getLocale();
  209. while (!$catalogue->defines($id, $domain)) {
  210. if ($cat = $catalogue->getFallbackCatalogue()) {
  211. $catalogue = $cat;
  212. $locale = $catalogue->getLocale();
  213. } else {
  214. break;
  215. }
  216. }
  217.  
  218. $len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX);
  219. if ($this->hasIntlFormatter
  220. && ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)
  221. || (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len)))
  222. ) {
  223. return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters);
  224. }
  225.  
  226. return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters);
  227. }
  228.  
  229. /**
  230. * {@inheritdoc}
  231. */
  232. public function getCatalogue(string $locale = null)
  233. {
  234. if (!$locale) {
  235. $locale = $this->getLocale();
  236. } else {
  237. $this->assertValidLocale($locale);
  238. }
  239.  
  240. if (!isset($this->catalogues[$locale])) {
  241. $this->loadCatalogue($locale);
  242. }
  243.  
  244. return $this->catalogues[$locale];
  245. }
  246.  
  247. /**
  248. * {@inheritdoc}
  249. */
  250. public function getCatalogues(): array
  251. {
  252. return array_values($this->catalogues);
  253. }
  254.  
  255. /**
  256. * Gets the loaders.
  257. *
  258. * @return LoaderInterface[]
  259. */
  260. protected function getLoaders()
  261. {
  262. return $this->loaders;
  263. }
  264.  
  265. protected function loadCatalogue(string $locale)
  266. {
  267. if (null === $this->cacheDir) {
  268. $this->initializeCatalogue($locale);
  269. } else {
  270. $this->initializeCacheCatalogue($locale);
  271. }
  272. }
  273.  
  274. protected function initializeCatalogue(string $locale)
  275. {
  276. $this->assertValidLocale($locale);
  277.  
  278. try {
  279. $this->doLoadCatalogue($locale);
  280. } catch (NotFoundResourceException $e) {
  281. if (!$this->computeFallbackLocales($locale)) {
  282. throw $e;
  283. }
  284. }
  285. $this->loadFallbackCatalogues($locale);
  286. }
  287.  
  288. private function initializeCacheCatalogue(string $locale): void
  289. {
  290. if (isset($this->catalogues[$locale])) {
  291. /* Catalogue already initialized. */
  292. return;
  293. }
  294.  
  295. $this->assertValidLocale($locale);
  296. $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale),
  297. function (ConfigCacheInterface $cache) use ($locale) {
  298. $this->dumpCatalogue($locale, $cache);
  299. }
  300. );
  301.  
  302. if (isset($this->catalogues[$locale])) {
  303. /* Catalogue has been initialized as it was written out to cache. */
  304. return;
  305. }
  306.  
  307. /* Read catalogue from cache. */
  308. $this->catalogues[$locale] = include $cache->getPath();
  309. }
  310.  
  311. private function dumpCatalogue(string $locale, ConfigCacheInterface $cache): void
  312. {
  313. $this->initializeCatalogue($locale);
  314. $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]);
  315.  
  316. $content = sprintf(<<<EOF
  317. <?php
  318.  
  319. use Symfony\Component\Translation\MessageCatalogue;
  320.  
  321. \$catalogue = new MessageCatalogue('%s', %s);
  322.  
  323. %s
  324. return \$catalogue;
  325.  
  326. EOF
  327. ,
  328. $locale,
  329. var_export($this->getAllMessages($this->catalogues[$locale]), true),
  330. $fallbackContent
  331. );
  332.  
  333. $cache->write($content, $this->catalogues[$locale]->getResources());
  334. }
  335.  
  336. private function getFallbackContent(MessageCatalogue $catalogue): string
  337. {
  338. $fallbackContent = '';
  339. $current = '';
  340. $replacementPattern = '/[^a-z0-9_]/i';
  341. $fallbackCatalogue = $catalogue->getFallbackCatalogue();
  342. while ($fallbackCatalogue) {
  343. $fallback = $fallbackCatalogue->getLocale();
  344. $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback));
  345. $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current));
  346.  
  347. $fallbackContent .= sprintf(<<<'EOF'
  348. $catalogue%s = new MessageCatalogue('%s', %s);
  349. $catalogue%s->addFallbackCatalogue($catalogue%s);
  350.  
  351. EOF
  352. ,
  353. $fallbackSuffix,
  354. $fallback,
  355. var_export($this->getAllMessages($fallbackCatalogue), true),
  356. $currentSuffix,
  357. $fallbackSuffix
  358. );
  359. $current = $fallbackCatalogue->getLocale();
  360. $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
  361. }
  362.  
  363. return $fallbackContent;
  364. }
  365.  
  366. private function getCatalogueCachePath(string $locale): string
  367. {
  368. return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php';
  369. }
  370.  
  371. /**
  372. * @internal
  373. */
  374. protected function doLoadCatalogue(string $locale): void
  375. {
  376. $this->catalogues[$locale] = new MessageCatalogue($locale);
  377.  
  378. if (isset($this->resources[$locale])) {
  379. foreach ($this->resources[$locale] as $resource) {
  380. if (!isset($this->loaders[$resource[0]])) {
  381. if (\is_string($resource[1])) {
  382. throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1]));
  383. }
  384.  
  385. throw new RuntimeException(sprintf('No loader is registered for the "%s" format.', $resource[0]));
  386. }
  387. $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2]));
  388. }
  389. }
  390. }
  391.  
  392. private function loadFallbackCatalogues(string $locale): void
  393. {
  394. $current = $this->catalogues[$locale];
  395.  
  396. foreach ($this->computeFallbackLocales($locale) as $fallback) {
  397. if (!isset($this->catalogues[$fallback])) {
  398. $this->initializeCatalogue($fallback);
  399. }
  400.  
  401. $fallbackCatalogue = new MessageCatalogue($fallback, $this->getAllMessages($this->catalogues[$fallback]));
  402. foreach ($this->catalogues[$fallback]->getResources() as $resource) {
  403. $fallbackCatalogue->addResource($resource);
  404. }
  405. $current->addFallbackCatalogue($fallbackCatalogue);
  406. $current = $fallbackCatalogue;
  407. }
  408. }
  409.  
  410. protected function computeFallbackLocales(string $locale)
  411. {
  412. if (null === $this->parentLocales) {
  413. $this->parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true);
  414. }
  415.  
  416. $originLocale = $locale;
  417. $locales = [];
  418.  
  419. while ($locale) {
  420. $parent = $this->parentLocales[$locale] ?? null;
  421.  
  422. if ($parent) {
  423. $locale = 'root' !== $parent ? $parent : null;
  424. } elseif (\function_exists('locale_parse')) {
  425. $localeSubTags = locale_parse($locale);
  426. $locale = null;
  427. if (1 < \count($localeSubTags)) {
  428. array_pop($localeSubTags);
  429. $locale = locale_compose($localeSubTags) ?: null;
  430. }
  431. } elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) {
  432. $locale = substr($locale, 0, $i);
  433. } else {
  434. $locale = null;
  435. }
  436.  
  437. if (null !== $locale) {
  438. $locales[] = $locale;
  439. }
  440. }
  441.  
  442. foreach ($this->fallbackLocales as $fallback) {
  443. if ($fallback === $originLocale) {
  444. continue;
  445. }
  446.  
  447. $locales[] = $fallback;
  448. }
  449.  
  450. return array_unique($locales);
  451. }
  452.  
  453. /**
  454. * Asserts that the locale is valid, throws an Exception if not.
  455. *
  456. * @throws InvalidArgumentException If the locale contains invalid characters
  457. */
  458. protected function assertValidLocale(string $locale)
  459. {
  460. if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) {
  461. throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
  462. }
  463. }
  464.  
  465. /**
  466. * Provides the ConfigCache factory implementation, falling back to a
  467. * default implementation if necessary.
  468. */
  469. private function getConfigCacheFactory(): ConfigCacheFactoryInterface
  470. {
  471. if (!$this->configCacheFactory) {
  472. $this->configCacheFactory = new ConfigCacheFactory($this->debug);
  473. }
  474.  
  475. return $this->configCacheFactory;
  476. }
  477.  
  478. private function getAllMessages(MessageCatalogueInterface $catalogue): array
  479. {
  480. $allMessages = [];
  481.  
  482. foreach ($catalogue->all() as $domain => $messages) {
  483. if ($intlMessages = $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
  484. $allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages;
  485. $messages = array_diff_key($messages, $intlMessages);
  486. }
  487. if ($messages) {
  488. $allMessages[$domain] = $messages;
  489. }
  490. }
  491.  
  492. return $allMessages;
  493. }
  494. }