3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
10 namespace Zend\I18n\Translator
;
15 use Zend\Cache\Storage\StorageInterface
as CacheStorage
;
16 use Zend\EventManager\Event
;
17 use Zend\EventManager\EventManager
;
18 use Zend\EventManager\EventManagerInterface
;
19 use Zend\I18n\Exception
;
20 use Zend\I18n\Translator\Loader\FileLoaderInterface
;
21 use Zend\I18n\Translator\Loader\RemoteLoaderInterface
;
22 use Zend\Stdlib\ArrayUtils
;
23 use Zend\ServiceManager\ServiceManager
;
28 class Translator
implements TranslatorInterface
31 * Event fired when the translation for a message is missing.
33 const EVENT_MISSING_TRANSLATION
= 'missingTranslation';
36 * Event fired when no messages were loaded for a locale/text-domain combination.
38 const EVENT_NO_MESSAGES_LOADED
= 'noMessagesLoaded';
41 * Messages loaded by the translator.
45 protected $messages = [];
48 * Files used for loading messages.
52 protected $files = [];
55 * Patterns used for loading messages.
59 protected $patterns = [];
62 * Remote locations for loading messages.
66 protected $remote = [];
76 * Locale to use as fallback if there is no translation.
80 protected $fallbackLocale;
90 * Plugin manager for translation loaders.
92 * @var LoaderPluginManager
94 protected $pluginManager;
97 * Event manager for triggering translator events.
99 * @var EventManagerInterface
104 * Whether events are enabled
108 protected $eventsEnabled = false;
111 * Instantiate a translator
113 * @param array|Traversable $options
115 * @throws Exception\InvalidArgumentException
117 public static function factory($options)
119 if ($options instanceof Traversable
) {
120 $options = ArrayUtils
::iteratorToArray($options);
121 } elseif (! is_array($options)) {
122 throw new Exception\
InvalidArgumentException(sprintf(
123 '%s expects an array or Traversable object; received "%s"',
125 (is_object($options) ?
get_class($options) : gettype($options))
129 $translator = new static();
132 if (isset($options['locale'])) {
133 $locales = (array) $options['locale'];
134 $translator->setLocale(array_shift($locales));
135 if (count($locales) > 0) {
136 $translator->setFallbackLocale(array_shift($locales));
141 if (isset($options['translation_file_patterns'])) {
142 if (! is_array($options['translation_file_patterns'])) {
143 throw new Exception\
InvalidArgumentException(
144 '"translation_file_patterns" should be an array'
148 $requiredKeys = ['type', 'base_dir', 'pattern'];
149 foreach ($options['translation_file_patterns'] as $pattern) {
150 foreach ($requiredKeys as $key) {
151 if (! isset($pattern[$key])) {
152 throw new Exception\
InvalidArgumentException(
153 "'{$key}' is missing for translation pattern options"
158 $translator->addTranslationFilePattern(
160 $pattern['base_dir'],
162 isset($pattern['text_domain']) ?
$pattern['text_domain'] : 'default'
168 if (isset($options['translation_files'])) {
169 if (! is_array($options['translation_files'])) {
170 throw new Exception\
InvalidArgumentException(
171 '"translation_files" should be an array'
175 $requiredKeys = ['type', 'filename'];
176 foreach ($options['translation_files'] as $file) {
177 foreach ($requiredKeys as $key) {
178 if (! isset($file[$key])) {
179 throw new Exception\
InvalidArgumentException(
180 "'{$key}' is missing for translation file options"
185 $translator->addTranslationFile(
188 isset($file['text_domain']) ?
$file['text_domain'] : 'default',
189 isset($file['locale']) ?
$file['locale'] : null
195 if (isset($options['remote_translation'])) {
196 if (! is_array($options['remote_translation'])) {
197 throw new Exception\
InvalidArgumentException(
198 '"remote_translation" should be an array'
202 $requiredKeys = ['type'];
203 foreach ($options['remote_translation'] as $remote) {
204 foreach ($requiredKeys as $key) {
205 if (! isset($remote[$key])) {
206 throw new Exception\
InvalidArgumentException(
207 "'{$key}' is missing for remote translation options"
212 $translator->addRemoteTranslations(
214 isset($remote['text_domain']) ?
$remote['text_domain'] : 'default'
220 if (isset($options['cache'])) {
221 if ($options['cache'] instanceof CacheStorage
) {
222 $translator->setCache($options['cache']);
224 $translator->setCache(Cache\StorageFactory
::factory($options['cache']));
228 // event manager enabled
229 if (isset($options['event_manager_enabled']) && $options['event_manager_enabled']) {
230 $translator->enableEventManager();
237 * Set the default locale.
239 * @param string $locale
242 public function setLocale($locale)
244 $this->locale
= $locale;
250 * Get the default locale.
253 * @throws Exception\ExtensionNotLoadedException if ext/intl is not present and no locale set
255 public function getLocale()
257 if ($this->locale
=== null) {
258 if (! extension_loaded('intl')) {
259 throw new Exception\
ExtensionNotLoadedException(sprintf(
260 '%s component requires the intl PHP extension',
264 $this->locale
= Locale
::getDefault();
267 return $this->locale
;
271 * Set the fallback locale.
273 * @param string $locale
276 public function setFallbackLocale($locale)
278 $this->fallbackLocale
= $locale;
284 * Get the fallback locale.
288 public function getFallbackLocale()
290 return $this->fallbackLocale
;
296 * @param CacheStorage $cache
299 public function setCache(CacheStorage
$cache = null)
301 $this->cache
= $cache;
307 * Returns the set cache
309 * @return CacheStorage The set cache
311 public function getCache()
317 * Set the plugin manager for translation loaders
319 * @param LoaderPluginManager $pluginManager
322 public function setPluginManager(LoaderPluginManager
$pluginManager)
324 $this->pluginManager
= $pluginManager;
330 * Retrieve the plugin manager for translation loaders.
332 * Lazy loads an instance if none currently set.
334 * @return LoaderPluginManager
336 public function getPluginManager()
338 if (! $this->pluginManager
instanceof LoaderPluginManager
) {
339 $this->setPluginManager(new LoaderPluginManager(new ServiceManager
));
342 return $this->pluginManager
;
346 * Translate a message.
348 * @param string $message
349 * @param string $textDomain
350 * @param string $locale
353 public function translate($message, $textDomain = 'default', $locale = null)
355 $locale = ($locale ?
: $this->getLocale());
356 $translation = $this->getTranslatedMessage($message, $locale, $textDomain);
358 if ($translation !== null && $translation !== '') {
362 if (null !== ($fallbackLocale = $this->getFallbackLocale())
363 && $locale !== $fallbackLocale
365 return $this->translate($message, $textDomain, $fallbackLocale);
372 * Translate a plural message.
374 * @param string $singular
375 * @param string $plural
377 * @param string $textDomain
378 * @param string|null $locale
380 * @throws Exception\OutOfBoundsException
382 public function translatePlural(
386 $textDomain = 'default',
389 $locale = $locale ?
: $this->getLocale();
390 $translation = $this->getTranslatedMessage($singular, $locale, $textDomain);
392 if ($translation === null ||
$translation === '') {
393 if (null !== ($fallbackLocale = $this->getFallbackLocale())
394 && $locale !== $fallbackLocale
396 return $this->translatePlural(
405 return ($number == 1 ?
$singular : $plural);
406 } elseif (is_string($translation)) {
407 $translation = [$translation];
410 $index = $this->messages
[$textDomain][$locale]
414 if (! isset($translation[$index])) {
415 throw new Exception\
OutOfBoundsException(
416 sprintf('Provided index %d does not exist in plural array', $index)
420 return $translation[$index];
424 * Get a translated message.
426 * @triggers getTranslatedMessage.missing-translation
427 * @param string $message
428 * @param string $locale
429 * @param string $textDomain
430 * @return string|null
432 protected function getTranslatedMessage(
435 $textDomain = 'default'
437 if ($message === '' ||
$message === null) {
441 if (! isset($this->messages
[$textDomain][$locale])) {
442 $this->loadMessages($textDomain, $locale);
445 if (isset($this->messages
[$textDomain][$locale][$message])) {
446 return $this->messages
[$textDomain][$locale][$message];
451 * issue https://github.com/zendframework/zend-i18n/issues/53
453 * storage: array:8 [â–¼
454 * "default\x04Welcome" => "Cześć"
455 * "default\x04Top %s Product" => array:3 [â–¼
456 * 0 => "Top %s Produkt"
457 * 1 => "Top %s Produkty"
458 * 2 => "Top %s Produktów"
460 * "Top %s Products" => ""
463 if (isset($this->messages
[$textDomain][$locale][$textDomain . "\x04" . $message])) {
464 return $this->messages
[$textDomain][$locale][$textDomain . "\x04" . $message];
467 if ($this->isEventManagerEnabled()) {
468 $until = function ($r) {
469 return is_string($r);
472 $event = new Event(self
::EVENT_MISSING_TRANSLATION
, $this, [
473 'message' => $message,
475 'text_domain' => $textDomain,
478 $results = $this->getEventManager()->triggerEventUntil($until, $event);
480 $last = $results->last();
481 if (is_string($last)) {
490 * Add a translation file.
492 * @param string $type
493 * @param string $filename
494 * @param string $textDomain
495 * @param string $locale
498 public function addTranslationFile(
501 $textDomain = 'default',
504 $locale = $locale ?
: '*';
506 if (! isset($this->files
[$textDomain])) {
507 $this->files
[$textDomain] = [];
510 $this->files
[$textDomain][$locale][] = [
512 'filename' => $filename,
519 * Add multiple translations with a file pattern.
521 * @param string $type
522 * @param string $baseDir
523 * @param string $pattern
524 * @param string $textDomain
527 public function addTranslationFilePattern(
531 $textDomain = 'default'
533 if (! isset($this->patterns
[$textDomain])) {
534 $this->patterns
[$textDomain] = [];
537 $this->patterns
[$textDomain][] = [
539 'baseDir' => rtrim($baseDir, '/'),
540 'pattern' => $pattern,
547 * Add remote translations.
549 * @param string $type
550 * @param string $textDomain
553 public function addRemoteTranslations($type, $textDomain = 'default')
555 if (! isset($this->remote
[$textDomain])) {
556 $this->remote
[$textDomain] = [];
559 $this->remote
[$textDomain][] = $type;
565 * Get the cache identifier for a specific textDomain and locale.
567 * @param string $textDomain
568 * @param string $locale
571 public function getCacheId($textDomain, $locale)
573 return 'Zend_I18n_Translator_Messages_' . md5($textDomain . $locale);
577 * Clears the cache for a specific textDomain and locale.
579 * @param string $textDomain
580 * @param string $locale
583 public function clearCache($textDomain, $locale)
585 if (null === ($cache = $this->getCache())) {
588 return $cache->removeItem($this->getCacheId($textDomain, $locale));
592 * Load messages for a given language and domain.
594 * @triggers loadMessages.no-messages-loaded
595 * @param string $textDomain
596 * @param string $locale
597 * @throws Exception\RuntimeException
600 protected function loadMessages($textDomain, $locale)
602 if (! isset($this->messages
[$textDomain])) {
603 $this->messages
[$textDomain] = [];
606 if (null !== ($cache = $this->getCache())) {
607 $cacheId = $this->getCacheId($textDomain, $locale);
609 if (null !== ($result = $cache->getItem($cacheId))) {
610 $this->messages
[$textDomain][$locale] = $result;
616 $messagesLoaded = false;
617 $messagesLoaded |
= $this->loadMessagesFromRemote($textDomain, $locale);
618 $messagesLoaded |
= $this->loadMessagesFromPatterns($textDomain, $locale);
619 $messagesLoaded |
= $this->loadMessagesFromFiles($textDomain, $locale);
621 if (! $messagesLoaded) {
622 $discoveredTextDomain = null;
623 if ($this->isEventManagerEnabled()) {
624 $until = function ($r) {
625 return ($r instanceof TextDomain
);
628 $event = new Event(self
::EVENT_NO_MESSAGES_LOADED
, $this, [
630 'text_domain' => $textDomain,
633 $results = $this->getEventManager()->triggerEventUntil($until, $event);
635 $last = $results->last();
636 if ($last instanceof TextDomain
) {
637 $discoveredTextDomain = $last;
641 $this->messages
[$textDomain][$locale] = $discoveredTextDomain;
642 $messagesLoaded = true;
645 if ($messagesLoaded && $cache !== null) {
646 $cache->setItem($cacheId, $this->messages
[$textDomain][$locale]);
651 * Load messages from remote sources.
653 * @param string $textDomain
654 * @param string $locale
656 * @throws Exception\RuntimeException When specified loader is not a remote loader
658 protected function loadMessagesFromRemote($textDomain, $locale)
660 $messagesLoaded = false;
662 if (isset($this->remote
[$textDomain])) {
663 foreach ($this->remote
[$textDomain] as $loaderType) {
664 $loader = $this->getPluginManager()->get($loaderType);
666 if (! $loader instanceof RemoteLoaderInterface
) {
667 throw new Exception\
RuntimeException('Specified loader is not a remote loader');
670 if (isset($this->messages
[$textDomain][$locale])) {
671 $this->messages
[$textDomain][$locale]->merge($loader->load($locale, $textDomain));
673 $this->messages
[$textDomain][$locale] = $loader->load($locale, $textDomain);
676 $messagesLoaded = true;
680 return $messagesLoaded;
684 * Load messages from patterns.
686 * @param string $textDomain
687 * @param string $locale
689 * @throws Exception\RuntimeException When specified loader is not a file loader
691 protected function loadMessagesFromPatterns($textDomain, $locale)
693 $messagesLoaded = false;
695 if (isset($this->patterns
[$textDomain])) {
696 foreach ($this->patterns
[$textDomain] as $pattern) {
697 $filename = $pattern['baseDir'] . '/' . sprintf($pattern['pattern'], $locale);
699 if (is_file($filename)) {
700 $loader = $this->getPluginManager()->get($pattern['type']);
702 if (! $loader instanceof FileLoaderInterface
) {
703 throw new Exception\
RuntimeException('Specified loader is not a file loader');
706 if (isset($this->messages
[$textDomain][$locale])) {
707 $this->messages
[$textDomain][$locale]->merge($loader->load($locale, $filename));
709 $this->messages
[$textDomain][$locale] = $loader->load($locale, $filename);
712 $messagesLoaded = true;
717 return $messagesLoaded;
721 * Load messages from files.
723 * @param string $textDomain
724 * @param string $locale
726 * @throws Exception\RuntimeException When specified loader is not a file loader
728 protected function loadMessagesFromFiles($textDomain, $locale)
730 $messagesLoaded = false;
732 foreach ([$locale, '*'] as $currentLocale) {
733 if (! isset($this->files
[$textDomain][$currentLocale])) {
737 foreach ($this->files
[$textDomain][$currentLocale] as $file) {
738 $loader = $this->getPluginManager()->get($file['type']);
740 if (! $loader instanceof FileLoaderInterface
) {
741 throw new Exception\
RuntimeException('Specified loader is not a file loader');
744 if (isset($this->messages
[$textDomain][$locale])) {
745 $this->messages
[$textDomain][$locale]->merge($loader->load($locale, $file['filename']));
747 $this->messages
[$textDomain][$locale] = $loader->load($locale, $file['filename']);
750 $messagesLoaded = true;
753 unset($this->files
[$textDomain][$currentLocale]);
756 return $messagesLoaded;
760 * Return all the messages.
762 * @param string $textDomain
763 * @param null $locale
767 public function getAllMessages($textDomain = 'default', $locale = null)
769 $locale = $locale ?
: $this->getLocale();
771 if (! isset($this->messages
[$textDomain][$locale])) {
772 $this->loadMessages($textDomain, $locale);
775 return $this->messages
[$textDomain][$locale];
779 * Get the event manager.
781 * @return EventManagerInterface|null
783 public function getEventManager()
785 if (! $this->events
instanceof EventManagerInterface
) {
786 $this->setEventManager(new EventManager());
789 return $this->events
;
793 * Set the event manager instance used by this translator.
795 * @param EventManagerInterface $events
798 public function setEventManager(EventManagerInterface
$events)
800 $events->setIdentifiers([
805 $this->events
= $events;
810 * Check whether the event manager is enabled.
814 public function isEventManagerEnabled()
816 return $this->eventsEnabled
;
820 * Enable the event manager.
824 public function enableEventManager()
826 $this->eventsEnabled
= true;
831 * Disable the event manager.
835 public function disableEventManager()
837 $this->eventsEnabled
= false;