1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:
20:
21: App::uses('CakePlugin', 'Core');
22: App::uses('L10n', 'I18n');
23: App::uses('Multibyte', 'I18n');
24: App::uses('CakeSession', 'Model/Datasource');
25:
26: 27: 28: 29: 30:
31: class I18n {
32:
33: 34: 35: 36: 37:
38: public $l10n = null;
39:
40: 41: 42: 43: 44:
45: public static $defaultDomain = 'default';
46:
47: 48: 49: 50: 51:
52: public $domain = null;
53:
54: 55: 56: 57: 58:
59: public $category = 'LC_MESSAGES';
60:
61: 62: 63: 64: 65:
66: protected $_lang = null;
67:
68: 69: 70: 71: 72:
73: protected $_domains = array();
74:
75: 76: 77: 78: 79: 80:
81: protected $_noLocale = false;
82:
83: 84: 85: 86: 87:
88: protected $_categories = array(
89: 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
90: );
91:
92: 93: 94: 95: 96:
97: protected $_escape = null;
98:
99: 100: 101: 102: 103:
104: public function __construct() {
105: $this->l10n = new L10n();
106: }
107:
108: 109: 110: 111: 112:
113: public static function getInstance() {
114: static $instance = array();
115: if (!$instance) {
116: $instance[0] = new I18n();
117: }
118: return $instance[0];
119: }
120:
121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135:
136: public static function translate($singular, $plural = null, $domain = null, $category = 6, $count = null, $language = null) {
137: $_this = I18n::getInstance();
138:
139: if (strpos($singular, "\r\n") !== false) {
140: $singular = str_replace("\r\n", "\n", $singular);
141: }
142: if ($plural !== null && strpos($plural, "\r\n") !== false) {
143: $plural = str_replace("\r\n", "\n", $plural);
144: }
145:
146: if (is_numeric($category)) {
147: $_this->category = $_this->_categories[$category];
148: }
149:
150: if (empty($language)) {
151: if (CakeSession::started()) {
152: $language = CakeSession::read('Config.language');
153: }
154: if (empty($language)) {
155: $language = Configure::read('Config.language');
156: }
157: }
158:
159: if (($_this->_lang && $_this->_lang !== $language) || !$_this->_lang) {
160: $lang = $_this->l10n->get($language);
161: $_this->_lang = $lang;
162: }
163:
164: if ($domain === null) {
165: $domain = self::$defaultDomain;
166: }
167: if ($domain === '') {
168: throw new CakeException(__d('cake_dev', 'You cannot use "" as a domain.'));
169: }
170:
171: $_this->domain = $domain . '_' . $_this->l10n->lang;
172:
173: if (!isset($_this->_domains[$domain][$_this->_lang])) {
174: $_this->_domains[$domain][$_this->_lang] = Cache::read($_this->domain, '_cake_core_');
175: }
176:
177: if (!isset($_this->_domains[$domain][$_this->_lang][$_this->category])) {
178: $_this->_bindTextDomain($domain);
179: Cache::write($_this->domain, $_this->_domains[$domain][$_this->_lang], '_cake_core_');
180: }
181:
182: if ($_this->category === 'LC_TIME') {
183: return $_this->_translateTime($singular, $domain);
184: }
185:
186: if (!isset($count)) {
187: $plurals = 0;
188: } elseif (!empty($_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"]) && $_this->_noLocale === false) {
189: $header = $_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"];
190: $plurals = $_this->_pluralGuess($header, $count);
191: } else {
192: if ($count != 1) {
193: $plurals = 1;
194: } else {
195: $plurals = 0;
196: }
197: }
198:
199: if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category][$singular])) {
200: if (($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$singular]) || ($plurals) && ($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$plural])) {
201: if (is_array($trans)) {
202: if (isset($trans[$plurals])) {
203: $trans = $trans[$plurals];
204: } else {
205: trigger_error(
206: __d('cake_dev',
207: 'Missing plural form translation for "%s" in "%s" domain, "%s" locale. ' .
208: ' Check your po file for correct plurals and valid Plural-Forms header.',
209: $singular,
210: $domain,
211: $_this->_lang
212: ),
213: E_USER_WARNING
214: );
215: $trans = $trans[0];
216: }
217: }
218: if (strlen($trans)) {
219: return $trans;
220: }
221: }
222: }
223:
224: if (!empty($plurals)) {
225: return $plural;
226: }
227: return $singular;
228: }
229:
230: 231: 232: 233: 234:
235: public static function clear() {
236: $self = I18n::getInstance();
237: $self->_domains = array();
238: }
239:
240: 241: 242: 243: 244:
245: public static function domains() {
246: $self = I18n::getInstance();
247: return $self->_domains;
248: }
249:
250: 251: 252: 253: 254: 255: 256:
257: protected function _pluralGuess($header, $n) {
258: if (!is_string($header) || $header === "nplurals=1;plural=0;" || !isset($header[0])) {
259: return 0;
260: }
261:
262: if ($header === "nplurals=2;plural=n!=1;") {
263: return $n != 1 ? 1 : 0;
264: } elseif ($header === "nplurals=2;plural=n>1;") {
265: return $n > 1 ? 1 : 0;
266: }
267:
268: if (strpos($header, "plurals=3")) {
269: if (strpos($header, "100!=11")) {
270: if (strpos($header, "10<=4")) {
271: return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
272: } elseif (strpos($header, "100<10")) {
273: return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
274: }
275: return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n != 0 ? 1 : 2);
276: } elseif (strpos($header, "n==2")) {
277: return $n == 1 ? 0 : ($n == 2 ? 1 : 2);
278: } elseif (strpos($header, "n==0")) {
279: return $n == 1 ? 0 : ($n == 0 || ($n % 100 > 0 && $n % 100 < 20) ? 1 : 2);
280: } elseif (strpos($header, "n>=2")) {
281: return $n == 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2);
282: } elseif (strpos($header, "10>=2")) {
283: return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
284: }
285: return $n % 10 == 1 ? 0 : ($n % 10 == 2 ? 1 : 2);
286: } elseif (strpos($header, "plurals=4")) {
287: if (strpos($header, "100==2")) {
288: return $n % 100 == 1 ? 0 : ($n % 100 == 2 ? 1 : ($n % 100 == 3 || $n % 100 == 4 ? 2 : 3));
289: } elseif (strpos($header, "n>=3")) {
290: return $n == 1 ? 0 : ($n == 2 ? 1 : ($n == 0 || ($n >= 3 && $n <= 10) ? 2 : 3));
291: } elseif (strpos($header, "100>=1")) {
292: return $n == 1 ? 0 : ($n == 0 || ($n % 100 >= 1 && $n % 100 <= 10) ? 1 : ($n % 100 >= 11 && $n % 100 <= 20 ? 2 : 3));
293: }
294: } elseif (strpos($header, "plurals=5")) {
295: return $n == 1 ? 0 : ($n == 2 ? 1 : ($n >= 3 && $n <= 6 ? 2 : ($n >= 7 && $n <= 10 ? 3 : 4)));
296: }
297: }
298:
299: 300: 301: 302: 303: 304:
305: protected function _bindTextDomain($domain) {
306: $this->_noLocale = true;
307: $core = true;
308: $merge = array();
309: $searchPaths = App::path('locales');
310: $plugins = CakePlugin::loaded();
311:
312: if (!empty($plugins)) {
313: foreach ($plugins as $plugin) {
314: $pluginDomain = Inflector::underscore($plugin);
315: if ($pluginDomain === $domain) {
316: $searchPaths[] = CakePlugin::path($plugin) . 'Locale' . DS;
317: $searchPaths = array_reverse($searchPaths);
318: break;
319: }
320: }
321: }
322:
323: foreach ($searchPaths as $directory) {
324: foreach ($this->l10n->languagePath as $lang) {
325: $localeDef = $directory . $lang . DS . $this->category;
326: if (is_file($localeDef)) {
327: $definitions = self::loadLocaleDefinition($localeDef);
328: if ($definitions !== false) {
329: $this->_domains[$domain][$this->_lang][$this->category] = $definitions;
330: $this->_noLocale = false;
331: return $domain;
332: }
333: }
334:
335: if ($core) {
336: $app = $directory . $lang . DS . $this->category . DS . 'core';
337: $translations = false;
338:
339: if (is_file($app . '.mo')) {
340: $translations = self::loadMo($app . '.mo');
341: }
342: if ($translations === false && is_file($app . '.po')) {
343: $translations = self::loadPo($app . '.po');
344: }
345:
346: if ($translations !== false) {
347: $this->_domains[$domain][$this->_lang][$this->category] = $translations;
348: $merge[$domain][$this->_lang][$this->category] = $this->_domains[$domain][$this->_lang][$this->category];
349: $this->_noLocale = false;
350: $core = null;
351: }
352: }
353:
354: $file = $directory . $lang . DS . $this->category . DS . $domain;
355: $translations = false;
356:
357: if (is_file($file . '.mo')) {
358: $translations = self::loadMo($file . '.mo');
359: }
360: if ($translations === false && is_file($file . '.po')) {
361: $translations = self::loadPo($file . '.po');
362: }
363:
364: if ($translations !== false) {
365: $this->_domains[$domain][$this->_lang][$this->category] = $translations;
366: $this->_noLocale = false;
367: break 2;
368: }
369: }
370: }
371:
372: if (empty($this->_domains[$domain][$this->_lang][$this->category])) {
373: $this->_domains[$domain][$this->_lang][$this->category] = array();
374: return $domain;
375: }
376:
377: if (isset($this->_domains[$domain][$this->_lang][$this->category][""])) {
378: $head = $this->_domains[$domain][$this->_lang][$this->category][""];
379:
380: foreach (explode("\n", $head) as $line) {
381: $header = strtok($line, ':');
382: $line = trim(strtok("\n"));
383: $this->_domains[$domain][$this->_lang][$this->category]["%po-header"][strtolower($header)] = $line;
384: }
385:
386: if (isset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"])) {
387: $switch = preg_replace("/(?:[() {}\\[\\]^\\s*\\]]+)/", "", $this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"]);
388: $this->_domains[$domain][$this->_lang][$this->category]["%plural-c"] = $switch;
389: unset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]);
390: }
391: $this->_domains = Hash::mergeDiff($this->_domains, $merge);
392:
393: if (isset($this->_domains[$domain][$this->_lang][$this->category][null])) {
394: unset($this->_domains[$domain][$this->_lang][$this->category][null]);
395: }
396: }
397:
398: return $domain;
399: }
400:
401: 402: 403: 404: 405: 406:
407: public static function loadMo($filename) {
408: $translations = false;
409:
410:
411:
412: if ($data = file_get_contents($filename)) {
413: $translations = array();
414: $header = substr($data, 0, 20);
415: $header = unpack('L1magic/L1version/L1count/L1o_msg/L1o_trn', $header);
416: extract($header);
417:
418: if ((dechex($magic) === '950412de' || dechex($magic) === 'ffffffff950412de') && !$version) {
419: for ($n = 0; $n < $count; $n++) {
420: $r = unpack("L1len/L1offs", substr($data, $o_msg + $n * 8, 8));
421: $msgid = substr($data, $r["offs"], $r["len"]);
422: unset($msgid_plural);
423:
424: if (strpos($msgid, "\000")) {
425: list($msgid, $msgid_plural) = explode("\000", $msgid);
426: }
427: $r = unpack("L1len/L1offs", substr($data, $o_trn + $n * 8, 8));
428: $msgstr = substr($data, $r["offs"], $r["len"]);
429:
430: if (strpos($msgstr, "\000")) {
431: $msgstr = explode("\000", $msgstr);
432: }
433: $translations[$msgid] = $msgstr;
434:
435: if (isset($msgid_plural)) {
436: $translations[$msgid_plural] =& $translations[$msgid];
437: }
438: }
439: }
440: }
441:
442:
443: return $translations;
444: }
445:
446: 447: 448: 449: 450: 451:
452: public static function loadPo($filename) {
453: if (!$file = fopen($filename, 'r')) {
454: return false;
455: }
456:
457: $type = 0;
458: $translations = array();
459: $translationKey = '';
460: $plural = 0;
461: $header = '';
462:
463: do {
464: $line = trim(fgets($file));
465: if ($line === '' || $line[0] === '#') {
466: continue;
467: }
468: if (preg_match("/msgid[[:space:]]+\"(.+)\"$/i", $line, $regs)) {
469: $type = 1;
470: $translationKey = stripcslashes($regs[1]);
471: } elseif (preg_match("/msgid[[:space:]]+\"\"$/i", $line, $regs)) {
472: $type = 2;
473: $translationKey = '';
474: } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && ($type == 1 || $type == 2 || $type == 3)) {
475: $type = 3;
476: $translationKey .= stripcslashes($regs[1]);
477: } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
478: $translations[$translationKey] = stripcslashes($regs[1]);
479: $type = 4;
480: } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
481: $type = 4;
482: $translations[$translationKey] = '';
483: } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 4 && $translationKey) {
484: $translations[$translationKey] .= stripcslashes($regs[1]);
485: } elseif (preg_match("/msgid_plural[[:space:]]+\".*\"$/i", $line, $regs)) {
486: $type = 6;
487: } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 6 && $translationKey) {
488: $type = 6;
489: } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
490: $plural = $regs[1];
491: $translations[$translationKey][$plural] = stripcslashes($regs[2]);
492: $type = 7;
493: } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
494: $plural = $regs[1];
495: $translations[$translationKey][$plural] = '';
496: $type = 7;
497: } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 7 && $translationKey) {
498: $translations[$translationKey][$plural] .= stripcslashes($regs[1]);
499: } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && $type == 2 && !$translationKey) {
500: $header .= stripcslashes($regs[1]);
501: $type = 5;
502: } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && !$translationKey) {
503: $header = '';
504: $type = 5;
505: } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 5) {
506: $header .= stripcslashes($regs[1]);
507: } else {
508: unset($translations[$translationKey]);
509: $type = 0;
510: $translationKey = '';
511: $plural = 0;
512: }
513: } while (!feof($file));
514: fclose($file);
515:
516: $merge[''] = $header;
517: return array_merge($merge, $translations);
518: }
519:
520: 521: 522: 523: 524: 525:
526: public static function loadLocaleDefinition($filename) {
527: if (!$file = fopen($filename, 'r')) {
528: return false;
529: }
530:
531: $definitions = array();
532: $comment = '#';
533: $escape = '\\';
534: $currentToken = false;
535: $value = '';
536: $_this = I18n::getInstance();
537: while ($line = fgets($file)) {
538: $line = trim($line);
539: if (empty($line) || $line[0] === $comment) {
540: continue;
541: }
542: $parts = preg_split("/[[:space:]]+/", $line);
543: if ($parts[0] === 'comment_char') {
544: $comment = $parts[1];
545: continue;
546: }
547: if ($parts[0] === 'escape_char') {
548: $escape = $parts[1];
549: continue;
550: }
551: $count = count($parts);
552: if ($count == 2) {
553: $currentToken = $parts[0];
554: $value = $parts[1];
555: } elseif ($count == 1) {
556: $value .= $parts[0];
557: } else {
558: continue;
559: }
560:
561: $len = strlen($value) - 1;
562: if ($value[$len] === $escape) {
563: $value = substr($value, 0, $len);
564: continue;
565: }
566:
567: $mustEscape = array($escape . ',', $escape . ';', $escape . '<', $escape . '>', $escape . $escape);
568: $replacements = array_map('crc32', $mustEscape);
569: $value = str_replace($mustEscape, $replacements, $value);
570: $value = explode(';', $value);
571: $_this->_escape = $escape;
572: foreach ($value as $i => $val) {
573: $val = trim($val, '"');
574: $val = preg_replace_callback('/(?:<)?(.[^>]*)(?:>)?/', array(&$_this, '_parseLiteralValue'), $val);
575: $val = str_replace($replacements, $mustEscape, $val);
576: $value[$i] = $val;
577: }
578: if (count($value) == 1) {
579: $definitions[$currentToken] = array_pop($value);
580: } else {
581: $definitions[$currentToken] = $value;
582: }
583: }
584:
585: return $definitions;
586: }
587:
588: 589: 590: 591: 592: 593:
594: protected function _parseLiteralValue($string) {
595: $string = $string[1];
596: if (substr($string, 0, 2) === $this->_escape . 'x') {
597: $delimiter = $this->_escape . 'x';
598: return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string)))));
599: }
600: if (substr($string, 0, 2) === $this->_escape . 'd') {
601: $delimiter = $this->_escape . 'd';
602: return implode('', array_map('chr', array_filter(explode($delimiter, $string))));
603: }
604: if ($string[0] === $this->_escape && isset($string[1]) && is_numeric($string[1])) {
605: $delimiter = $this->_escape;
606: return implode('', array_map('chr', array_filter(explode($delimiter, $string))));
607: }
608: if (substr($string, 0, 3) === 'U00') {
609: $delimiter = 'U00';
610: return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string)))));
611: }
612: if (preg_match('/U([0-9a-fA-F]{4})/', $string, $match)) {
613: return Multibyte::ascii(array(hexdec($match[1])));
614: }
615: return $string;
616: }
617:
618: 619: 620: 621: 622: 623: 624:
625: protected function _translateTime($format, $domain) {
626: if (!empty($this->_domains[$domain][$this->_lang]['LC_TIME'][$format])) {
627: if (($trans = $this->_domains[$domain][$this->_lang][$this->category][$format])) {
628: return $trans;
629: }
630: }
631: return $format;
632: }
633:
634: }
635: