Open-Source PHP Framework - Designed for rapid development of performance-oriented scalable applications

/mvc/components/i18n

[return to app]
1 <?php
2
/**
3  * Internationalization
4  *
5  * This is CakePHP's i18n class updated to PHP5 syntax for Vork
6  *
7  * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
8  * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
9  *
10  * Licensed under The MIT License
11  * Redistributions of files must retain the above copyright notice.
12  *
13  * @copyright     Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
14  * @link          http://cakephp.org CakePHP(tm) Project
15  * @package       cake
16  * @subpackage    cake.cake.libs
17  * @since         CakePHP(tm) v 1.2.0.4116
18  * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
19  */
20
21 /**
22  * I18n handles translation of Text and time format strings.
23  *
24  * @package       cake
25  * @subpackage    cake.cake.libs
26  */
27
class i18nComponent {
28     
/**
29      * Instance of the I10n class for localization
30      *
31      * @var I10n
32      * @access public
33      */
34     
public $l10n null;
35     
36
37     
/**
38      * Current domain of translation
39      *
40      * @var string
41      * @access public
42      */
43     
public $domain null;
44
45     
/**
46      * Current category of translation
47      *
48      * @var string
49      * @access public
50      */
51     
public $category 'LC_MESSAGES';
52
53     
/**
54      * List of search directories to text domains
55      *
56      * @var array
57      * @access public
58      */
59     
public $searchPaths = array();
60
61     
/**
62      * Current language used for translations
63      *
64      * @var string
65      * @access private
66      */
67     
protected $_lang null;
68
69     
/**
70      * Translation strings for a specific domain read from the .mo or .po files
71      *
72      * @var array
73      * @access private
74      */
75     
protected $_domains = array();
76
77     
/**
78      * Set to true when I18N::_bindTextDomain() is called for the first time.
79      * If a translation file is found it is set to false again
80      *
81      * @var boolean
82      * @access private
83      */
84     
protected $_noLocale false;
85
86     
/**
87      * Set to true when I18N::_bindTextDomain() is called for the first time.
88      * If a translation file is found it is set to false again
89      *
90      * @var array
91      * @access private
92      */
93     
protected $_categories = array(
94          
'LC_ALL''LC_COLLATE''LC_CTYPE''LC_MONETARY''LC_NUMERIC''LC_TIME''LC_MESSAGES'
95     
);
96
97
98     public function 
__construct(){
99         
$this->l10n get::component('l10n');
100     }
101
102
103     
/**
104      * Used by the translation functions in basics.php
105      * Can also be used like I18n::translate(); but only if the App::import('I18n'); has been used to load the
 
class.
106      *
107      * @
param string $singular String to translate
108      
* @param string $plural Plural string (if any)
109      * @
param string $domain Domain The domain of the translation.  Domains are often used by plugin translations
110      
* @param string $category Category The integer value of the category to use.
111      * @
param integer $count Count Count is used with $plural to choose the correct plural form.
112      * @return 
string translated string.
113      * @
access public
114      */
115     public function 
translate($singular$plural null$domain null$category 6$count null) {
116         if (
strpos($singular"\r\n") !== false) {
117             
$singular str_replace("\r\n""\n"$singular);
118         }
119         if (
$plural !== null && strpos($plural"\r\n") !== false) {
120             
$plural str_replace("\r\n""\n"$plural);
121         }
122
123         if (
is_numeric($category)) {
124             
$this->category $this->_categories[$category];
125         }
126
127         if (!empty(
$_SESSION['Config']['language'])) {
128             
$language $_SESSION['Config']['language'];
129         }
130
131         if ((
$this->_lang && $this->_lang !== $language) || !$this->_lang) {
132             
$lang $this->l10n->get();
133             
$this->_lang $lang;
134         }
135
136         if (
is_null($domain)) {
137             
$domain 'default';
138         }
139
140         
$this->domain $domain '_' $this->l10n->lang;
141         
$this->_bindTextDomain($domain);
142
143         if (
$this->category == 'LC_TIME') {
144             return 
$this->_translateTime($singular$domain);
145         }
146
147         if (!isset(
$count)) {
148             
$plurals 0;
149         } elseif (!empty(
$this->_domains[$domain][$this->_lang][$this->category]["%plural-c"])
150                   && 
$this->_noLocale === false) {
151             
$header $this->_domains[$domain][$this->_lang][$this->category]["%plural-c"];
152             
$plurals $this->_pluralGuess($header$count);
153         } else {
154             if (
$count != 1) {
155                 
$plurals 1;
156             } else {
157                 
$plurals 0;
158             }
159         }
160
161         if (!empty(
$this->_domains[$domain][$this->_lang][$this->category][$singular])) {
162             if ((
$trans $this->_domains[$domain][$this->_lang][$this->category][$singular]) || ($plurals)
163                  && (
$trans $this->_domains[$domain][$this->_lang][$this->category][$plural])) {
164                 if (
is_array($trans)) {
165                     if (isset(
$trans[$plurals])) {
166                         
$trans $trans[$plurals];
167                     }
168                 }
169                 if (
strlen($trans)) {
170                     return 
$trans;
171                 }
172             }
173         }
174
175         if (!empty(
$plurals)) {
176             return 
$plural;
177         }
178         return 
$singular;
179     }
180
181     
/**
182      * Clears the domains internal data array.  Useful for testing i18n.
183      *
184      * @return void
185      */
186     
public function clear() {
187         
$this->_domains = array();
188     }
189
190     
/**
191      * Attempts to find the plural form of a string.
192      *
193      * @param string $header Type
194      * @param integrer $n Number
195      * @return integer plural match
196      * @access private
197      */
198     
protected function _pluralGuess($header$n) {
199         if (!
is_string($header) || $header === "nplurals=1;plural=0;" || !isset($header[0])) {
200             return 
0;
201         }
202
203         if (
$header === "nplurals=2;plural=n!=1;") {
204             return 
$n != 0;
205         } elseif (
$header === "nplurals=2;plural=n>1;") {
206             return 
$n 0;
207         }
208
209         if (
strpos($header"plurals=3")) {
210             if (
strpos($header"100!=11")) {
211                 if (
strpos($header"10<=4")) {
212                     return 
$n 10 == && $n 100 != 11 : ($n 10 >= && $n 10 <= 4
213                                                                  
&& ($n 100 10 || $n 100 >= 20) ? 2);
214                 } elseif (
strpos($header"100<10")) {
215                     return 
$n 10 == && $n 100 != 11 : ($n 10 >= 2
216                                                                  
&& ($n 100 10 || $n 100 >= 20) ? 2);
217                 }
218                 return 
$n 10 == && $n 100 != 11 : ($n != 2);
219             } elseif (
strpos($header"n==2")) {
220                 return 
$n == : ($n == 2);
221             } elseif (
strpos($header"n==0")) {
222                 return 
$n == : ($n == || ($n 100 && $n 100 20) ? 2);
223             } elseif (
strpos($header"n>=2")) {
224                 return 
$n == : ($n >= && $n <= 2);
225             } elseif (
strpos($header"10>=2")) {
226                 return 
$n == : ($n 10 >= && $n 10 <= && ($n 100 10 || $n 100 >= 20) ? 2);
227             }
228             return 
$n 10 == : ($n 10 == 2);
229         } elseif (
strpos($header"plurals=4")) {
230             if (
strpos($header"100==2")) {
231                 return 
$n 100 == : ($n 100 == : ($n 100 == || $n 100 == 3));
232             } elseif (
strpos($header"n>=3")) {
233                 return 
$n == : ($n == : ($n == || ($n >= && $n <= 10) ? 3));
234             } elseif (
strpos($header"100>=1")) {
235                 return 
$n == : ($n == || ($n 100 >= && $n 100 <= 10) ? : ($n 100 >= 11
236                                                                                           
&& $n 100 <= 20 :
 
3));
237             }
238         } elseif (
strpos($header"plurals=5")) {
239             return 
$n == : ($n == : ($n >= && $n <= : ($n >= && $n <= 10 4)));
240         }
241     }
242
243     
/**
244      * Binds the given domain to a file in the specified directory.
245      *
246      * @param string $domain Domain to bind
247      * @return string Domain binded
248      * @access private
249      */
250     
protected function _bindTextDomain($domain) {
251         
$this->_noLocale true;
252         
$core true;
253         
$merge = array();
254
255         foreach (
$this->searchPaths as $directory) {
256             foreach (
$this->l10n->languagePath as $lang) {
257                 
$file $directory $lang config::DS $this->category config::DS $domain;
258                 
$localeDef $directory $lang config::DS $this->category;
259
260                 if (
$core) {
261                     
$app $directory $lang config::DS $this->category config::DS 'core';
262
263                     if (
file_exists($fn "$app.mo")) {
264                         
$this->_loadMo($fn$domain);
265                         
$this->_noLocale false;
266                         
$merge[$domain][$this->_lang][$this->category] =
 
$this->_domains[$domain][$this->_lang][$this->category];
267                         
$core null;
268                     } elseif (
file_exists($fn "$app.po") && ($f fopen($fn"r"))) {
269                         
$this->_loadPo($f$domain);
270                         
$this->_noLocale false;
271                         
$merge[$domain][$this->_lang][$this->category] =
 
$this->_domains[$domain][$this->_lang][$this->category];
272                         
$core null;
273                     }
274                 }
275
276                 if (
file_exists($fn "$file.mo")) {
277                     
$this->_loadMo($fn$domain);
278                     
$this->_noLocale false;
279                     break 
2;
280                 } elseif (
file_exists($fn "$file.po") && ($f fopen($fn"r"))) {
281                     
$this->_loadPo($f$domain);
282                     
$this->_noLocale false;
283                     break 
2;
284                 } elseif (
is_file($localeDef) && ($f fopen($localeDef"r"))) {
285                     
$this->_loadLocaleDefinition($f$domain);
286                     
$this->_noLocale false;
287                     return 
$domain;
288                 }
289             }
290         }
291
292         if (empty(
$this->_domains[$domain][$this->_lang][$this->category])) {
293             
$this->_domains[$domain][$this->_lang][$this->category] = array();
294             return 
$domain;
295         }
296
297         if (
$head $this->_domains[$domain][$this->_lang][$this->category][""]) {
298             foreach (
explode("\n"$head) as $line) {
299                 
$header strtok($line,":");
300                 
$line trim(strtok("\n"));
301                 
$this->_domains[$domain][$this->_lang][$this->category]["%po-header"][strtolower($header)] =
 
$line;
302             }
303
304             if (isset(
$this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"])) {
305                 
$switch $this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"];
306                 
$switch preg_replace("/(?:[() {}\\[\\]^\\s*\\]]+)/"""$switch);
307                 
$this->_domains[$domain][$this->_lang][$this->category]["%plural-c"] = $switch;
308                 unset(
$this->_domains[$domain][$this->_lang][$this->category]["%po-header"]);
309             }
310             
$this->_domains $this->_pushDiff($this->_domains$merge);
311
312             if (isset(
$this->_domains[$domain][$this->_lang][$this->category][null])) {
313                 unset(
$this->_domains[$domain][$this->_lang][$this->category][null]);
314             }
315         }
316         return 
$domain;
317     }
318
319     
/**
320      * Pushes the differences in $array2 onto the end of $array
321      *
322      * @param mixed $array Original array
323      * @param mixed $array2 Differences to push
324      * @return array Combined array
325      * @access private
326      */
327     
protected function _pushDiff($array$array2) {
328         if (empty(
$array) && !empty($array2)) {
329             return 
$array2;
330         }
331         if (!empty(
$array) && !empty($array2)) {
332             foreach (
$array2 as $key => $value) {
333                 if (!
array_key_exists($key$array)) {
334                     
$array[$key] = $value;
335                 } else {
336                     if (
is_array($value)) {
337                         
$array[$key] = $this->_pushDiff($array[$key], $array2[$key]);
338                     }
339                 }
340             }
341         }
342         return 
$array;
343     }
344
345     
/**
346      * Loads the binary .mo file for translation and sets the values for this translation in the var
 
I18n::_domains
347      
*
348      * @
param resource $file Binary .mo file to load
349      
* @param string $domain Domain where to load file in
350      
* @access private
351      */
352     protected function 
_loadMo($file$domain) {
353         
$data file_get_contents($file);
354
355         if (
$data) {
356             
$header substr($data020);
357             
$header unpack("L1magic/L1version/L1count/L1o_msg/L1o_trn"$header);
358             
extract($header);
359
360             if ((
dechex($magic) == '950412de' || dechex($magic) == 'ffffffff950412de') && $version == 0) {
361                 for (
$n 0$n $count$n++) {
362                     
$r unpack("L1len/L1offs"substr($data$o_msg $n 88));
363                     
$msgid substr($data$r["offs"], $r["len"]);
364                     unset(
$msgid_plural);
365
366                     if (
strpos($msgid"\000")) {
367                         list(
$msgid$msgid_plural) = explode("\000"$msgid);
368                     }
369                     
$r unpack("L1len/L1offs"substr($data$o_trn $n 88));
370                     
$msgstr substr($data$r["offs"], $r["len"]);
371
372                     if (
strpos($msgstr"\000")) {
373                         
$msgstr explode("\000"$msgstr);
374                     }
375                     
$this->_domains[$domain][$this->_lang][$this->category][$msgid] = $msgstr;
376
377                     if (isset(
$msgid_plural)) {
378                         
$this->_domains[$domain][$this->_lang][$this->category][$msgid_plural] =&
 
$this->_domains[$domain][$this->_lang][$this->category][$msgid];
379                     }
380                 }
381             }
382         }
383     }
384
385     
/**
386      * Loads the text .po file for translation and sets the values for this translation in the var I18n::_domains
387      *
388      * @param resource $file Text .po file to load
389      * @param string $domain Domain to load file in
390      * @return array Binded domain elements
391      * @access private
392      */
393     
protected function _loadPo($file$domain) {
394         
$type 0;
395         
$translations = array();
396         
$translationKey "";
397         
$plural 0;
398         
$header "";
399
400         do {
401             
$line trim(fgets($file));
402             if (
$line == "" || $line[0] == "#") {
403                 continue;
404             }
405             if (
preg_match("/msgid[[:space:]]+\"(.+)\"$/i"$line$regs)) {
406                 
$type 1;
407                 
$translationKey stripcslashes($regs[1]);
408             } elseif (
preg_match("/msgid[[:space:]]+\"\"$/i"$line$regs)) {
409                 
$type 2;
410                 
$translationKey "";
411             } elseif (
preg_match("/^\"(.*)\"$/i"$line$regs) && ($type == || $type == || $type == 3)) {
412                 
$type 3;
413                 
$translationKey .= stripcslashes($regs[1]);
414             } elseif (
preg_match("/msgstr[[:space:]]+\"(.+)\"$/i"$line$regs)
415                       && (
$type == || $type == 3) && $translationKey) {
416                 
$translations[$translationKey] = stripcslashes($regs[1]);
417                 
$type 4;
418             } elseif (
preg_match("/msgstr[[:space:]]+\"\"$/i"$line$regs)
419                       && (
$type == || $type == 3) && $translationKey) {
420                 
$type 4;
421                 
$translations[$translationKey] = "";
422             } elseif (
preg_match("/^\"(.*)\"$/i"$line$regs) && $type == && $translationKey) {
423                 
$translations[$translationKey] .= stripcslashes($regs[1]);
424             } elseif (
preg_match("/msgid_plural[[:space:]]+\".*\"$/i"$line$regs)) {
425                 
$type 6;
426             } elseif (
preg_match("/^\"(.*)\"$/i"$line$regs) && $type == && $translationKey) {
427                 
$type 6;
428             } elseif (
preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i"$line$regs)
429                       && (
$type == || $type == 7) && $translationKey) {
430                 
$plural $regs[1];
431                 
$translations[$translationKey][$plural] = stripcslashes($regs[2]);
432                 
$type 7;
433             } elseif (
preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i"$line$regs)
434                       && (
$type == || $type == 7) && $translationKey) {
435                 
$plural $regs[1];
436                 
$translations[$translationKey][$plural] = "";
437                 
$type 7;
438             } elseif (
preg_match("/^\"(.*)\"$/i"$line$regs) && $type == && $translationKey) {
439                 
$translations[$translationKey][$plural] .= stripcslashes($regs[1]);
440             } elseif (
preg_match("/msgstr[[:space:]]+\"(.+)\"$/i"$line$regs) && $type == &&
 
!$translationKey) {
441                 
$header .= stripcslashes($regs[1]);
442                 
$type 5;
443             } elseif (
preg_match("/msgstr[[:space:]]+\"\"$/i"$line$regs) && !$translationKey) {
444                 
$header "";
445                 
$type 5;
446             } elseif (
preg_match("/^\"(.*)\"$/i"$line$regs) && $type == 5) {
447                 
$header .= stripcslashes($regs[1]);
448             } else {
449                 unset(
$translations[$translationKey]);
450                 
$type 0;
451                 
$translationKey "";
452                 
$plural 0;
453             }
454         } while (!
feof($file));
455         
fclose($file);
456         
$merge[""] = $header;
457         return 
$this->_domains[$domain][$this->_lang][$this->category] = array_merge($merge$translations);
458     }
459
460     
/**
461      * Parses a locale definition file following the POSIX standard
462      *
463      * @param resource $file file handler
464      * @param string $domain Domain where locale definitions will be stored
465      * @return void
466      * @access private
467      */
468     
protected function _loadLocaleDefinition($file$domain null) {
469         
$comment '#';
470         
$escape '\\';
471         
$currentToken false;
472         
$value '';
473         while (
$line fgets($file)) {
474             
$line trim($line);
475             if (empty(
$line) || $line[0] === $comment) {
476                 continue;
477             }
478             
$parts preg_split("/[[:space:]]+/",$line);
479             if (
$parts[0] === 'comment_char') {
480                 
$comment $parts[1];
481                 continue;
482             }
483             if (
$parts[0] === 'escape_char') {
484                 
$escape $parts[1];
485                 continue;
486             }
487             
$count count($parts);
488             if (
$count == 2) {
489                 
$currentToken $parts[0];
490                 
$value $parts[1];
491             } elseif (
$count == 1) {
492                 
$value .= $parts[0];
493             } else {
494                 continue;
495             }
496
497             
$len strlen($value) - 1;
498             if (
$value[$len] === $escape) {
499                 
$value substr($value0$len);
500                 continue;
501             }
502
503             
$mustEscape = array($escape ',' $escape ';'$escape '<'$escape '>'$escape $escape);
504             
$replacements array_map('crc32'$mustEscape);
505             
$value str_replace($mustEscape$replacements$value);
506             
$value explode(';'$value);
507             
$this->_escape $escape;
508             foreach (
$value as $i => $val) {
509                 
$val trim($val'"');
510                 
$val preg_replace_callback('/(?:<)?(.[^>]*)(?:>)?/', array(&$this'_parseLiteralValue'),
 
$val);
511                 
$val str_replace($replacements$mustEscape$val);
512                 
$value[$i] = $val;
513             }
514             if (
count($value) == 1) {
515                 
$this->_domains[$domain][$this->_lang][$this->category][$currentToken] = array_pop($value);
516             } else {
517                 
$this->_domains[$domain][$this->_lang][$this->category][$currentToken] = $value;
518             }
519         }
520     }
521
522     
/**
523      * Auxiliary function to parse a symbol from a locale definition file
524      *
525      * @param string $string Symbol to be parsed
526      * @return string parsed symbol
527      * @access private
528      */
529     
protected function _parseLiteralValue($string) {
530         
$string $string[1];
531         if (
substr($string02) === $this->_escape 'x') {
532             
$delimiter $this->_escape 'x';
533             return 
join(''array_map('chr'array_map('hexdec'array_filter(explode($delimiter$string)))));
534         }
535         if (
substr($string02) === $this->_escape 'd') {
536             
$delimiter $this->_escape 'd';
537             return 
join(''array_map('chr'array_filter(explode($delimiter$string))));
538         }
539         if (
$string[0] === $this->_escape && isset($string[1]) && is_numeric($string[1])) {
540             
$delimiter $this->_escape;
541             return 
join(''array_map('chr'array_filter(explode($delimiter$string))));
542         }
543         if (
substr($string03) === 'U00') {
544             
$delimiter 'U00';
545             return 
join(''array_map('chr'array_map('hexdec'array_filter(explode($delimiter$string)))));
546         }
547         if (
preg_match('/U([0-9a-fA-F]{4})/'$string$match)) {
548             return 
$this->_ascii(array(hexdec($match[1])));
549         }
550         return 
$string;
551     }
552
553     
/**
554      * Converts the decimal value of a multibyte character string
555      * to a string
556      *
557      * @param array $array
558      * @return string
559      * @access private
560      */
561     
protected function _ascii($array) {
562         
$ascii '';
563
564         foreach (
$array as $utf8) {
565             if (
$utf8 128) {
566                 
$ascii .= chr($utf8);
567             } elseif (
$utf8 2048) {
568                 
$ascii .= chr(192 + (($utf8 - ($utf8 64)) / 64));
569                 
$ascii .= chr(128 + ($utf8 64));
570             } else {
571                 
$ascii .= chr(224 + (($utf8 - ($utf8 4096)) / 4096));
572                 
$ascii .= chr(128 + ((($utf8 4096) - ($utf8 64)) / 64));
573                 
$ascii .= chr(128 + ($utf8 64));
574             }
575         }
576         return 
$ascii;
577     }
578
579     
/**
580      * Returns a Time format definition from corresponding domain
581      *
582      * @param string $format Format to be translated
583      * @param string $domain Domain where format is stored
584      * @return mixed translated format string if only value or array of translated strings for corresponding
 
format.
585      * @
access private
586      */
587     protected function 
_translateTime($format$domain) {
588         if (!empty(
$this->_domains[$domain][$this->_lang]['LC_TIME'][$format])) {
589             if ((
$trans $this->_domains[$domain][$this->_lang][$this->category][$format])) {
590                 return 
$trans;
591             }
592         }
593         return 
$format;
594     }
595 }