PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/cake/libs/i18n.php

https://github.com/cgajardo/repositorium
PHP | 568 lines | 387 code | 49 blank | 132 comment | 156 complexity | 62c1ec960d8f65540ba21a791cda3f8d MD5 | raw file
  1. <?php
  2. /**
  3. * Internationalization
  4. *
  5. * PHP versions 4 and 5
  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. * Included libraries.
  22. */
  23. App::import('Core', array('l10n', 'Multibyte'));
  24. /**
  25. * I18n handles translation of Text and time format strings.
  26. *
  27. * @package cake
  28. * @subpackage cake.cake.libs
  29. */
  30. class I18n extends Object {
  31. /**
  32. * Instance of the I10n class for localization
  33. *
  34. * @var I10n
  35. * @access public
  36. */
  37. var $l10n = null;
  38. /**
  39. * Current domain of translation
  40. *
  41. * @var string
  42. * @access public
  43. */
  44. var $domain = null;
  45. /**
  46. * Current category of translation
  47. *
  48. * @var string
  49. * @access public
  50. */
  51. var $category = 'LC_MESSAGES';
  52. /**
  53. * Current language used for translations
  54. *
  55. * @var string
  56. * @access private
  57. */
  58. var $__lang = null;
  59. /**
  60. * Translation strings for a specific domain read from the .mo or .po files
  61. *
  62. * @var array
  63. * @access private
  64. */
  65. var $__domains = array();
  66. /**
  67. * Set to true when I18N::__bindTextDomain() is called for the first time.
  68. * If a translation file is found it is set to false again
  69. *
  70. * @var boolean
  71. * @access private
  72. */
  73. var $__noLocale = false;
  74. /**
  75. * Set to true when I18N::__bindTextDomain() is called for the first time.
  76. * If a translation file is found it is set to false again
  77. *
  78. * @var array
  79. * @access private
  80. */
  81. var $__categories = array(
  82. 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
  83. );
  84. /**
  85. * Return a static instance of the I18n class
  86. *
  87. * @return object I18n
  88. * @access public
  89. */
  90. function &getInstance() {
  91. static $instance = array();
  92. if (!$instance) {
  93. $instance[0] =& new I18n();
  94. $instance[0]->l10n =& new L10n();
  95. }
  96. return $instance[0];
  97. }
  98. /**
  99. * Used by the translation functions in basics.php
  100. * Can also be used like I18n::translate(); but only if the App::import('I18n'); has been used to load the class.
  101. *
  102. * @param string $singular String to translate
  103. * @param string $plural Plural string (if any)
  104. * @param string $domain Domain The domain of the translation. Domains are often used by plugin translations
  105. * @param string $category Category The integer value of the category to use.
  106. * @param integer $count Count Count is used with $plural to choose the correct plural form.
  107. * @return string translated string.
  108. * @access public
  109. */
  110. function translate($singular, $plural = null, $domain = null, $category = 6, $count = null) {
  111. $_this =& I18n::getInstance();
  112. if (strpos($singular, "\r\n") !== false) {
  113. $singular = str_replace("\r\n", "\n", $singular);
  114. }
  115. if ($plural !== null && strpos($plural, "\r\n") !== false) {
  116. $plural = str_replace("\r\n", "\n", $plural);
  117. }
  118. if (is_numeric($category)) {
  119. $_this->category = $_this->__categories[$category];
  120. }
  121. $language = Configure::read('Config.language');
  122. if (!empty($_SESSION['Config']['language'])) {
  123. $language = $_SESSION['Config']['language'];
  124. }
  125. if (($_this->__lang && $_this->__lang !== $language) || !$_this->__lang) {
  126. $lang = $_this->l10n->get($language);
  127. $_this->__lang = $lang;
  128. }
  129. if (is_null($domain)) {
  130. $domain = 'default';
  131. }
  132. $_this->domain = $domain . '_' . $_this->l10n->lang;
  133. if (!isset($_this->__domains[$domain][$_this->__lang])) {
  134. $_this->__domains[$domain][$_this->__lang] = Cache::read($_this->domain, '_cake_core_');
  135. }
  136. if (!isset($_this->__domains[$domain][$_this->__lang][$_this->category])) {
  137. $_this->__bindTextDomain($domain);
  138. Cache::write($_this->domain, $_this->__domains[$domain][$_this->__lang], '_cake_core_');
  139. }
  140. if ($_this->category == 'LC_TIME') {
  141. return $_this->__translateTime($singular,$domain);
  142. }
  143. if (!isset($count)) {
  144. $plurals = 0;
  145. } elseif (!empty($_this->__domains[$domain][$_this->__lang][$_this->category]["%plural-c"]) && $_this->__noLocale === false) {
  146. $header = $_this->__domains[$domain][$_this->__lang][$_this->category]["%plural-c"];
  147. $plurals = $_this->__pluralGuess($header, $count);
  148. } else {
  149. if ($count != 1) {
  150. $plurals = 1;
  151. } else {
  152. $plurals = 0;
  153. }
  154. }
  155. if (!empty($_this->__domains[$domain][$_this->__lang][$_this->category][$singular])) {
  156. if (($trans = $_this->__domains[$domain][$_this->__lang][$_this->category][$singular]) || ($plurals) && ($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. if (!empty($plurals)) {
  168. return $plural;
  169. }
  170. return $singular;
  171. }
  172. /**
  173. * Clears the domains internal data array. Useful for testing i18n.
  174. *
  175. * @return void
  176. */
  177. function clear() {
  178. $self =& I18n::getInstance();
  179. $self->__domains = array();
  180. }
  181. /**
  182. * Attempts to find the plural form of a string.
  183. *
  184. * @param string $header Type
  185. * @param integrer $n Number
  186. * @return integer plural match
  187. * @access private
  188. */
  189. function __pluralGuess($header, $n) {
  190. if (!is_string($header) || $header === "nplurals=1;plural=0;" || !isset($header[0])) {
  191. return 0;
  192. }
  193. if ($header === "nplurals=2;plural=n!=1;") {
  194. return $n != 1 ? 1 : 0;
  195. } elseif ($header === "nplurals=2;plural=n>1;") {
  196. return $n > 1 ? 1 : 0;
  197. }
  198. if (strpos($header, "plurals=3")) {
  199. if (strpos($header, "100!=11")) {
  200. if (strpos($header, "10<=4")) {
  201. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  202. } elseif (strpos($header, "100<10")) {
  203. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  204. }
  205. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n != 0 ? 1 : 2);
  206. } elseif (strpos($header, "n==2")) {
  207. return $n == 1 ? 0 : ($n == 2 ? 1 : 2);
  208. } elseif (strpos($header, "n==0")) {
  209. return $n == 1 ? 0 : ($n == 0 || ($n % 100 > 0 && $n % 100 < 20) ? 1 : 2);
  210. } elseif (strpos($header, "n>=2")) {
  211. return $n == 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2);
  212. } elseif (strpos($header, "10>=2")) {
  213. return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  214. }
  215. return $n % 10 == 1 ? 0 : ($n % 10 == 2 ? 1 : 2);
  216. } elseif (strpos($header, "plurals=4")) {
  217. if (strpos($header, "100==2")) {
  218. return $n % 100 == 1 ? 0 : ($n % 100 == 2 ? 1 : ($n % 100 == 3 || $n % 100 == 4 ? 2 : 3));
  219. } elseif (strpos($header, "n>=3")) {
  220. return $n == 1 ? 0 : ($n == 2 ? 1 : ($n == 0 || ($n >= 3 && $n <= 10) ? 2 : 3));
  221. } elseif (strpos($header, "100>=1")) {
  222. return $n == 1 ? 0 : ($n == 0 || ($n % 100 >= 1 && $n % 100 <= 10) ? 1 : ($n % 100 >= 11 && $n % 100 <= 20 ? 2 : 3));
  223. }
  224. } elseif (strpos($header, "plurals=5")) {
  225. return $n == 1 ? 0 : ($n == 2 ? 1 : ($n >= 3 && $n <= 6 ? 2 : ($n >= 7 && $n <= 10 ? 3 : 4)));
  226. }
  227. }
  228. /**
  229. * Binds the given domain to a file in the specified directory.
  230. *
  231. * @param string $domain Domain to bind
  232. * @return string Domain binded
  233. * @access private
  234. */
  235. function __bindTextDomain($domain) {
  236. $this->__noLocale = true;
  237. $core = true;
  238. $merge = array();
  239. $searchPaths = App::path('locales');
  240. $plugins = App::objects('plugin');
  241. if (!empty($plugins)) {
  242. foreach ($plugins as $plugin) {
  243. $plugin = Inflector::underscore($plugin);
  244. if ($plugin === $domain) {
  245. $searchPaths[] = App::pluginPath($plugin) . DS . 'locale' . DS;
  246. $searchPaths = array_reverse($searchPaths);
  247. break;
  248. }
  249. }
  250. }
  251. foreach ($searchPaths as $directory) {
  252. foreach ($this->l10n->languagePath as $lang) {
  253. $file = $directory . $lang . DS . $this->category . DS . $domain;
  254. $localeDef = $directory . $lang . DS . $this->category;
  255. if ($core) {
  256. $app = $directory . $lang . DS . $this->category . DS . 'core';
  257. if (file_exists($fn = "$app.mo")) {
  258. $this->__loadMo($fn, $domain);
  259. $this->__noLocale = false;
  260. $merge[$domain][$this->__lang][$this->category] = $this->__domains[$domain][$this->__lang][$this->category];
  261. $core = null;
  262. } elseif (file_exists($fn = "$app.po") && ($f = fopen($fn, "r"))) {
  263. $this->__loadPo($f, $domain);
  264. $this->__noLocale = false;
  265. $merge[$domain][$this->__lang][$this->category] = $this->__domains[$domain][$this->__lang][$this->category];
  266. $core = null;
  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. if (empty($this->__domains[$domain][$this->__lang][$this->category])) {
  285. $this->__domains[$domain][$this->__lang][$this->category] = array();
  286. return $domain;
  287. }
  288. if (isset($this->__domains[$domain][$this->__lang][$this->category][""])) {
  289. $head = $this->__domains[$domain][$this->__lang][$this->category][""];
  290. foreach (explode("\n", $head) as $line) {
  291. $header = strtok($line,":");
  292. $line = trim(strtok("\n"));
  293. $this->__domains[$domain][$this->__lang][$this->category]["%po-header"][strtolower($header)] = $line;
  294. }
  295. if (isset($this->__domains[$domain][$this->__lang][$this->category]["%po-header"]["plural-forms"])) {
  296. $switch = preg_replace("/(?:[() {}\\[\\]^\\s*\\]]+)/", "", $this->__domains[$domain][$this->__lang][$this->category]["%po-header"]["plural-forms"]);
  297. $this->__domains[$domain][$this->__lang][$this->category]["%plural-c"] = $switch;
  298. unset($this->__domains[$domain][$this->__lang][$this->category]["%po-header"]);
  299. }
  300. $this->__domains = Set::pushDiff($this->__domains, $merge);
  301. if (isset($this->__domains[$domain][$this->__lang][$this->category][null])) {
  302. unset($this->__domains[$domain][$this->__lang][$this->category][null]);
  303. }
  304. }
  305. return $domain;
  306. }
  307. /**
  308. * Loads the binary .mo file for translation and sets the values for this translation in the var I18n::__domains
  309. *
  310. * @param resource $file Binary .mo file to load
  311. * @param string $domain Domain where to load file in
  312. * @access private
  313. */
  314. function __loadMo($file, $domain) {
  315. $data = file_get_contents($file);
  316. if ($data) {
  317. $header = substr($data, 0, 20);
  318. $header = unpack("L1magic/L1version/L1count/L1o_msg/L1o_trn", $header);
  319. extract($header);
  320. if ((dechex($magic) == '950412de' || dechex($magic) == 'ffffffff950412de') && $version == 0) {
  321. for ($n = 0; $n < $count; $n++) {
  322. $r = unpack("L1len/L1offs", substr($data, $o_msg + $n * 8, 8));
  323. $msgid = substr($data, $r["offs"], $r["len"]);
  324. unset($msgid_plural);
  325. if (strpos($msgid, "\000")) {
  326. list($msgid, $msgid_plural) = explode("\000", $msgid);
  327. }
  328. $r = unpack("L1len/L1offs", substr($data, $o_trn + $n * 8, 8));
  329. $msgstr = substr($data, $r["offs"], $r["len"]);
  330. if (strpos($msgstr, "\000")) {
  331. $msgstr = explode("\000", $msgstr);
  332. }
  333. $this->__domains[$domain][$this->__lang][$this->category][$msgid] = $msgstr;
  334. if (isset($msgid_plural)) {
  335. $this->__domains[$domain][$this->__lang][$this->category][$msgid_plural] =& $this->__domains[$domain][$this->__lang][$this->category][$msgid];
  336. }
  337. }
  338. }
  339. }
  340. }
  341. /**
  342. * Loads the text .po file for translation and sets the values for this translation in the var I18n::__domains
  343. *
  344. * @param resource $file Text .po file to load
  345. * @param string $domain Domain to load file in
  346. * @return array Binded domain elements
  347. * @access private
  348. */
  349. function __loadPo($file, $domain) {
  350. $type = 0;
  351. $translations = array();
  352. $translationKey = "";
  353. $plural = 0;
  354. $header = "";
  355. do {
  356. $line = trim(fgets($file));
  357. if ($line == "" || $line[0] == "#") {
  358. continue;
  359. }
  360. if (preg_match("/msgid[[:space:]]+\"(.+)\"$/i", $line, $regs)) {
  361. $type = 1;
  362. $translationKey = stripcslashes($regs[1]);
  363. } elseif (preg_match("/msgid[[:space:]]+\"\"$/i", $line, $regs)) {
  364. $type = 2;
  365. $translationKey = "";
  366. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && ($type == 1 || $type == 2 || $type == 3)) {
  367. $type = 3;
  368. $translationKey .= stripcslashes($regs[1]);
  369. } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
  370. $translations[$translationKey] = stripcslashes($regs[1]);
  371. $type = 4;
  372. } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
  373. $type = 4;
  374. $translations[$translationKey] = "";
  375. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 4 && $translationKey) {
  376. $translations[$translationKey] .= stripcslashes($regs[1]);
  377. } elseif (preg_match("/msgid_plural[[:space:]]+\".*\"$/i", $line, $regs)) {
  378. $type = 6;
  379. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 6 && $translationKey) {
  380. $type = 6;
  381. } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
  382. $plural = $regs[1];
  383. $translations[$translationKey][$plural] = stripcslashes($regs[2]);
  384. $type = 7;
  385. } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
  386. $plural = $regs[1];
  387. $translations[$translationKey][$plural] = "";
  388. $type = 7;
  389. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 7 && $translationKey) {
  390. $translations[$translationKey][$plural] .= stripcslashes($regs[1]);
  391. } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && $type == 2 && !$translationKey) {
  392. $header .= stripcslashes($regs[1]);
  393. $type = 5;
  394. } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && !$translationKey) {
  395. $header = "";
  396. $type = 5;
  397. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 5) {
  398. $header .= stripcslashes($regs[1]);
  399. } else {
  400. unset($translations[$translationKey]);
  401. $type = 0;
  402. $translationKey = "";
  403. $plural = 0;
  404. }
  405. } while (!feof($file));
  406. fclose($file);
  407. $merge[""] = $header;
  408. return $this->__domains[$domain][$this->__lang][$this->category] = array_merge($merge ,$translations);
  409. }
  410. /**
  411. * Parses a locale definition file following the POSIX standard
  412. *
  413. * @param resource $file file handler
  414. * @param string $domain Domain where locale definitions will be stored
  415. * @return void
  416. * @access private
  417. */
  418. function __loadLocaleDefinition($file, $domain = null) {
  419. $comment = '#';
  420. $escape = '\\';
  421. $currentToken = false;
  422. $value = '';
  423. while ($line = fgets($file)) {
  424. $line = trim($line);
  425. if (empty($line) || $line[0] === $comment) {
  426. continue;
  427. }
  428. $parts = preg_split("/[[:space:]]+/",$line);
  429. if ($parts[0] === 'comment_char') {
  430. $comment = $parts[1];
  431. continue;
  432. }
  433. if ($parts[0] === 'escape_char') {
  434. $escape = $parts[1];
  435. continue;
  436. }
  437. $count = count($parts);
  438. if ($count == 2) {
  439. $currentToken = $parts[0];
  440. $value = $parts[1];
  441. } elseif ($count == 1) {
  442. $value .= $parts[0];
  443. } else {
  444. continue;
  445. }
  446. $len = strlen($value) - 1;
  447. if ($value[$len] === $escape) {
  448. $value = substr($value, 0, $len);
  449. continue;
  450. }
  451. $mustEscape = array($escape . ',' , $escape . ';', $escape . '<', $escape . '>', $escape . $escape);
  452. $replacements = array_map('crc32', $mustEscape);
  453. $value = str_replace($mustEscape, $replacements, $value);
  454. $value = explode(';', $value);
  455. $this->__escape = $escape;
  456. foreach ($value as $i => $val) {
  457. $val = trim($val, '"');
  458. $val = preg_replace_callback('/(?:<)?(.[^>]*)(?:>)?/', array(&$this, '__parseLiteralValue'), $val);
  459. $val = str_replace($replacements, $mustEscape, $val);
  460. $value[$i] = $val;
  461. }
  462. if (count($value) == 1) {
  463. $this->__domains[$domain][$this->__lang][$this->category][$currentToken] = array_pop($value);
  464. } else {
  465. $this->__domains[$domain][$this->__lang][$this->category][$currentToken] = $value;
  466. }
  467. }
  468. }
  469. /**
  470. * Auxiliary function to parse a symbol from a locale definition file
  471. *
  472. * @param string $string Symbol to be parsed
  473. * @return string parsed symbol
  474. * @access private
  475. */
  476. function __parseLiteralValue($string) {
  477. $string = $string[1];
  478. if (substr($string, 0, 2) === $this->__escape . 'x') {
  479. $delimiter = $this->__escape . 'x';
  480. return join('', array_map('chr', array_map('hexdec',array_filter(explode($delimiter, $string)))));
  481. }
  482. if (substr($string, 0, 2) === $this->__escape . 'd') {
  483. $delimiter = $this->__escape . 'd';
  484. return join('', array_map('chr', array_filter(explode($delimiter, $string))));
  485. }
  486. if ($string[0] === $this->__escape && isset($string[1]) && is_numeric($string[1])) {
  487. $delimiter = $this->__escape;
  488. return join('', array_map('chr', array_filter(explode($delimiter, $string))));
  489. }
  490. if (substr($string, 0, 3) === 'U00') {
  491. $delimiter = 'U00';
  492. return join('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string)))));
  493. }
  494. if (preg_match('/U([0-9a-fA-F]{4})/', $string, $match)) {
  495. return Multibyte::ascii(array(hexdec($match[1])));
  496. }
  497. return $string;
  498. }
  499. /**
  500. * Returns a Time format definition from corresponding domain
  501. *
  502. * @param string $format Format to be translated
  503. * @param string $domain Domain where format is stored
  504. * @return mixed translated format string if only value or array of translated strings for corresponding format.
  505. * @access private
  506. */
  507. function __translateTime($format, $domain) {
  508. if (!empty($this->__domains[$domain][$this->__lang]['LC_TIME'][$format])) {
  509. if (($trans = $this->__domains[$domain][$this->__lang][$this->category][$format])) {
  510. return $trans;
  511. }
  512. }
  513. return $format;
  514. }
  515. }