[ZF-2736] Zend_Translate:
[zend.git] / library / Zend / Translate / Adapter.php
blobe050da038f1f1bc6fba3f6c7dd97679a31f2af77
1 <?php
2 /**
3 * Zend Framework
5 * LICENSE
7 * This source file is subject to the new BSD license that is bundled
8 * with this package in the file LICENSE.txt.
9 * It is also available through the world-wide-web at this URL:
10 * http://framework.zend.com/license/new-bsd
11 * If you did not receive a copy of the license and are unable to
12 * obtain it through the world-wide-web, please send an email
13 * to license@zend.com so we can send you a copy immediately.
15 * @category Zend
16 * @package Zend_Translate
17 * @subpackage Zend_Translate_Adapter
18 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
19 * @license http://framework.zend.com/license/new-bsd New BSD License
20 * @version $Id$
23 /**
24 * @see Zend_Locale
26 require_once 'Zend/Locale.php';
28 /**
29 * @see Zend_Translate_Plural
31 require_once 'Zend/Translate/Plural.php';
33 /**
34 * Basic adapter class for each translation source adapter
36 * @category Zend
37 * @package Zend_Translate
38 * @subpackage Zend_Translate_Adapter
39 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
40 * @license http://framework.zend.com/license/new-bsd New BSD License
42 abstract class Zend_Translate_Adapter {
43 /**
44 * Shows if locale detection is in automatic level
45 * @var boolean
47 private $_automatic = true;
49 /**
50 * Internal value to see already routed languages
51 * @var array()
53 private $_routed = array();
55 /**
56 * Internal cache for all adapters
57 * @var Zend_Cache_Core
59 protected static $_cache = null;
61 /**
62 * Scans for the locale within the name of the directory
63 * @constant integer
65 const LOCALE_DIRECTORY = 'directory';
67 /**
68 * Scans for the locale within the name of the file
69 * @constant integer
71 const LOCALE_FILENAME = 'filename';
73 /**
74 * Array with all options, each adapter can have own additional options
75 * 'clear' => when true, clears already loaded translations when adding new files
76 * 'content' => content to translate or file or directory with content
77 * 'disableNotices' => when true, omits notices from being displayed
78 * 'ignore' => a prefix for files and directories which are not being added
79 * 'locale' => the actual set locale to use
80 * 'log' => a instance of Zend_Log where logs are written to
81 * 'logMessage' => message to be logged
82 * 'logUntranslated' => when true, untranslated messages are not logged
83 * 'reload' => reloads the cache by reading the content again
84 * 'scan' => searches for translation files using the LOCALE constants
86 * @var array
88 protected $_options = array(
89 'clear' => false,
90 'content' => null,
91 'disableNotices' => false,
92 'ignore' => '.',
93 'locale' => 'auto',
94 'log' => null,
95 'logMessage' => "Untranslated message within '%locale%': %message%",
96 'logUntranslated' => false,
97 'reload' => false,
98 'route' => null,
99 'scan' => null
103 * Translation table
104 * @var array
106 protected $_translate = array();
109 * Generates the adapter
111 * @param array|Zend_Config $options Translation options for this adapter
112 * @throws Zend_Translate_Exception
113 * @return void
115 public function __construct($options = array())
117 if ($options instanceof Zend_Config) {
118 $options = $options->toArray();
119 } else if (func_num_args() > 1) {
120 $args = func_get_args();
121 $options = array();
122 $options['content'] = array_shift($args);
124 if (!empty($args)) {
125 $options['locale'] = array_shift($args);
128 if (!empty($args)) {
129 $opt = array_shift($args);
130 $options = array_merge($opt, $options);
132 } else if (!is_array($options)) {
133 $options = array('content' => $options);
136 if (isset(self::$_cache)) {
137 $id = 'Zend_Translate_' . $this->toString() . '_Options';
138 $result = self::$_cache->load($id);
139 if ($result) {
140 $this->_options = $result;
144 if (empty($options['locale']) || ($options['locale'] === "auto")) {
145 $this->_automatic = true;
146 } else {
147 $this->_automatic = false;
150 $locale = null;
151 if (!empty($options['locale'])) {
152 $locale = $options['locale'];
153 unset($options['locale']);
156 $this->setOptions($options);
157 $options['locale'] = $locale;
159 if (!empty($options['content'])) {
160 $this->addTranslation($options);
163 if ($this->getLocale() !== (string) $options['locale']) {
164 $this->setLocale($options['locale']);
169 * Add translations
171 * This may be a new language or additional content for an existing language
172 * If the key 'clear' is true, then translations for the specified
173 * language will be replaced and added otherwise
175 * @param array|Zend_Config $options Options and translations to be added
176 * @throws Zend_Translate_Exception
177 * @return Zend_Translate_Adapter Provides fluent interface
179 public function addTranslation($options = array())
181 if ($options instanceof Zend_Config) {
182 $options = $options->toArray();
183 } else if (func_num_args() > 1) {
184 $args = func_get_args();
185 $options = array();
186 $options['content'] = array_shift($args);
188 if (!empty($args)) {
189 $options['locale'] = array_shift($args);
192 if (!empty($args)) {
193 $opt = array_shift($args);
194 $options = array_merge($opt, $options);
196 } else if (!is_array($options)) {
197 $options = array('content' => $options);
200 $originate = null;
201 if (!empty($options['locale'])) {
202 $originate = (string) $options['locale'];
205 if ((array_key_exists('log', $options)) && !($options['log'] instanceof Zend_Log)) {
206 require_once 'Zend/Translate/Exception.php';
207 throw new Zend_Translate_Exception('Instance of Zend_Log expected for option log');
210 try {
211 if (!($options['content'] instanceof Zend_Translate) && !($options['content'] instanceof Zend_Translate_Adapter)) {
212 if (empty($options['locale'])) {
213 $options['locale'] = null;
216 $options['locale'] = Zend_Locale::findLocale($options['locale']);
218 } catch (Zend_Locale_Exception $e) {
219 require_once 'Zend/Translate/Exception.php';
220 throw new Zend_Translate_Exception("The given Language '{$options['locale']}' does not exist", 0, $e);
223 $options = $options + $this->_options;
224 if (is_string($options['content']) and is_dir($options['content'])) {
225 $options['content'] = realpath($options['content']);
226 $prev = '';
227 foreach (new RecursiveIteratorIterator(
228 new RecursiveDirectoryIterator($options['content'], RecursiveDirectoryIterator::KEY_AS_PATHNAME),
229 RecursiveIteratorIterator::SELF_FIRST) as $directory => $info) {
230 $file = $info->getFilename();
231 if (is_array($options['ignore'])) {
232 foreach ($options['ignore'] as $key => $ignore) {
233 if (strpos($key, 'regex') !== false) {
234 if (preg_match($ignore, $directory)) {
235 // ignore files matching the given regex from option 'ignore' and all files below
236 continue 2;
238 } else if (strpos($directory, DIRECTORY_SEPARATOR . $ignore) !== false) {
239 // ignore files matching first characters from option 'ignore' and all files below
240 continue 2;
243 } else {
244 if (strpos($directory, DIRECTORY_SEPARATOR . $options['ignore']) !== false) {
245 // ignore files matching first characters from option 'ignore' and all files below
246 continue;
250 if ($info->isDir()) {
251 // pathname as locale
252 if (($options['scan'] === self::LOCALE_DIRECTORY) and (Zend_Locale::isLocale($file, true, false))) {
253 $options['locale'] = $file;
254 $prev = (string) $options['locale'];
256 } else if ($info->isFile()) {
257 // filename as locale
258 if ($options['scan'] === self::LOCALE_FILENAME) {
259 $filename = explode('.', $file);
260 array_pop($filename);
261 $filename = implode('.', $filename);
262 if (Zend_Locale::isLocale((string) $filename, true, false)) {
263 $options['locale'] = (string) $filename;
264 } else {
265 $parts = explode('.', $file);
266 $parts2 = array();
267 foreach($parts as $token) {
268 $parts2 += explode('_', $token);
270 $parts = array_merge($parts, $parts2);
271 $parts2 = array();
272 foreach($parts as $token) {
273 $parts2 += explode('-', $token);
275 $parts = array_merge($parts, $parts2);
276 $parts = array_unique($parts);
277 $prev = '';
278 foreach($parts as $token) {
279 if (Zend_Locale::isLocale($token, true, false)) {
280 if (strlen($prev) <= strlen($token)) {
281 $options['locale'] = $token;
282 $prev = $token;
289 try {
290 $options['content'] = $info->getPathname();
291 $this->_addTranslationData($options);
292 } catch (Zend_Translate_Exception $e) {
293 // ignore failed sources while scanning
297 } else {
298 $this->_addTranslationData($options);
301 if ((isset($this->_translate[$originate]) === true) and (count($this->_translate[$originate]) > 0)) {
302 $this->setLocale($originate);
305 return $this;
309 * Sets new adapter options
311 * @param array $options Adapter options
312 * @throws Zend_Translate_Exception
313 * @return Zend_Translate_Adapter Provides fluent interface
315 public function setOptions(array $options = array())
317 $change = false;
318 $locale = null;
319 foreach ($options as $key => $option) {
320 if ($key == 'locale') {
321 $locale = $option;
322 } else if ((isset($this->_options[$key]) and ($this->_options[$key] != $option)) or
323 !isset($this->_options[$key])) {
324 if (($key == 'log') && !($option instanceof Zend_Log)) {
325 require_once 'Zend/Translate/Exception.php';
326 throw new Zend_Translate_Exception('Instance of Zend_Log expected for option log');
329 $this->_options[$key] = $option;
330 $change = true;
334 if ($locale !== null) {
335 $this->setLocale($locale);
338 if (isset(self::$_cache) and ($change == true)) {
339 $id = 'Zend_Translate_' . $this->toString() . '_Options';
340 self::$_cache->save($this->_options, $id, array('Zend_Translate'));
343 return $this;
347 * Returns the adapters name and it's options
349 * @param string|null $optionKey String returns this option
350 * null returns all options
351 * @return integer|string|array|null
353 public function getOptions($optionKey = null)
355 if ($optionKey === null) {
356 return $this->_options;
359 if (isset($this->_options[$optionKey]) === true) {
360 return $this->_options[$optionKey];
363 return null;
367 * Gets locale
369 * @return Zend_Locale|string|null
371 public function getLocale()
373 return $this->_options['locale'];
377 * Sets locale
379 * @param string|Zend_Locale $locale Locale to set
380 * @throws Zend_Translate_Exception
381 * @return Zend_Translate_Adapter Provides fluent interface
383 public function setLocale($locale)
385 if (($locale === "auto") or ($locale === null)) {
386 $this->_automatic = true;
387 } else {
388 $this->_automatic = false;
391 try {
392 $locale = Zend_Locale::findLocale($locale);
393 } catch (Zend_Locale_Exception $e) {
394 require_once 'Zend/Translate/Exception.php';
395 throw new Zend_Translate_Exception("The given Language ({$locale}) does not exist", 0, $e);
398 if (!isset($this->_translate[$locale])) {
399 $temp = explode('_', $locale);
400 if (!isset($this->_translate[$temp[0]]) and !isset($this->_translate[$locale])) {
401 if (!$this->_options['disableNotices']) {
402 if ($this->_options['log']) {
403 $this->_options['log']->notice("The language '{$locale}' has to be added before it can be used.");
404 } else {
405 trigger_error("The language '{$locale}' has to be added before it can be used.", E_USER_NOTICE);
410 $locale = $temp[0];
413 if (empty($this->_translate[$locale])) {
414 if (!$this->_options['disableNotices']) {
415 if ($this->_options['log']) {
416 $this->_options['log']->notice("No translation for the language '{$locale}' available.");
417 } else {
418 trigger_error("No translation for the language '{$locale}' available.", E_USER_NOTICE);
423 if ($this->_options['locale'] != $locale) {
424 $this->_options['locale'] = $locale;
426 if (isset(self::$_cache)) {
427 $id = 'Zend_Translate_' . $this->toString() . '_Options';
428 self::$_cache->save($this->_options, $id, array('Zend_Translate'));
432 return $this;
436 * Returns the available languages from this adapter
438 * @return array
440 public function getList()
442 $list = array_keys($this->_translate);
443 $result = null;
444 foreach($list as $value) {
445 if (!empty($this->_translate[$value])) {
446 $result[$value] = $value;
449 return $result;
453 * Returns the message id for a given translation
454 * If no locale is given, the actual language will be used
456 * @param string $message Message to get the key for
457 * @param string|Zend_Locale $locale (optional) Language to return the message ids from
458 * @return string|array|false
460 public function getMessageId($message, $locale = null)
462 if (empty($locale) or !$this->isAvailable($locale)) {
463 $locale = $this->_options['locale'];
466 return array_search($message, $this->_translate[(string) $locale]);
470 * Returns all available message ids from this adapter
471 * If no locale is given, the actual language will be used
473 * @param string|Zend_Locale $locale (optional) Language to return the message ids from
474 * @return array
476 public function getMessageIds($locale = null)
478 if (empty($locale) or !$this->isAvailable($locale)) {
479 $locale = $this->_options['locale'];
482 return array_keys($this->_translate[(string) $locale]);
486 * Returns all available translations from this adapter
487 * If no locale is given, the actual language will be used
488 * If 'all' is given the complete translation dictionary will be returned
490 * @param string|Zend_Locale $locale (optional) Language to return the messages from
491 * @return array
493 public function getMessages($locale = null)
495 if ($locale === 'all') {
496 return $this->_translate;
499 if ((empty($locale) === true) or ($this->isAvailable($locale) === false)) {
500 $locale = $this->_options['locale'];
503 return $this->_translate[(string) $locale];
507 * Is the wished language available ?
509 * @see Zend_Locale
510 * @param string|Zend_Locale $locale Language to search for, identical with locale identifier,
511 * @see Zend_Locale for more information
512 * @return boolean
514 public function isAvailable($locale)
516 $return = isset($this->_translate[(string) $locale]);
517 return $return;
521 * Load translation data
523 * @param mixed $data
524 * @param string|Zend_Locale $locale
525 * @param array $options (optional)
526 * @return array
528 abstract protected function _loadTranslationData($data, $locale, array $options = array());
531 * Internal function for adding translation data
533 * This may be a new language or additional data for an existing language
534 * If the options 'clear' is true, then the translation data for the specified
535 * language is replaced and added otherwise
537 * @see Zend_Locale
538 * @param array|Zend_Config $content Translation data to add
539 * @throws Zend_Translate_Exception
540 * @return Zend_Translate_Adapter Provides fluent interface
542 private function _addTranslationData($options = array())
544 if ($options instanceof Zend_Config) {
545 $options = $options->toArray();
546 } else if (func_num_args() > 1) {
547 $args = func_get_args();
548 $options['content'] = array_shift($args);
550 if (!empty($args)) {
551 $options['locale'] = array_shift($args);
554 if (!empty($args)) {
555 $options += array_shift($args);
559 if (($options['content'] instanceof Zend_Translate) || ($options['content'] instanceof Zend_Translate_Adapter)) {
560 $options['usetranslateadapter'] = true;
561 if (!empty($options['locale'])) {
562 $options['content'] = $options['content']->getMessages($options['locale']);
563 } else {
564 $locales = $options['content']->getList();
565 foreach ($locales as $locale) {
566 $options['locale'] = $locale;
567 $options['content'] = $options['content']->getMessages($locale);
568 $this->_addTranslationData($options);
571 return $this;
575 try {
576 $options['locale'] = Zend_Locale::findLocale($options['locale']);
577 } catch (Zend_Locale_Exception $e) {
578 require_once 'Zend/Translate/Exception.php';
579 throw new Zend_Translate_Exception("The given Language '{$options['locale']}' does not exist", 0, $e);
582 if ($options['clear'] || !isset($this->_translate[$options['locale']])) {
583 $this->_translate[$options['locale']] = array();
586 $read = true;
587 if (isset(self::$_cache)) {
588 $id = 'Zend_Translate_' . md5(serialize($options['content'])) . '_' . $this->toString();
589 $temp = self::$_cache->load($id);
590 if ($temp) {
591 $read = false;
595 if ($options['reload']) {
596 $read = true;
599 if ($read) {
600 if (!empty($options['usetranslateadapter'])) {
601 $temp = array($options['locale'] => $options['content']);
602 } else {
603 $temp = $this->_loadTranslationData($options['content'], $options['locale'], $options);
607 if (empty($temp)) {
608 $temp = array();
611 $keys = array_keys($temp);
612 foreach($keys as $key) {
613 if (!isset($this->_translate[$key])) {
614 $this->_translate[$key] = array();
617 if (array_key_exists($key, $temp) && is_array($temp[$key])) {
618 $this->_translate[$key] = $temp[$key] + $this->_translate[$key];
622 if ($this->_automatic === true) {
623 $find = new Zend_Locale($options['locale']);
624 $browser = $find->getEnvironment() + $find->getBrowser();
625 arsort($browser);
626 foreach($browser as $language => $quality) {
627 if (isset($this->_translate[$language])) {
628 $this->_options['locale'] = $language;
629 break;
634 if (($read) and (isset(self::$_cache))) {
635 $id = 'Zend_Translate_' . md5(serialize($options['content'])) . '_' . $this->toString();
636 self::$_cache->save($temp, $id, array('Zend_Translate'));
639 return $this;
643 * Translates the given string
644 * returns the translation
646 * @see Zend_Locale
647 * @param string|array $messageId Translation string, or Array for plural translations
648 * @param string|Zend_Locale $locale (optional) Locale/Language to use, identical with
649 * locale identifier, @see Zend_Locale for more information
650 * @return string
652 public function translate($messageId, $locale = null)
654 if ($locale === null) {
655 $locale = $this->_options['locale'];
658 $plural = null;
659 if (is_array($messageId)) {
660 if (count($messageId) > 2) {
661 $number = array_pop($messageId);
662 if (!is_numeric($number)) {
663 $plocale = $number;
664 $number = array_pop($messageId);
665 } else {
666 $plocale = 'en';
669 $plural = $messageId;
670 $messageId = $messageId[0];
671 } else {
672 $messageId = $messageId[0];
676 if (!Zend_Locale::isLocale($locale, true, false)) {
677 if (!Zend_Locale::isLocale($locale, false, false)) {
678 // language does not exist, return original string
679 $this->_log($messageId, $locale);
680 // use rerouting when enabled
681 if (!empty($this->_options['route'])) {
682 if (array_key_exists($locale, $this->_options['route']) &&
683 !array_key_exists($locale, $this->_routed)) {
684 $this->_routed[$locale] = true;
685 return $this->translate($messageId, $this->_options['route'][$locale]);
688 $this->_routed = array();
691 if ($plural === null) {
692 return $messageId;
695 $rule = Zend_Translate_Plural::getPlural($number, $plocale);
696 if (!isset($plural[$rule])) {
697 $rule = 0;
700 return $plural[$rule];
703 $locale = new Zend_Locale($locale);
706 $locale = (string) $locale;
707 if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
708 // return original translation
709 if ($plural === null) {
710 return $this->_translate[$locale][$messageId];
713 $rule = Zend_Translate_Plural::getPlural($number, $locale);
714 if (isset($this->_translate[$locale][$plural[0]][$rule])) {
715 return $this->_translate[$locale][$plural[0]][$rule];
717 } else if (strlen($locale) != 2) {
718 // faster than creating a new locale and separate the leading part
719 $locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
721 if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
722 // return regionless translation (en_US -> en)
723 if ($plural === null) {
724 return $this->_translate[$locale][$messageId];
727 $rule = Zend_Translate_Plural::getPlural($number, $locale);
728 if (isset($this->_translate[$locale][$plural[0]][$rule])) {
729 return $this->_translate[$locale][$plural[0]][$rule];
734 $this->_log($messageId, $locale);
735 // use rerouting when enabled
736 if (!empty($this->_options['route'])) {
737 if (array_key_exists($locale, $this->_options['route']) &&
738 !array_key_exists($locale, $this->_routed)) {
739 $this->_routed[$locale] = true;
740 return $this->translate($messageId, $this->_options['route'][$locale]);
743 $this->_routed = array();
746 if ($plural === null) {
747 return $messageId;
750 $rule = Zend_Translate_Plural::getPlural($number, $plocale);
751 if (!isset($plural[$rule])) {
752 $rule = 0;
755 return $plural[$rule];
759 * Translates the given string using plural notations
760 * Returns the translated string
762 * @see Zend_Locale
763 * @param string $singular Singular translation string
764 * @param string $plural Plural translation string
765 * @param integer $number Number for detecting the correct plural
766 * @param string|Zend_Locale $locale (Optional) Locale/Language to use, identical with
767 * locale identifier, @see Zend_Locale for more information
768 * @return string
770 public function plural($singular, $plural, $number, $locale = null)
772 return $this->translate(array($singular, $plural, $number), $locale);
776 * Logs a message when the log option is set
778 * @param string $message Message to log
779 * @param String $locale Locale to log
781 protected function _log($message, $locale) {
782 if ($this->_options['logUntranslated']) {
783 $message = str_replace('%message%', $message, $this->_options['logMessage']);
784 $message = str_replace('%locale%', $locale, $message);
785 if ($this->_options['log']) {
786 $this->_options['log']->notice($message);
787 } else {
788 trigger_error($message, E_USER_NOTICE);
794 * Translates the given string
795 * returns the translation
797 * @param string $messageId Translation string
798 * @param string|Zend_Locale $locale (optional) Locale/Language to use, identical with locale
799 * identifier, @see Zend_Locale for more information
800 * @return string
802 public function _($messageId, $locale = null)
804 return $this->translate($messageId, $locale);
808 * Checks if a string is translated within the source or not
809 * returns boolean
811 * @param string $messageId Translation string
812 * @param boolean $original (optional) Allow translation only for original language
813 * when true, a translation for 'en_US' would give false when it can
814 * be translated with 'en' only
815 * @param string|Zend_Locale $locale (optional) Locale/Language to use, identical with locale identifier,
816 * see Zend_Locale for more information
817 * @return boolean
819 public function isTranslated($messageId, $original = false, $locale = null)
821 if (($original !== false) and ($original !== true)) {
822 $locale = $original;
823 $original = false;
826 if ($locale === null) {
827 $locale = $this->_options['locale'];
830 if (!Zend_Locale::isLocale($locale, true, false)) {
831 if (!Zend_Locale::isLocale($locale, false, false)) {
832 // language does not exist, return original string
833 return false;
836 $locale = new Zend_Locale($locale);
839 $locale = (string) $locale;
840 if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
841 // return original translation
842 return true;
843 } else if ((strlen($locale) != 2) and ($original === false)) {
844 // faster than creating a new locale and separate the leading part
845 $locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
847 if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
848 // return regionless translation (en_US -> en)
849 return true;
853 // No translation found, return original
854 return false;
858 * Returns the set cache
860 * @return Zend_Cache_Core The set cache
862 public static function getCache()
864 return self::$_cache;
868 * Sets a cache for all Zend_Translate_Adapters
870 * @param Zend_Cache_Core $cache Cache to store to
872 public static function setCache(Zend_Cache_Core $cache)
874 self::$_cache = $cache;
878 * Returns true when a cache is set
880 * @return boolean
882 public static function hasCache()
884 if (self::$_cache !== null) {
885 return true;
888 return false;
892 * Removes any set cache
894 * @return void
896 public static function removeCache()
898 self::$_cache = null;
902 * Clears all set cache data
904 * @return void
906 public static function clearCache()
908 require_once 'Zend/Cache.php';
909 self::$_cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('Zend_Translate'));
913 * Returns the adapter name
915 * @return string
917 abstract public function toString();