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