PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Cake/I18n/I18n.php

https://bitbucket.org/udeshika/fake_twitter
PHP | 590 lines | 411 code | 52 blank | 127 comment | 159 complexity | d07571e78a7f4f5ed596677536ed30f6 MD5 | raw file
  1. <?php
  2. /**
  3. * Internationalization
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2011, 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-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.I18n
  16. * @since CakePHP(tm) v 1.2.0.4116
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. /**
  20. * Included libraries.
  21. */
  22. App::uses('CakePlugin', 'Core');
  23. App::uses('L10n', 'I18n');
  24. App::uses('Multibyte', 'I18n');
  25. if (function_exists('mb_internal_encoding')) {
  26. $encoding = Configure::read('App.encoding');
  27. if (!empty($encoding)) {
  28. mb_internal_encoding($encoding);
  29. }
  30. }
  31. /**
  32. * I18n handles translation of Text and time format strings.
  33. *
  34. * @package Cake.I18n
  35. */
  36. class I18n {
  37. /**
  38. * Instance of the L10n class for localization
  39. *
  40. * @var L10n
  41. */
  42. public $l10n = null;
  43. /**
  44. * Current domain of translation
  45. *
  46. * @var string
  47. */
  48. public $domain = null;
  49. /**
  50. * Current category of translation
  51. *
  52. * @var string
  53. */
  54. public $category = 'LC_MESSAGES';
  55. /**
  56. * Current language used for translations
  57. *
  58. * @var string
  59. */
  60. protected $_lang = null;
  61. /**
  62. * Translation strings for a specific domain read from the .mo or .po files
  63. *
  64. * @var array
  65. */
  66. protected $_domains = array();
  67. /**
  68. * Set to true when I18N::_bindTextDomain() is called for the first time.
  69. * If a translation file is found it is set to false again
  70. *
  71. * @var boolean
  72. */
  73. protected $_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. */
  80. protected $_categories = array(
  81. 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
  82. );
  83. /**
  84. * Constructor, use I18n::getInstance() to get the i18n translation object.
  85. *
  86. * @return void
  87. */
  88. public function __construct() {
  89. $this->l10n = new L10n();
  90. }
  91. /**
  92. * Return a static instance of the I18n class
  93. *
  94. * @return I18n
  95. */
  96. public static function &getInstance() {
  97. static $instance = array();
  98. if (!$instance) {
  99. $instance[0] = new I18n();
  100. }
  101. return $instance[0];
  102. }
  103. /**
  104. * Used by the translation functions in basics.php
  105. * Returns a translated string based on current language and translation files stored in locale folder
  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. */
  114. public static function translate($singular, $plural = null, $domain = null, $category = 6, $count = null) {
  115. $_this = I18n::getInstance();
  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. if (is_numeric($category)) {
  123. $_this->category = $_this->_categories[$category];
  124. }
  125. $language = Configure::read('Config.language');
  126. if (!empty($_SESSION['Config']['language'])) {
  127. $language = $_SESSION['Config']['language'];
  128. }
  129. if (($_this->_lang && $_this->_lang !== $language) || !$_this->_lang) {
  130. $lang = $_this->l10n->get($language);
  131. $_this->_lang = $lang;
  132. }
  133. if (is_null($domain)) {
  134. $domain = 'default';
  135. }
  136. $_this->domain = $domain . '_' . $_this->l10n->lang;
  137. if (!isset($_this->_domains[$domain][$_this->_lang])) {
  138. $_this->_domains[$domain][$_this->_lang] = Cache::read($_this->domain, '_cake_core_');
  139. }
  140. if (!isset($_this->_domains[$domain][$_this->_lang][$_this->category])) {
  141. $_this->_bindTextDomain($domain);
  142. Cache::write($_this->domain, $_this->_domains[$domain][$_this->_lang], '_cake_core_');
  143. }
  144. if ($_this->category == 'LC_TIME') {
  145. return $_this->_translateTime($singular, $domain);
  146. }
  147. if (!isset($count)) {
  148. $plurals = 0;
  149. } elseif (!empty($_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"]) && $_this->_noLocale === false) {
  150. $header = $_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"];
  151. $plurals = $_this->_pluralGuess($header, $count);
  152. } else {
  153. if ($count != 1) {
  154. $plurals = 1;
  155. } else {
  156. $plurals = 0;
  157. }
  158. }
  159. if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category][$singular])) {
  160. if (($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$singular]) || ($plurals) && ($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$plural])) {
  161. if (is_array($trans)) {
  162. if (isset($trans[$plurals])) {
  163. $trans = $trans[$plurals];
  164. } else {
  165. trigger_error(
  166. __d('cake_dev',
  167. 'Missing plural form translation for "%s" in "%s" domain, "%s" locale. ' .
  168. ' Check your po file for correct plurals and valid Plural-Forms header.',
  169. $singular,
  170. $domain,
  171. $_this->_lang
  172. ),
  173. E_USER_WARNING
  174. );
  175. $trans = $trans[0];
  176. }
  177. }
  178. if (strlen($trans)) {
  179. return $trans;
  180. }
  181. }
  182. }
  183. if (!empty($plurals)) {
  184. return $plural;
  185. }
  186. return $singular;
  187. }
  188. /**
  189. * Clears the domains internal data array. Useful for testing i18n.
  190. *
  191. * @return void
  192. */
  193. public static function clear() {
  194. $self = I18n::getInstance();
  195. $self->_domains = array();
  196. }
  197. /**
  198. * Get the loaded domains cache.
  199. *
  200. * @return array
  201. */
  202. public static function domains() {
  203. $self = I18n::getInstance();
  204. return $self->_domains;
  205. }
  206. /**
  207. * Attempts to find the plural form of a string.
  208. *
  209. * @param string $header Type
  210. * @param integer $n Number
  211. * @return integer plural match
  212. */
  213. protected function _pluralGuess($header, $n) {
  214. if (!is_string($header) || $header === "nplurals=1;plural=0;" || !isset($header[0])) {
  215. return 0;
  216. }
  217. if ($header === "nplurals=2;plural=n!=1;") {
  218. return $n != 1 ? 1 : 0;
  219. } elseif ($header === "nplurals=2;plural=n>1;") {
  220. return $n > 1 ? 1 : 0;
  221. }
  222. if (strpos($header, "plurals=3")) {
  223. if (strpos($header, "100!=11")) {
  224. if (strpos($header, "10<=4")) {
  225. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  226. } elseif (strpos($header, "100<10")) {
  227. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  228. }
  229. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n != 0 ? 1 : 2);
  230. } elseif (strpos($header, "n==2")) {
  231. return $n == 1 ? 0 : ($n == 2 ? 1 : 2);
  232. } elseif (strpos($header, "n==0")) {
  233. return $n == 1 ? 0 : ($n == 0 || ($n % 100 > 0 && $n % 100 < 20) ? 1 : 2);
  234. } elseif (strpos($header, "n>=2")) {
  235. return $n == 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2);
  236. } elseif (strpos($header, "10>=2")) {
  237. return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  238. }
  239. return $n % 10 == 1 ? 0 : ($n % 10 == 2 ? 1 : 2);
  240. } elseif (strpos($header, "plurals=4")) {
  241. if (strpos($header, "100==2")) {
  242. return $n % 100 == 1 ? 0 : ($n % 100 == 2 ? 1 : ($n % 100 == 3 || $n % 100 == 4 ? 2 : 3));
  243. } elseif (strpos($header, "n>=3")) {
  244. return $n == 1 ? 0 : ($n == 2 ? 1 : ($n == 0 || ($n >= 3 && $n <= 10) ? 2 : 3));
  245. } elseif (strpos($header, "100>=1")) {
  246. return $n == 1 ? 0 : ($n == 0 || ($n % 100 >= 1 && $n % 100 <= 10) ? 1 : ($n % 100 >= 11 && $n % 100 <= 20 ? 2 : 3));
  247. }
  248. } elseif (strpos($header, "plurals=5")) {
  249. return $n == 1 ? 0 : ($n == 2 ? 1 : ($n >= 3 && $n <= 6 ? 2 : ($n >= 7 && $n <= 10 ? 3 : 4)));
  250. }
  251. }
  252. /**
  253. * Binds the given domain to a file in the specified directory.
  254. *
  255. * @param string $domain Domain to bind
  256. * @return string Domain binded
  257. */
  258. protected function _bindTextDomain($domain) {
  259. $this->_noLocale = true;
  260. $core = true;
  261. $merge = array();
  262. $searchPaths = App::path('locales');
  263. $plugins = CakePlugin::loaded();
  264. if (!empty($plugins)) {
  265. foreach ($plugins as $plugin) {
  266. $pluginDomain = Inflector::underscore($plugin);
  267. if ($pluginDomain === $domain) {
  268. $searchPaths[] = CakePlugin::path($plugin) . 'Locale' . DS;
  269. $searchPaths = array_reverse($searchPaths);
  270. break;
  271. }
  272. }
  273. }
  274. foreach ($searchPaths as $directory) {
  275. foreach ($this->l10n->languagePath as $lang) {
  276. $file = $directory . $lang . DS . $this->category . DS . $domain;
  277. $localeDef = $directory . $lang . DS . $this->category;
  278. if ($core) {
  279. $app = $directory . $lang . DS . $this->category . DS . 'core';
  280. if (file_exists($fn = "$app.mo")) {
  281. $this->_loadMo($fn, $domain);
  282. $this->_noLocale = false;
  283. $merge[$domain][$this->_lang][$this->category] = $this->_domains[$domain][$this->_lang][$this->category];
  284. $core = null;
  285. } elseif (file_exists($fn = "$app.po") && ($f = fopen($fn, "r"))) {
  286. $this->_loadPo($f, $domain);
  287. $this->_noLocale = false;
  288. $merge[$domain][$this->_lang][$this->category] = $this->_domains[$domain][$this->_lang][$this->category];
  289. $core = null;
  290. }
  291. }
  292. if (file_exists($fn = "$file.mo")) {
  293. $this->_loadMo($fn, $domain);
  294. $this->_noLocale = false;
  295. break 2;
  296. } elseif (file_exists($fn = "$file.po") && ($f = fopen($fn, "r"))) {
  297. $this->_loadPo($f, $domain);
  298. $this->_noLocale = false;
  299. break 2;
  300. } elseif (is_file($localeDef) && ($f = fopen($localeDef, "r"))) {
  301. $this->_loadLocaleDefinition($f, $domain);
  302. $this->_noLocale = false;
  303. return $domain;
  304. }
  305. }
  306. }
  307. if (empty($this->_domains[$domain][$this->_lang][$this->category])) {
  308. $this->_domains[$domain][$this->_lang][$this->category] = array();
  309. return $domain;
  310. }
  311. if (isset($this->_domains[$domain][$this->_lang][$this->category][""])) {
  312. $head = $this->_domains[$domain][$this->_lang][$this->category][""];
  313. foreach (explode("\n", $head) as $line) {
  314. $header = strtok($line,":");
  315. $line = trim(strtok("\n"));
  316. $this->_domains[$domain][$this->_lang][$this->category]["%po-header"][strtolower($header)] = $line;
  317. }
  318. if (isset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"])) {
  319. $switch = preg_replace("/(?:[() {}\\[\\]^\\s*\\]]+)/", "", $this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"]);
  320. $this->_domains[$domain][$this->_lang][$this->category]["%plural-c"] = $switch;
  321. unset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]);
  322. }
  323. $this->_domains = Set::pushDiff($this->_domains, $merge);
  324. if (isset($this->_domains[$domain][$this->_lang][$this->category][null])) {
  325. unset($this->_domains[$domain][$this->_lang][$this->category][null]);
  326. }
  327. }
  328. return $domain;
  329. }
  330. /**
  331. * Loads the binary .mo file for translation and sets the values for this translation in the var I18n::_domains
  332. *
  333. * @param string $file Binary .mo file to load
  334. * @param string $domain Domain where to load file in
  335. * @return void
  336. */
  337. protected function _loadMo($file, $domain) {
  338. $data = file_get_contents($file);
  339. if ($data) {
  340. $header = substr($data, 0, 20);
  341. $header = unpack("L1magic/L1version/L1count/L1o_msg/L1o_trn", $header);
  342. extract($header);
  343. if ((dechex($magic) == '950412de' || dechex($magic) == 'ffffffff950412de') && $version == 0) {
  344. for ($n = 0; $n < $count; $n++) {
  345. $r = unpack("L1len/L1offs", substr($data, $o_msg + $n * 8, 8));
  346. $msgid = substr($data, $r["offs"], $r["len"]);
  347. unset($msgid_plural);
  348. if (strpos($msgid, "\000")) {
  349. list($msgid, $msgid_plural) = explode("\000", $msgid);
  350. }
  351. $r = unpack("L1len/L1offs", substr($data, $o_trn + $n * 8, 8));
  352. $msgstr = substr($data, $r["offs"], $r["len"]);
  353. if (strpos($msgstr, "\000")) {
  354. $msgstr = explode("\000", $msgstr);
  355. }
  356. $this->_domains[$domain][$this->_lang][$this->category][$msgid] = $msgstr;
  357. if (isset($msgid_plural)) {
  358. $this->_domains[$domain][$this->_lang][$this->category][$msgid_plural] =& $this->_domains[$domain][$this->_lang][$this->category][$msgid];
  359. }
  360. }
  361. }
  362. }
  363. }
  364. /**
  365. * Loads the text .po file for translation and sets the values for this translation in the var I18n::_domains
  366. *
  367. * @param resource $file Text .po file to load
  368. * @param string $domain Domain to load file in
  369. * @return array Binded domain elements
  370. */
  371. protected function _loadPo($file, $domain) {
  372. $type = 0;
  373. $translations = array();
  374. $translationKey = "";
  375. $plural = 0;
  376. $header = "";
  377. do {
  378. $line = trim(fgets($file));
  379. if ($line == "" || $line[0] == "#") {
  380. continue;
  381. }
  382. if (preg_match("/msgid[[:space:]]+\"(.+)\"$/i", $line, $regs)) {
  383. $type = 1;
  384. $translationKey = stripcslashes($regs[1]);
  385. } elseif (preg_match("/msgid[[:space:]]+\"\"$/i", $line, $regs)) {
  386. $type = 2;
  387. $translationKey = "";
  388. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && ($type == 1 || $type == 2 || $type == 3)) {
  389. $type = 3;
  390. $translationKey .= stripcslashes($regs[1]);
  391. } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
  392. $translations[$translationKey] = stripcslashes($regs[1]);
  393. $type = 4;
  394. } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
  395. $type = 4;
  396. $translations[$translationKey] = "";
  397. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 4 && $translationKey) {
  398. $translations[$translationKey] .= stripcslashes($regs[1]);
  399. } elseif (preg_match("/msgid_plural[[:space:]]+\".*\"$/i", $line, $regs)) {
  400. $type = 6;
  401. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 6 && $translationKey) {
  402. $type = 6;
  403. } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
  404. $plural = $regs[1];
  405. $translations[$translationKey][$plural] = stripcslashes($regs[2]);
  406. $type = 7;
  407. } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
  408. $plural = $regs[1];
  409. $translations[$translationKey][$plural] = "";
  410. $type = 7;
  411. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 7 && $translationKey) {
  412. $translations[$translationKey][$plural] .= stripcslashes($regs[1]);
  413. } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && $type == 2 && !$translationKey) {
  414. $header .= stripcslashes($regs[1]);
  415. $type = 5;
  416. } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && !$translationKey) {
  417. $header = "";
  418. $type = 5;
  419. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 5) {
  420. $header .= stripcslashes($regs[1]);
  421. } else {
  422. unset($translations[$translationKey]);
  423. $type = 0;
  424. $translationKey = "";
  425. $plural = 0;
  426. }
  427. } while (!feof($file));
  428. fclose($file);
  429. $merge[""] = $header;
  430. return $this->_domains[$domain][$this->_lang][$this->category] = array_merge($merge, $translations);
  431. }
  432. /**
  433. * Parses a locale definition file following the POSIX standard
  434. *
  435. * @param resource $file file handler
  436. * @param string $domain Domain where locale definitions will be stored
  437. * @return void
  438. */
  439. protected function _loadLocaleDefinition($file, $domain = null) {
  440. $comment = '#';
  441. $escape = '\\';
  442. $currentToken = false;
  443. $value = '';
  444. while ($line = fgets($file)) {
  445. $line = trim($line);
  446. if (empty($line) || $line[0] === $comment) {
  447. continue;
  448. }
  449. $parts = preg_split("/[[:space:]]+/", $line);
  450. if ($parts[0] === 'comment_char') {
  451. $comment = $parts[1];
  452. continue;
  453. }
  454. if ($parts[0] === 'escape_char') {
  455. $escape = $parts[1];
  456. continue;
  457. }
  458. $count = count($parts);
  459. if ($count == 2) {
  460. $currentToken = $parts[0];
  461. $value = $parts[1];
  462. } elseif ($count == 1) {
  463. $value .= $parts[0];
  464. } else {
  465. continue;
  466. }
  467. $len = strlen($value) - 1;
  468. if ($value[$len] === $escape) {
  469. $value = substr($value, 0, $len);
  470. continue;
  471. }
  472. $mustEscape = array($escape . ',', $escape . ';', $escape . '<', $escape . '>', $escape . $escape);
  473. $replacements = array_map('crc32', $mustEscape);
  474. $value = str_replace($mustEscape, $replacements, $value);
  475. $value = explode(';', $value);
  476. $this->__escape = $escape;
  477. foreach ($value as $i => $val) {
  478. $val = trim($val, '"');
  479. $val = preg_replace_callback('/(?:<)?(.[^>]*)(?:>)?/', array(&$this, '_parseLiteralValue'), $val);
  480. $val = str_replace($replacements, $mustEscape, $val);
  481. $value[$i] = $val;
  482. }
  483. if (count($value) == 1) {
  484. $this->_domains[$domain][$this->_lang][$this->category][$currentToken] = array_pop($value);
  485. } else {
  486. $this->_domains[$domain][$this->_lang][$this->category][$currentToken] = $value;
  487. }
  488. }
  489. }
  490. /**
  491. * Auxiliary function to parse a symbol from a locale definition file
  492. *
  493. * @param string $string Symbol to be parsed
  494. * @return string parsed symbol
  495. */
  496. protected function _parseLiteralValue($string) {
  497. $string = $string[1];
  498. if (substr($string, 0, 2) === $this->__escape . 'x') {
  499. $delimiter = $this->__escape . 'x';
  500. return join('', array_map('chr', array_map('hexdec',array_filter(explode($delimiter, $string)))));
  501. }
  502. if (substr($string, 0, 2) === $this->__escape . 'd') {
  503. $delimiter = $this->__escape . 'd';
  504. return join('', array_map('chr', array_filter(explode($delimiter, $string))));
  505. }
  506. if ($string[0] === $this->__escape && isset($string[1]) && is_numeric($string[1])) {
  507. $delimiter = $this->__escape;
  508. return join('', array_map('chr', array_filter(explode($delimiter, $string))));
  509. }
  510. if (substr($string, 0, 3) === 'U00') {
  511. $delimiter = 'U00';
  512. return join('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string)))));
  513. }
  514. if (preg_match('/U([0-9a-fA-F]{4})/', $string, $match)) {
  515. return Multibyte::ascii(array(hexdec($match[1])));
  516. }
  517. return $string;
  518. }
  519. /**
  520. * Returns a Time format definition from corresponding domain
  521. *
  522. * @param string $format Format to be translated
  523. * @param string $domain Domain where format is stored
  524. * @return mixed translated format string if only value or array of translated strings for corresponding format.
  525. */
  526. protected function _translateTime($format, $domain) {
  527. if (!empty($this->_domains[$domain][$this->_lang]['LC_TIME'][$format])) {
  528. if (($trans = $this->_domains[$domain][$this->_lang][$this->category][$format])) {
  529. return $trans;
  530. }
  531. }
  532. return $format;
  533. }
  534. }