4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Translation;
14 use Symfony\Component\Translation\Loader\LoaderInterface;
15 use Symfony\Component\Translation\Exception\NotFoundResourceException;
16 use Symfony\Component\Translation\Exception\InvalidArgumentException;
17 use Symfony\Component\Translation\Exception\LogicException;
18 use Symfony\Component\Translation\Exception\RuntimeException;
19 use Symfony\Component\Config\ConfigCacheInterface;
20 use Symfony\Component\Config\ConfigCacheFactoryInterface;
21 use Symfony\Component\Config\ConfigCacheFactory;
22 use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
23 use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface;
24 use Symfony\Component\Translation\Formatter\MessageFormatter;
27 * @author Fabien Potencier <fabien@symfony.com>
29 class Translator implements TranslatorInterface, TranslatorBagInterface
32 * @var MessageCatalogueInterface[]
34 protected $catalogues = array();
44 private $fallbackLocales = array();
47 * @var LoaderInterface[]
49 private $loaders = array();
54 private $resources = array();
57 * @var MessageFormatterInterface
72 * @var ConfigCacheFactoryInterface|null
74 private $configCacheFactory;
77 * @param string $locale The locale
78 * @param MessageFormatterInterface|null $formatter The message formatter
79 * @param string|null $cacheDir The directory to use for the cache
80 * @param bool $debug Use cache in debug mode ?
82 * @throws InvalidArgumentException If a locale contains invalid characters
84 public function __construct($locale, $formatter = null, $cacheDir = null, $debug = false)
86 $this->setLocale($locale);
88 if ($formatter instanceof MessageSelector) {
89 $formatter = new MessageFormatter($formatter);
90 @trigger_error(sprintf('Passing a "%s" instance into the "%s" 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);
91 } elseif (null === $formatter) {
92 $formatter = new MessageFormatter();
95 $this->formatter = $formatter;
96 $this->cacheDir = $cacheDir;
97 $this->debug = $debug;
100 public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
102 $this->configCacheFactory = $configCacheFactory;
108 * @param string $format The name of the loader (@see addResource())
109 * @param LoaderInterface $loader A LoaderInterface instance
111 public function addLoader($format, LoaderInterface $loader)
113 $this->loaders[$format] = $loader;
119 * @param string $format The name of the loader (@see addLoader())
120 * @param mixed $resource The resource name
121 * @param string $locale The locale
122 * @param string $domain The domain
124 * @throws InvalidArgumentException If the locale contains invalid characters
126 public function addResource($format, $resource, $locale, $domain = null)
128 if (null === $domain) {
129 $domain = 'messages';
132 $this->assertValidLocale($locale);
134 $this->resources[$locale][] = array($format, $resource, $domain);
136 if (in_array($locale, $this->fallbackLocales)) {
137 $this->catalogues = array();
139 unset($this->catalogues[$locale]);
146 public function setLocale($locale)
148 $this->assertValidLocale($locale);
149 $this->locale = $locale;
155 public function getLocale()
157 return $this->locale;
161 * Sets the fallback locales.
163 * @param array $locales The fallback locales
165 * @throws InvalidArgumentException If a locale contains invalid characters
167 public function setFallbackLocales(array $locales)
169 // needed as the fallback locales are linked to the already loaded catalogues
170 $this->catalogues = array();
172 foreach ($locales as $locale) {
173 $this->assertValidLocale($locale);
176 $this->fallbackLocales = $locales;
180 * Gets the fallback locales.
182 * @return array $locales The fallback locales
184 public function getFallbackLocales()
186 return $this->fallbackLocales;
192 public function trans($id, array $parameters = array(), $domain = null, $locale = null)
194 if (null === $domain) {
195 $domain = 'messages';
198 return $this->formatter->format($this->getCatalogue($locale)->get((string) $id, $domain), $locale, $parameters);
204 public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
206 if (!$this->formatter instanceof ChoiceMessageFormatterInterface) {
207 throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', get_class($this->formatter)));
210 if (null === $domain) {
211 $domain = 'messages';
215 $catalogue = $this->getCatalogue($locale);
216 $locale = $catalogue->getLocale();
217 while (!$catalogue->defines($id, $domain)) {
218 if ($cat = $catalogue->getFallbackCatalogue()) {
220 $locale = $catalogue->getLocale();
226 return $this->formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters);
232 public function getCatalogue($locale = null)
234 if (null === $locale) {
235 $locale = $this->getLocale();
237 $this->assertValidLocale($locale);
240 if (!isset($this->catalogues[$locale])) {
241 $this->loadCatalogue($locale);
244 return $this->catalogues[$locale];
250 * @return array LoaderInterface[]
252 protected function getLoaders()
254 return $this->loaders;
258 * @param string $locale
260 protected function loadCatalogue($locale)
262 if (null === $this->cacheDir) {
263 $this->initializeCatalogue($locale);
265 $this->initializeCacheCatalogue($locale);
270 * @param string $locale
272 protected function initializeCatalogue($locale)
274 $this->assertValidLocale($locale);
277 $this->doLoadCatalogue($locale);
278 } catch (NotFoundResourceException $e) {
279 if (!$this->computeFallbackLocales($locale)) {
283 $this->loadFallbackCatalogues($locale);
287 * @param string $locale
289 private function initializeCacheCatalogue($locale)
291 if (isset($this->catalogues[$locale])) {
292 /* Catalogue already initialized. */
296 $this->assertValidLocale($locale);
297 $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale),
298 function (ConfigCacheInterface $cache) use ($locale) {
299 $this->dumpCatalogue($locale, $cache);
303 if (isset($this->catalogues[$locale])) {
304 /* Catalogue has been initialized as it was written out to cache. */
308 /* Read catalogue from cache. */
309 $this->catalogues[$locale] = include $cache->getPath();
312 private function dumpCatalogue($locale, ConfigCacheInterface $cache)
314 $this->initializeCatalogue($locale);
315 $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]);
317 $content = sprintf(<<<EOF
320 use Symfony\Component\Translation\MessageCatalogue;
322 \$catalogue = new MessageCatalogue('%s', %s);
330 var_export($this->catalogues[$locale]->all(), true),
334 $cache->write($content, $this->catalogues[$locale]->getResources());
337 private function getFallbackContent(MessageCatalogue $catalogue)
339 $fallbackContent = '';
341 $replacementPattern = '/[^a-z0-9_]/i';
342 $fallbackCatalogue = $catalogue->getFallbackCatalogue();
343 while ($fallbackCatalogue) {
344 $fallback = $fallbackCatalogue->getLocale();
345 $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback));
346 $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current));
348 $fallbackContent .= sprintf(<<<'EOF'
349 $catalogue%s = new MessageCatalogue('%s', %s);
350 $catalogue%s->addFallbackCatalogue($catalogue%s);
356 var_export($fallbackCatalogue->all(), true),
360 $current = $fallbackCatalogue->getLocale();
361 $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
364 return $fallbackContent;
367 private function getCatalogueCachePath($locale)
369 return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->fallbackLocales), true)), 0, 7), '/', '_').'.php';
372 private function doLoadCatalogue($locale)
374 $this->catalogues[$locale] = new MessageCatalogue($locale);
376 if (isset($this->resources[$locale])) {
377 foreach ($this->resources[$locale] as $resource) {
378 if (!isset($this->loaders[$resource[0]])) {
379 throw new RuntimeException(sprintf('The "%s" translation loader is not registered.', $resource[0]));
381 $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2]));
386 private function loadFallbackCatalogues($locale)
388 $current = $this->catalogues[$locale];
390 foreach ($this->computeFallbackLocales($locale) as $fallback) {
391 if (!isset($this->catalogues[$fallback])) {
392 $this->initializeCatalogue($fallback);
395 $fallbackCatalogue = new MessageCatalogue($fallback, $this->catalogues[$fallback]->all());
396 foreach ($this->catalogues[$fallback]->getResources() as $resource) {
397 $fallbackCatalogue->addResource($resource);
399 $current->addFallbackCatalogue($fallbackCatalogue);
400 $current = $fallbackCatalogue;
404 protected function computeFallbackLocales($locale)
407 foreach ($this->fallbackLocales as $fallback) {
408 if ($fallback === $locale) {
412 $locales[] = $fallback;
415 if (false !== strrchr($locale, '_')) {
416 array_unshift($locales, substr($locale, 0, -strlen(strrchr($locale, '_'))));
419 return array_unique($locales);
423 * Asserts that the locale is valid, throws an Exception if not.
425 * @param string $locale Locale to tests
427 * @throws InvalidArgumentException If the locale contains invalid characters
429 protected function assertValidLocale($locale)
431 if (1 !== preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) {
432 throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
437 * Provides the ConfigCache factory implementation, falling back to a
438 * default implementation if necessary.
440 * @return ConfigCacheFactoryInterface $configCacheFactory
442 private function getConfigCacheFactory()
444 if (!$this->configCacheFactory) {
445 $this->configCacheFactory = new ConfigCacheFactory($this->debug);
448 return $this->configCacheFactory;