composer package updates
[openemr.git] / vendor / zendframework / zend-i18n / src / Translator / Translator.php
blob578387afbb791811eacaa3ffa998de6079c2c78a
1 <?php
2 /**
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
8 */
10 namespace Zend\I18n\Translator;
12 use Locale;
13 use Traversable;
14 use Zend\Cache;
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;
25 /**
26 * Translator.
28 class Translator implements TranslatorInterface
30 /**
31 * Event fired when the translation for a message is missing.
33 const EVENT_MISSING_TRANSLATION = 'missingTranslation';
35 /**
36 * Event fired when no messages were loaded for a locale/text-domain combination.
38 const EVENT_NO_MESSAGES_LOADED = 'noMessagesLoaded';
40 /**
41 * Messages loaded by the translator.
43 * @var array
45 protected $messages = [];
47 /**
48 * Files used for loading messages.
50 * @var array
52 protected $files = [];
54 /**
55 * Patterns used for loading messages.
57 * @var array
59 protected $patterns = [];
61 /**
62 * Remote locations for loading messages.
64 * @var array
66 protected $remote = [];
68 /**
69 * Default locale.
71 * @var string
73 protected $locale;
75 /**
76 * Locale to use as fallback if there is no translation.
78 * @var string
80 protected $fallbackLocale;
82 /**
83 * Translation cache.
85 * @var CacheStorage
87 protected $cache;
89 /**
90 * Plugin manager for translation loaders.
92 * @var LoaderPluginManager
94 protected $pluginManager;
96 /**
97 * Event manager for triggering translator events.
99 * @var EventManagerInterface
101 protected $events;
104 * Whether events are enabled
106 * @var bool
108 protected $eventsEnabled = false;
111 * Instantiate a translator
113 * @param array|Traversable $options
114 * @return Translator
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"',
124 __METHOD__,
125 (is_object($options) ? get_class($options) : gettype($options))
129 $translator = new static();
131 // locales
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));
140 // file patterns
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(
159 $pattern['type'],
160 $pattern['base_dir'],
161 $pattern['pattern'],
162 isset($pattern['text_domain']) ? $pattern['text_domain'] : 'default'
167 // files
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(
186 $file['type'],
187 $file['filename'],
188 isset($file['text_domain']) ? $file['text_domain'] : 'default',
189 isset($file['locale']) ? $file['locale'] : null
194 // remote
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(
213 $remote['type'],
214 isset($remote['text_domain']) ? $remote['text_domain'] : 'default'
219 // cache
220 if (isset($options['cache'])) {
221 if ($options['cache'] instanceof CacheStorage) {
222 $translator->setCache($options['cache']);
223 } else {
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();
233 return $translator;
237 * Set the default locale.
239 * @param string $locale
240 * @return Translator
242 public function setLocale($locale)
244 $this->locale = $locale;
246 return $this;
250 * Get the default locale.
252 * @return string
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',
261 __NAMESPACE__
264 $this->locale = Locale::getDefault();
267 return $this->locale;
271 * Set the fallback locale.
273 * @param string $locale
274 * @return Translator
276 public function setFallbackLocale($locale)
278 $this->fallbackLocale = $locale;
280 return $this;
284 * Get the fallback locale.
286 * @return string
288 public function getFallbackLocale()
290 return $this->fallbackLocale;
294 * Sets a cache
296 * @param CacheStorage $cache
297 * @return Translator
299 public function setCache(CacheStorage $cache = null)
301 $this->cache = $cache;
303 return $this;
307 * Returns the set cache
309 * @return CacheStorage The set cache
311 public function getCache()
313 return $this->cache;
317 * Set the plugin manager for translation loaders
319 * @param LoaderPluginManager $pluginManager
320 * @return Translator
322 public function setPluginManager(LoaderPluginManager $pluginManager)
324 $this->pluginManager = $pluginManager;
326 return $this;
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
351 * @return string
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 !== '') {
359 return $translation;
362 if (null !== ($fallbackLocale = $this->getFallbackLocale())
363 && $locale !== $fallbackLocale
365 return $this->translate($message, $textDomain, $fallbackLocale);
368 return $message;
372 * Translate a plural message.
374 * @param string $singular
375 * @param string $plural
376 * @param int $number
377 * @param string $textDomain
378 * @param string|null $locale
379 * @return string
380 * @throws Exception\OutOfBoundsException
382 public function translatePlural(
383 $singular,
384 $plural,
385 $number,
386 $textDomain = 'default',
387 $locale = null
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(
397 $singular,
398 $plural,
399 $number,
400 $textDomain,
401 $fallbackLocale
405 return ($number == 1 ? $singular : $plural);
406 } elseif (is_string($translation)) {
407 $translation = [$translation];
410 $index = $this->messages[$textDomain][$locale]
411 ->getPluralRule()
412 ->evaluate($number);
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(
433 $message,
434 $locale,
435 $textDomain = 'default'
437 if ($message === '' || $message === null) {
438 return '';
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,
474 'locale' => $locale,
475 'text_domain' => $textDomain,
478 $results = $this->getEventManager()->triggerEventUntil($until, $event);
480 $last = $results->last();
481 if (is_string($last)) {
482 return $last;
486 return;
490 * Add a translation file.
492 * @param string $type
493 * @param string $filename
494 * @param string $textDomain
495 * @param string $locale
496 * @return Translator
498 public function addTranslationFile(
499 $type,
500 $filename,
501 $textDomain = 'default',
502 $locale = null
504 $locale = $locale ?: '*';
506 if (! isset($this->files[$textDomain])) {
507 $this->files[$textDomain] = [];
510 $this->files[$textDomain][$locale][] = [
511 'type' => $type,
512 'filename' => $filename,
515 return $this;
519 * Add multiple translations with a file pattern.
521 * @param string $type
522 * @param string $baseDir
523 * @param string $pattern
524 * @param string $textDomain
525 * @return Translator
527 public function addTranslationFilePattern(
528 $type,
529 $baseDir,
530 $pattern,
531 $textDomain = 'default'
533 if (! isset($this->patterns[$textDomain])) {
534 $this->patterns[$textDomain] = [];
537 $this->patterns[$textDomain][] = [
538 'type' => $type,
539 'baseDir' => rtrim($baseDir, '/'),
540 'pattern' => $pattern,
543 return $this;
547 * Add remote translations.
549 * @param string $type
550 * @param string $textDomain
551 * @return Translator
553 public function addRemoteTranslations($type, $textDomain = 'default')
555 if (! isset($this->remote[$textDomain])) {
556 $this->remote[$textDomain] = [];
559 $this->remote[$textDomain][] = $type;
561 return $this;
565 * Get the cache identifier for a specific textDomain and locale.
567 * @param string $textDomain
568 * @param string $locale
569 * @return string
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
581 * @return bool
583 public function clearCache($textDomain, $locale)
585 if (null === ($cache = $this->getCache())) {
586 return false;
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
598 * @return void
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;
612 return;
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, [
629 'locale' => $locale,
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
655 * @return bool
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));
672 } else {
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
688 * @return bool
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));
708 } else {
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
725 * @return bool
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])) {
734 continue;
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']));
746 } else {
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
765 * @return mixed
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
796 * @return Translator
798 public function setEventManager(EventManagerInterface $events)
800 $events->setIdentifiers([
801 __CLASS__,
802 get_class($this),
803 'translator',
805 $this->events = $events;
806 return $this;
810 * Check whether the event manager is enabled.
812 * @return boolean
814 public function isEventManagerEnabled()
816 return $this->eventsEnabled;
820 * Enable the event manager.
822 * @return Translator
824 public function enableEventManager()
826 $this->eventsEnabled = true;
827 return $this;
831 * Disable the event manager.
833 * @return Translator
835 public function disableEventManager()
837 $this->eventsEnabled = false;
838 return $this;