Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Translation; Chris@0: Chris@17: use Symfony\Component\Config\ConfigCacheFactory; Chris@17: use Symfony\Component\Config\ConfigCacheFactoryInterface; Chris@17: use Symfony\Component\Config\ConfigCacheInterface; Chris@0: use Symfony\Component\Translation\Exception\InvalidArgumentException; Chris@14: use Symfony\Component\Translation\Exception\LogicException; Chris@17: use Symfony\Component\Translation\Exception\NotFoundResourceException; Chris@0: use Symfony\Component\Translation\Exception\RuntimeException; Chris@14: use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface; Chris@14: use Symfony\Component\Translation\Formatter\MessageFormatter; Chris@17: use Symfony\Component\Translation\Formatter\MessageFormatterInterface; Chris@17: use Symfony\Component\Translation\Loader\LoaderInterface; Chris@0: Chris@0: /** Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class Translator implements TranslatorInterface, TranslatorBagInterface Chris@0: { Chris@0: /** Chris@0: * @var MessageCatalogueInterface[] Chris@0: */ Chris@17: protected $catalogues = []; Chris@0: Chris@0: /** Chris@0: * @var string Chris@0: */ Chris@0: private $locale; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@17: private $fallbackLocales = []; Chris@0: Chris@0: /** Chris@0: * @var LoaderInterface[] Chris@0: */ Chris@17: private $loaders = []; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@17: private $resources = []; Chris@0: Chris@0: /** Chris@14: * @var MessageFormatterInterface Chris@0: */ Chris@14: private $formatter; Chris@0: Chris@0: /** Chris@0: * @var string Chris@0: */ Chris@0: private $cacheDir; Chris@0: Chris@0: /** Chris@0: * @var bool Chris@0: */ Chris@0: private $debug; Chris@0: Chris@0: /** Chris@0: * @var ConfigCacheFactoryInterface|null Chris@0: */ Chris@0: private $configCacheFactory; Chris@0: Chris@0: /** Chris@14: * @param string $locale The locale Chris@14: * @param MessageFormatterInterface|null $formatter The message formatter Chris@14: * @param string|null $cacheDir The directory to use for the cache Chris@14: * @param bool $debug Use cache in debug mode ? Chris@0: * Chris@0: * @throws InvalidArgumentException If a locale contains invalid characters Chris@0: */ Chris@14: public function __construct($locale, $formatter = null, $cacheDir = null, $debug = false) Chris@0: { Chris@0: $this->setLocale($locale); Chris@14: Chris@14: if ($formatter instanceof MessageSelector) { Chris@14: $formatter = new MessageFormatter($formatter); Chris@17: @trigger_error(sprintf('Passing a "%s" instance into the "%s()" method as a second argument is deprecated since Symfony 3.4 and will be removed in 4.0. Inject a "%s" implementation instead.', MessageSelector::class, __METHOD__, MessageFormatterInterface::class), E_USER_DEPRECATED); Chris@14: } elseif (null === $formatter) { Chris@14: $formatter = new MessageFormatter(); Chris@14: } Chris@14: Chris@14: $this->formatter = $formatter; Chris@0: $this->cacheDir = $cacheDir; Chris@0: $this->debug = $debug; Chris@0: } Chris@0: Chris@0: public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) Chris@0: { Chris@0: $this->configCacheFactory = $configCacheFactory; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Adds a Loader. Chris@0: * Chris@0: * @param string $format The name of the loader (@see addResource()) Chris@0: * @param LoaderInterface $loader A LoaderInterface instance Chris@0: */ Chris@0: public function addLoader($format, LoaderInterface $loader) Chris@0: { Chris@0: $this->loaders[$format] = $loader; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Adds a Resource. Chris@0: * Chris@0: * @param string $format The name of the loader (@see addLoader()) Chris@0: * @param mixed $resource The resource name Chris@0: * @param string $locale The locale Chris@0: * @param string $domain The domain Chris@0: * Chris@0: * @throws InvalidArgumentException If the locale contains invalid characters Chris@0: */ Chris@0: public function addResource($format, $resource, $locale, $domain = null) Chris@0: { Chris@0: if (null === $domain) { Chris@0: $domain = 'messages'; Chris@0: } Chris@0: Chris@0: $this->assertValidLocale($locale); Chris@0: Chris@17: $this->resources[$locale][] = [$format, $resource, $domain]; Chris@0: Chris@17: if (\in_array($locale, $this->fallbackLocales)) { Chris@17: $this->catalogues = []; Chris@0: } else { Chris@0: unset($this->catalogues[$locale]); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setLocale($locale) Chris@0: { Chris@0: $this->assertValidLocale($locale); Chris@0: $this->locale = $locale; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getLocale() Chris@0: { Chris@0: return $this->locale; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the fallback locales. Chris@0: * Chris@0: * @param array $locales The fallback locales Chris@0: * Chris@0: * @throws InvalidArgumentException If a locale contains invalid characters Chris@0: */ Chris@0: public function setFallbackLocales(array $locales) Chris@0: { Chris@0: // needed as the fallback locales are linked to the already loaded catalogues Chris@17: $this->catalogues = []; Chris@0: Chris@0: foreach ($locales as $locale) { Chris@0: $this->assertValidLocale($locale); Chris@0: } Chris@0: Chris@0: $this->fallbackLocales = $locales; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the fallback locales. Chris@0: * Chris@17: * @return array The fallback locales Chris@0: */ Chris@0: public function getFallbackLocales() Chris@0: { Chris@0: return $this->fallbackLocales; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function trans($id, array $parameters = [], $domain = null, $locale = null) Chris@0: { Chris@0: if (null === $domain) { Chris@0: $domain = 'messages'; Chris@0: } Chris@0: Chris@14: return $this->formatter->format($this->getCatalogue($locale)->get((string) $id, $domain), $locale, $parameters); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) Chris@0: { Chris@14: if (!$this->formatter instanceof ChoiceMessageFormatterInterface) { Chris@17: throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter))); Chris@14: } Chris@0: Chris@0: if (null === $domain) { Chris@0: $domain = 'messages'; Chris@0: } Chris@0: Chris@0: $id = (string) $id; Chris@0: $catalogue = $this->getCatalogue($locale); Chris@0: $locale = $catalogue->getLocale(); Chris@0: while (!$catalogue->defines($id, $domain)) { Chris@0: if ($cat = $catalogue->getFallbackCatalogue()) { Chris@0: $catalogue = $cat; Chris@0: $locale = $catalogue->getLocale(); Chris@0: } else { Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@14: return $this->formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getCatalogue($locale = null) Chris@0: { Chris@0: if (null === $locale) { Chris@0: $locale = $this->getLocale(); Chris@0: } else { Chris@0: $this->assertValidLocale($locale); Chris@0: } Chris@0: Chris@0: if (!isset($this->catalogues[$locale])) { Chris@0: $this->loadCatalogue($locale); Chris@0: } Chris@0: Chris@0: return $this->catalogues[$locale]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the loaders. Chris@0: * Chris@0: * @return array LoaderInterface[] Chris@0: */ Chris@0: protected function getLoaders() Chris@0: { Chris@0: return $this->loaders; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $locale Chris@0: */ Chris@0: protected function loadCatalogue($locale) Chris@0: { Chris@0: if (null === $this->cacheDir) { Chris@0: $this->initializeCatalogue($locale); Chris@0: } else { Chris@0: $this->initializeCacheCatalogue($locale); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $locale Chris@0: */ Chris@0: protected function initializeCatalogue($locale) Chris@0: { Chris@0: $this->assertValidLocale($locale); Chris@0: Chris@0: try { Chris@0: $this->doLoadCatalogue($locale); Chris@0: } catch (NotFoundResourceException $e) { Chris@0: if (!$this->computeFallbackLocales($locale)) { Chris@0: throw $e; Chris@0: } Chris@0: } Chris@0: $this->loadFallbackCatalogues($locale); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $locale Chris@0: */ Chris@0: private function initializeCacheCatalogue($locale) Chris@0: { Chris@0: if (isset($this->catalogues[$locale])) { Chris@0: /* Catalogue already initialized. */ Chris@0: return; Chris@0: } Chris@0: Chris@0: $this->assertValidLocale($locale); Chris@0: $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale), Chris@0: function (ConfigCacheInterface $cache) use ($locale) { Chris@0: $this->dumpCatalogue($locale, $cache); Chris@0: } Chris@0: ); Chris@0: Chris@0: if (isset($this->catalogues[$locale])) { Chris@0: /* Catalogue has been initialized as it was written out to cache. */ Chris@0: return; Chris@0: } Chris@0: Chris@0: /* Read catalogue from cache. */ Chris@0: $this->catalogues[$locale] = include $cache->getPath(); Chris@0: } Chris@0: Chris@0: private function dumpCatalogue($locale, ConfigCacheInterface $cache) Chris@0: { Chris@0: $this->initializeCatalogue($locale); Chris@0: $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); Chris@0: Chris@0: $content = sprintf(<<catalogues[$locale]->all(), true), Chris@0: $fallbackContent Chris@0: ); Chris@0: Chris@0: $cache->write($content, $this->catalogues[$locale]->getResources()); Chris@0: } Chris@0: Chris@0: private function getFallbackContent(MessageCatalogue $catalogue) Chris@0: { Chris@0: $fallbackContent = ''; Chris@0: $current = ''; Chris@0: $replacementPattern = '/[^a-z0-9_]/i'; Chris@0: $fallbackCatalogue = $catalogue->getFallbackCatalogue(); Chris@0: while ($fallbackCatalogue) { Chris@0: $fallback = $fallbackCatalogue->getLocale(); Chris@0: $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); Chris@0: $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); Chris@0: Chris@0: $fallbackContent .= sprintf(<<<'EOF' Chris@0: $catalogue%s = new MessageCatalogue('%s', %s); Chris@0: $catalogue%s->addFallbackCatalogue($catalogue%s); Chris@0: Chris@0: EOF Chris@0: , Chris@0: $fallbackSuffix, Chris@0: $fallback, Chris@0: var_export($fallbackCatalogue->all(), true), Chris@0: $currentSuffix, Chris@0: $fallbackSuffix Chris@0: ); Chris@0: $current = $fallbackCatalogue->getLocale(); Chris@0: $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); Chris@0: } Chris@0: Chris@0: return $fallbackContent; Chris@0: } Chris@0: Chris@0: private function getCatalogueCachePath($locale) Chris@0: { Chris@14: return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->fallbackLocales), true)), 0, 7), '/', '_').'.php'; Chris@0: } Chris@0: Chris@0: private function doLoadCatalogue($locale) Chris@0: { Chris@0: $this->catalogues[$locale] = new MessageCatalogue($locale); Chris@0: Chris@0: if (isset($this->resources[$locale])) { Chris@0: foreach ($this->resources[$locale] as $resource) { Chris@0: if (!isset($this->loaders[$resource[0]])) { Chris@0: throw new RuntimeException(sprintf('The "%s" translation loader is not registered.', $resource[0])); Chris@0: } Chris@0: $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2])); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: private function loadFallbackCatalogues($locale) Chris@0: { Chris@0: $current = $this->catalogues[$locale]; Chris@0: Chris@0: foreach ($this->computeFallbackLocales($locale) as $fallback) { Chris@0: if (!isset($this->catalogues[$fallback])) { Chris@0: $this->initializeCatalogue($fallback); Chris@0: } Chris@0: Chris@0: $fallbackCatalogue = new MessageCatalogue($fallback, $this->catalogues[$fallback]->all()); Chris@0: foreach ($this->catalogues[$fallback]->getResources() as $resource) { Chris@0: $fallbackCatalogue->addResource($resource); Chris@0: } Chris@0: $current->addFallbackCatalogue($fallbackCatalogue); Chris@0: $current = $fallbackCatalogue; Chris@0: } Chris@0: } Chris@0: Chris@0: protected function computeFallbackLocales($locale) Chris@0: { Chris@17: $locales = []; Chris@0: foreach ($this->fallbackLocales as $fallback) { Chris@0: if ($fallback === $locale) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $locales[] = $fallback; Chris@0: } Chris@0: Chris@14: if (false !== strrchr($locale, '_')) { Chris@17: array_unshift($locales, substr($locale, 0, -\strlen(strrchr($locale, '_')))); Chris@0: } Chris@0: Chris@0: return array_unique($locales); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Asserts that the locale is valid, throws an Exception if not. Chris@0: * Chris@0: * @param string $locale Locale to tests Chris@0: * Chris@0: * @throws InvalidArgumentException If the locale contains invalid characters Chris@0: */ Chris@0: protected function assertValidLocale($locale) Chris@0: { Chris@0: if (1 !== preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) { Chris@0: throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale)); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Provides the ConfigCache factory implementation, falling back to a Chris@0: * default implementation if necessary. Chris@0: * Chris@0: * @return ConfigCacheFactoryInterface $configCacheFactory Chris@0: */ Chris@0: private function getConfigCacheFactory() Chris@0: { Chris@0: if (!$this->configCacheFactory) { Chris@0: $this->configCacheFactory = new ConfigCacheFactory($this->debug); Chris@0: } Chris@0: Chris@0: return $this->configCacheFactory; Chris@0: } Chris@0: }