Base for a static organization website

I18n.php 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. <?php
  2. /**
  3. * Internationalization
  4. *
  5. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  6. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  7. *
  8. * Licensed under The MIT License
  9. * For full copyright and license information, please see the LICENSE.txt
  10. * Redistributions of files must retain the above copyright notice.
  11. *
  12. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  13. * @link http://cakephp.org CakePHP(tm) Project
  14. * @package Cake.I18n
  15. * @since CakePHP(tm) v 1.2.0.4116
  16. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  17. */
  18. App::uses('CakePlugin', 'Core');
  19. App::uses('L10n', 'I18n');
  20. App::uses('Multibyte', 'I18n');
  21. App::uses('CakeSession', 'Model/Datasource');
  22. /**
  23. * I18n handles translation of Text and time format strings.
  24. *
  25. * @package Cake.I18n
  26. */
  27. class I18n {
  28. /**
  29. * Instance of the L10n class for localization
  30. *
  31. * @var L10n
  32. */
  33. public $l10n = null;
  34. /**
  35. * Default domain of translation
  36. *
  37. * @var string
  38. */
  39. public static $defaultDomain = 'default';
  40. /**
  41. * Current domain of translation
  42. *
  43. * @var string
  44. */
  45. public $domain = null;
  46. /**
  47. * Current category of translation
  48. *
  49. * @var string
  50. */
  51. public $category = 'LC_MESSAGES';
  52. /**
  53. * Current language used for translations
  54. *
  55. * @var string
  56. */
  57. protected $_lang = null;
  58. /**
  59. * Translation strings for a specific domain read from the .mo or .po files
  60. *
  61. * @var array
  62. */
  63. protected $_domains = array();
  64. /**
  65. * Set to true when I18N::_bindTextDomain() is called for the first time.
  66. * If a translation file is found it is set to false again
  67. *
  68. * @var bool
  69. */
  70. protected $_noLocale = false;
  71. /**
  72. * Translation categories
  73. *
  74. * @var array
  75. */
  76. protected $_categories = array(
  77. 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
  78. );
  79. /**
  80. * Constants for the translation categories.
  81. *
  82. * The constants may be used in translation fetching
  83. * instead of hardcoded integers.
  84. * Example:
  85. * ```
  86. * I18n::translate('CakePHP is awesome.', null, null, I18n::LC_MESSAGES)
  87. * ```
  88. *
  89. * To keep the code more readable, I18n constants are preferred over
  90. * hardcoded integers.
  91. */
  92. /**
  93. * Constant for LC_ALL.
  94. *
  95. * @var int
  96. */
  97. const LC_ALL = 0;
  98. /**
  99. * Constant for LC_COLLATE.
  100. *
  101. * @var int
  102. */
  103. const LC_COLLATE = 1;
  104. /**
  105. * Constant for LC_CTYPE.
  106. *
  107. * @var int
  108. */
  109. const LC_CTYPE = 2;
  110. /**
  111. * Constant for LC_MONETARY.
  112. *
  113. * @var int
  114. */
  115. const LC_MONETARY = 3;
  116. /**
  117. * Constant for LC_NUMERIC.
  118. *
  119. * @var int
  120. */
  121. const LC_NUMERIC = 4;
  122. /**
  123. * Constant for LC_TIME.
  124. *
  125. * @var int
  126. */
  127. const LC_TIME = 5;
  128. /**
  129. * Constant for LC_MESSAGES.
  130. *
  131. * @var int
  132. */
  133. const LC_MESSAGES = 6;
  134. /**
  135. * Escape string
  136. *
  137. * @var string
  138. */
  139. protected $_escape = null;
  140. /**
  141. * Constructor, use I18n::getInstance() to get the i18n translation object.
  142. */
  143. public function __construct() {
  144. $this->l10n = new L10n();
  145. }
  146. /**
  147. * Return a static instance of the I18n class
  148. *
  149. * @return I18n
  150. */
  151. public static function getInstance() {
  152. static $instance = array();
  153. if (!$instance) {
  154. $instance[0] = new I18n();
  155. }
  156. return $instance[0];
  157. }
  158. /**
  159. * Used by the translation functions in basics.php
  160. * Returns a translated string based on current language and translation files stored in locale folder
  161. *
  162. * @param string $singular String to translate
  163. * @param string $plural Plural string (if any)
  164. * @param string $domain Domain The domain of the translation. Domains are often used by plugin translations.
  165. * If null, the default domain will be used.
  166. * @param string $category Category The integer value of the category to use.
  167. * @param int $count Count Count is used with $plural to choose the correct plural form.
  168. * @param string $language Language to translate string to.
  169. * If null it checks for language in session followed by Config.language configuration variable.
  170. * @param string $context Context The context of the translation, e.g a verb or a noun.
  171. * @return string translated string.
  172. * @throws CakeException When '' is provided as a domain.
  173. */
  174. public static function translate($singular, $plural = null, $domain = null, $category = self::LC_MESSAGES,
  175. $count = null, $language = null, $context = null
  176. ) {
  177. $_this = I18n::getInstance();
  178. if (strpos($singular, "\r\n") !== false) {
  179. $singular = str_replace("\r\n", "\n", $singular);
  180. }
  181. if ($plural !== null && strpos($plural, "\r\n") !== false) {
  182. $plural = str_replace("\r\n", "\n", $plural);
  183. }
  184. if (is_numeric($category)) {
  185. $_this->category = $_this->_categories[$category];
  186. }
  187. if (empty($language)) {
  188. if (CakeSession::started()) {
  189. $language = CakeSession::read('Config.language');
  190. }
  191. if (empty($language)) {
  192. $language = Configure::read('Config.language');
  193. }
  194. }
  195. if (($_this->_lang && $_this->_lang !== $language) || !$_this->_lang) {
  196. $lang = $_this->l10n->get($language);
  197. $_this->_lang = $lang;
  198. }
  199. if ($domain === null) {
  200. $domain = static::$defaultDomain;
  201. }
  202. if ($domain === '') {
  203. throw new CakeException(__d('cake_dev', 'You cannot use "" as a domain.'));
  204. }
  205. $_this->domain = $domain . '_' . $_this->l10n->lang;
  206. if (!isset($_this->_domains[$domain][$_this->_lang])) {
  207. $_this->_domains[$domain][$_this->_lang] = Cache::read($_this->domain, '_cake_core_');
  208. }
  209. if (!isset($_this->_domains[$domain][$_this->_lang][$_this->category])) {
  210. $_this->_bindTextDomain($domain);
  211. Cache::write($_this->domain, $_this->_domains[$domain][$_this->_lang], '_cake_core_');
  212. }
  213. if ($_this->category === 'LC_TIME') {
  214. return $_this->_translateTime($singular, $domain);
  215. }
  216. if (!isset($count)) {
  217. $plurals = 0;
  218. } elseif (!empty($_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"]) && $_this->_noLocale === false) {
  219. $header = $_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"];
  220. $plurals = $_this->_pluralGuess($header, $count);
  221. } else {
  222. if ($count != 1) {
  223. $plurals = 1;
  224. } else {
  225. $plurals = 0;
  226. }
  227. }
  228. if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context])) {
  229. if (($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context]) ||
  230. ($plurals) && ($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$plural][$context])
  231. ) {
  232. if (is_array($trans)) {
  233. if (isset($trans[$plurals])) {
  234. $trans = $trans[$plurals];
  235. } else {
  236. trigger_error(
  237. __d('cake_dev',
  238. 'Missing plural form translation for "%s" in "%s" domain, "%s" locale. ' .
  239. ' Check your po file for correct plurals and valid Plural-Forms header.',
  240. $singular,
  241. $domain,
  242. $_this->_lang
  243. ),
  244. E_USER_WARNING
  245. );
  246. $trans = $trans[0];
  247. }
  248. }
  249. if (strlen($trans)) {
  250. return $trans;
  251. }
  252. }
  253. }
  254. if (!empty($plurals)) {
  255. return $plural;
  256. }
  257. return $singular;
  258. }
  259. /**
  260. * Clears the domains internal data array. Useful for testing i18n.
  261. *
  262. * @return void
  263. */
  264. public static function clear() {
  265. $self = I18n::getInstance();
  266. $self->_domains = array();
  267. }
  268. /**
  269. * Get the loaded domains cache.
  270. *
  271. * @return array
  272. */
  273. public static function domains() {
  274. $self = I18n::getInstance();
  275. return $self->_domains;
  276. }
  277. /**
  278. * Attempts to find the plural form of a string.
  279. *
  280. * @param string $header Type
  281. * @param int $n Number
  282. * @return int plural match
  283. * @link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
  284. * @link https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#List_of_Plural_Rules
  285. */
  286. protected function _pluralGuess($header, $n) {
  287. if (!is_string($header) || $header === "nplurals=1;plural=0;" || !isset($header[0])) {
  288. return 0;
  289. }
  290. if ($header === "nplurals=2;plural=n!=1;") {
  291. return $n != 1 ? 1 : 0;
  292. } elseif ($header === "nplurals=2;plural=n>1;") {
  293. return $n > 1 ? 1 : 0;
  294. }
  295. if (strpos($header, "plurals=3")) {
  296. if (strpos($header, "100!=11")) {
  297. if (strpos($header, "10<=4")) {
  298. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  299. } elseif (strpos($header, "100<10")) {
  300. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  301. }
  302. return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n != 0 ? 1 : 2);
  303. } elseif (strpos($header, "n==2")) {
  304. return $n == 1 ? 0 : ($n == 2 ? 1 : 2);
  305. } elseif (strpos($header, "n==0")) {
  306. return $n == 1 ? 0 : ($n == 0 || ($n % 100 > 0 && $n % 100 < 20) ? 1 : 2);
  307. } elseif (strpos($header, "n>=2")) {
  308. return $n == 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2);
  309. } elseif (strpos($header, "10>=2")) {
  310. return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
  311. }
  312. return $n % 10 == 1 ? 0 : ($n % 10 == 2 ? 1 : 2);
  313. } elseif (strpos($header, "plurals=4")) {
  314. if (strpos($header, "100==2")) {
  315. return $n % 100 == 1 ? 0 : ($n % 100 == 2 ? 1 : ($n % 100 == 3 || $n % 100 == 4 ? 2 : 3));
  316. } elseif (strpos($header, "n>=3")) {
  317. return $n == 1 ? 0 : ($n == 2 ? 1 : ($n == 0 || ($n >= 3 && $n <= 10) ? 2 : 3));
  318. } elseif (strpos($header, "100>=1")) {
  319. return $n == 1 ? 0 : ($n == 0 || ($n % 100 >= 1 && $n % 100 <= 10) ? 1 : ($n % 100 >= 11 && $n % 100 <= 20 ? 2 : 3));
  320. }
  321. } elseif (strpos($header, "plurals=5")) {
  322. return $n == 1 ? 0 : ($n == 2 ? 1 : ($n >= 3 && $n <= 6 ? 2 : ($n >= 7 && $n <= 10 ? 3 : 4)));
  323. } elseif (strpos($header, "plurals=6")) {
  324. return $n == 0 ? 0 :
  325. ($n == 1 ? 1 :
  326. ($n == 2 ? 2 :
  327. ($n % 100 >= 3 && $n % 100 <= 10 ? 3 :
  328. ($n % 100 >= 11 ? 4 : 5))));
  329. }
  330. return 0;
  331. }
  332. /**
  333. * Binds the given domain to a file in the specified directory.
  334. *
  335. * @param string $domain Domain to bind
  336. * @return string Domain binded
  337. */
  338. protected function _bindTextDomain($domain) {
  339. $this->_noLocale = true;
  340. $core = true;
  341. $merge = array();
  342. $searchPaths = App::path('locales');
  343. $plugins = CakePlugin::loaded();
  344. if (!empty($plugins)) {
  345. foreach ($plugins as $plugin) {
  346. $pluginDomain = Inflector::underscore($plugin);
  347. if ($pluginDomain === $domain) {
  348. $searchPaths[] = CakePlugin::path($plugin) . 'Locale' . DS;
  349. if (!Configure::read('I18n.preferApp')) {
  350. $searchPaths = array_reverse($searchPaths);
  351. }
  352. break;
  353. }
  354. }
  355. }
  356. foreach ($searchPaths as $directory) {
  357. foreach ($this->l10n->languagePath as $lang) {
  358. $localeDef = $directory . $lang . DS . $this->category;
  359. if (is_file($localeDef)) {
  360. $definitions = static::loadLocaleDefinition($localeDef);
  361. if ($definitions !== false) {
  362. $this->_domains[$domain][$this->_lang][$this->category] = $definitions;
  363. $this->_noLocale = false;
  364. return $domain;
  365. }
  366. }
  367. if ($core) {
  368. $app = $directory . $lang . DS . $this->category . DS . 'core';
  369. $translations = false;
  370. if (is_file($app . '.mo')) {
  371. $translations = static::loadMo($app . '.mo');
  372. }
  373. if ($translations === false && is_file($app . '.po')) {
  374. $translations = static::loadPo($app . '.po');
  375. }
  376. if ($translations !== false) {
  377. $this->_domains[$domain][$this->_lang][$this->category] = $translations;
  378. $merge[$domain][$this->_lang][$this->category] = $this->_domains[$domain][$this->_lang][$this->category];
  379. $this->_noLocale = false;
  380. $core = null;
  381. }
  382. }
  383. $file = $directory . $lang . DS . $this->category . DS . $domain;
  384. $translations = false;
  385. if (is_file($file . '.mo')) {
  386. $translations = static::loadMo($file . '.mo');
  387. }
  388. if ($translations === false && is_file($file . '.po')) {
  389. $translations = static::loadPo($file . '.po');
  390. }
  391. if ($translations !== false) {
  392. $this->_domains[$domain][$this->_lang][$this->category] = $translations;
  393. $this->_noLocale = false;
  394. break 2;
  395. }
  396. }
  397. }
  398. if (empty($this->_domains[$domain][$this->_lang][$this->category])) {
  399. $this->_domains[$domain][$this->_lang][$this->category] = array();
  400. return $domain;
  401. }
  402. if (isset($this->_domains[$domain][$this->_lang][$this->category][""])) {
  403. $head = $this->_domains[$domain][$this->_lang][$this->category][""];
  404. foreach (explode("\n", $head) as $line) {
  405. $header = strtok($line, ':');
  406. $line = trim(strtok("\n"));
  407. $this->_domains[$domain][$this->_lang][$this->category]["%po-header"][strtolower($header)] = $line;
  408. }
  409. if (isset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"])) {
  410. $switch = preg_replace("/(?:[() {}\\[\\]^\\s*\\]]+)/", "", $this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"]);
  411. $this->_domains[$domain][$this->_lang][$this->category]["%plural-c"] = $switch;
  412. unset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]);
  413. }
  414. $this->_domains = Hash::mergeDiff($this->_domains, $merge);
  415. if (isset($this->_domains[$domain][$this->_lang][$this->category][null])) {
  416. unset($this->_domains[$domain][$this->_lang][$this->category][null]);
  417. }
  418. }
  419. return $domain;
  420. }
  421. /**
  422. * Loads the binary .mo file and returns array of translations
  423. *
  424. * @param string $filename Binary .mo file to load
  425. * @return mixed Array of translations on success or false on failure
  426. */
  427. public static function loadMo($filename) {
  428. $translations = false;
  429. // @codingStandardsIgnoreStart
  430. // Binary files extracted makes non-standard local variables
  431. if ($data = file_get_contents($filename)) {
  432. $translations = array();
  433. $context = null;
  434. $header = substr($data, 0, 20);
  435. $header = unpack('L1magic/L1version/L1count/L1o_msg/L1o_trn', $header);
  436. extract($header);
  437. if ((dechex($magic) === '950412de' || dechex($magic) === 'ffffffff950412de') && !$version) {
  438. for ($n = 0; $n < $count; $n++) {
  439. $r = unpack("L1len/L1offs", substr($data, $o_msg + $n * 8, 8));
  440. $msgid = substr($data, $r["offs"], $r["len"]);
  441. unset($msgid_plural);
  442. if (strpos($msgid, "\x04") !== false) {
  443. list($context, $msgid) = explode("\x04", $msgid);
  444. }
  445. if (strpos($msgid, "\000")) {
  446. list($msgid, $msgid_plural) = explode("\000", $msgid);
  447. }
  448. $r = unpack("L1len/L1offs", substr($data, $o_trn + $n * 8, 8));
  449. $msgstr = substr($data, $r["offs"], $r["len"]);
  450. if (strpos($msgstr, "\000")) {
  451. $msgstr = explode("\000", $msgstr);
  452. }
  453. if ($msgid != '') {
  454. $translations[$msgid][$context] = $msgstr;
  455. } else {
  456. $translations[$msgid] = $msgstr;
  457. }
  458. if (isset($msgid_plural)) {
  459. $translations[$msgid_plural] =& $translations[$msgid];
  460. }
  461. }
  462. }
  463. }
  464. // @codingStandardsIgnoreEnd
  465. return $translations;
  466. }
  467. /**
  468. * Loads the text .po file and returns array of translations
  469. *
  470. * @param string $filename Text .po file to load
  471. * @return mixed Array of translations on success or false on failure
  472. */
  473. public static function loadPo($filename) {
  474. if (!$file = fopen($filename, 'r')) {
  475. return false;
  476. }
  477. $type = 0;
  478. $translations = array();
  479. $translationKey = '';
  480. $translationContext = null;
  481. $plural = 0;
  482. $header = '';
  483. do {
  484. $line = trim(fgets($file));
  485. if ($line === '' || $line[0] === '#') {
  486. $translationContext = null;
  487. continue;
  488. }
  489. if (preg_match("/msgid[[:space:]]+\"(.+)\"$/i", $line, $regs)) {
  490. $type = 1;
  491. $translationKey = stripcslashes($regs[1]);
  492. } elseif (preg_match("/msgid[[:space:]]+\"\"$/i", $line, $regs)) {
  493. $type = 2;
  494. $translationKey = '';
  495. } elseif (preg_match("/msgctxt[[:space:]]+\"(.+)\"$/i", $line, $regs)) {
  496. $translationContext = $regs[1];
  497. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && ($type == 1 || $type == 2 || $type == 3)) {
  498. $type = 3;
  499. $translationKey .= stripcslashes($regs[1]);
  500. } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
  501. $translations[$translationKey][$translationContext] = stripcslashes($regs[1]);
  502. $type = 4;
  503. } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) {
  504. $type = 4;
  505. $translations[$translationKey][$translationContext] = '';
  506. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 4 && $translationKey) {
  507. $translations[$translationKey][$translationContext] .= stripcslashes($regs[1]);
  508. } elseif (preg_match("/msgid_plural[[:space:]]+\".*\"$/i", $line, $regs)) {
  509. $type = 6;
  510. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 6 && $translationKey) {
  511. $type = 6;
  512. } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
  513. $plural = $regs[1];
  514. $translations[$translationKey][$translationContext][$plural] = stripcslashes($regs[2]);
  515. $type = 7;
  516. } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) {
  517. $plural = $regs[1];
  518. $translations[$translationKey][$translationContext][$plural] = '';
  519. $type = 7;
  520. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 7 && $translationKey) {
  521. $translations[$translationKey][$translationContext][$plural] .= stripcslashes($regs[1]);
  522. } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && $type == 2 && !$translationKey) {
  523. $header .= stripcslashes($regs[1]);
  524. $type = 5;
  525. } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && !$translationKey) {
  526. $header = '';
  527. $type = 5;
  528. } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 5) {
  529. $header .= stripcslashes($regs[1]);
  530. } else {
  531. unset($translations[$translationKey][$translationContext]);
  532. $type = 0;
  533. $translationKey = '';
  534. $translationContext = null;
  535. $plural = 0;
  536. }
  537. } while (!feof($file));
  538. fclose($file);
  539. $merge[''] = $header;
  540. return array_merge($merge, $translations);
  541. }
  542. /**
  543. * Parses a locale definition file following the POSIX standard
  544. *
  545. * @param string $filename Locale definition filename
  546. * @return mixed Array of definitions on success or false on failure
  547. */
  548. public static function loadLocaleDefinition($filename) {
  549. if (!$file = fopen($filename, 'r')) {
  550. return false;
  551. }
  552. $definitions = array();
  553. $comment = '#';
  554. $escape = '\\';
  555. $currentToken = false;
  556. $value = '';
  557. $_this = I18n::getInstance();
  558. while ($line = fgets($file)) {
  559. $line = trim($line);
  560. if (empty($line) || $line[0] === $comment) {
  561. continue;
  562. }
  563. $parts = preg_split("/[[:space:]]+/", $line);
  564. if ($parts[0] === 'comment_char') {
  565. $comment = $parts[1];
  566. continue;
  567. }
  568. if ($parts[0] === 'escape_char') {
  569. $escape = $parts[1];
  570. continue;
  571. }
  572. $count = count($parts);
  573. if ($count === 2) {
  574. $currentToken = $parts[0];
  575. $value = $parts[1];
  576. } elseif ($count === 1) {
  577. $value = is_array($value) ? $parts[0] : $value . $parts[0];
  578. } else {
  579. continue;
  580. }
  581. $len = strlen($value) - 1;
  582. if ($value[$len] === $escape) {
  583. $value = substr($value, 0, $len);
  584. continue;
  585. }
  586. $mustEscape = array($escape . ',', $escape . ';', $escape . '<', $escape . '>', $escape . $escape);
  587. $replacements = array_map('crc32', $mustEscape);
  588. $value = str_replace($mustEscape, $replacements, $value);
  589. $value = explode(';', $value);
  590. $_this->_escape = $escape;
  591. foreach ($value as $i => $val) {
  592. $val = trim($val, '"');
  593. $val = preg_replace_callback('/(?:<)?(.[^>]*)(?:>)?/', array(&$_this, '_parseLiteralValue'), $val);
  594. $val = str_replace($replacements, $mustEscape, $val);
  595. $value[$i] = $val;
  596. }
  597. if (count($value) === 1) {
  598. $definitions[$currentToken] = array_pop($value);
  599. } else {
  600. $definitions[$currentToken] = $value;
  601. }
  602. }
  603. return $definitions;
  604. }
  605. /**
  606. * Puts the parameters in raw translated strings
  607. *
  608. * @param string $translated The raw translated string
  609. * @param array $args The arguments to put in the translation
  610. * @return string Translated string with arguments
  611. */
  612. public static function insertArgs($translated, array $args) {
  613. $len = count($args);
  614. if ($len === 0 || ($len === 1 && $args[0] === null)) {
  615. return $translated;
  616. }
  617. if (is_array($args[0])) {
  618. $args = $args[0];
  619. }
  620. $translated = preg_replace('/(?<!%)%(?![%\'\-+bcdeEfFgGosuxX\d\.])/', '%%', $translated);
  621. return vsprintf($translated, $args);
  622. }
  623. /**
  624. * Auxiliary function to parse a symbol from a locale definition file
  625. *
  626. * @param string $string Symbol to be parsed
  627. * @return string parsed symbol
  628. */
  629. protected function _parseLiteralValue($string) {
  630. $string = $string[1];
  631. if (substr($string, 0, 2) === $this->_escape . 'x') {
  632. $delimiter = $this->_escape . 'x';
  633. return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string)))));
  634. }
  635. if (substr($string, 0, 2) === $this->_escape . 'd') {
  636. $delimiter = $this->_escape . 'd';
  637. return implode('', array_map('chr', array_filter(explode($delimiter, $string))));
  638. }
  639. if ($string[0] === $this->_escape && isset($string[1]) && is_numeric($string[1])) {
  640. $delimiter = $this->_escape;
  641. return implode('', array_map('chr', array_filter(explode($delimiter, $string))));
  642. }
  643. if (substr($string, 0, 3) === 'U00') {
  644. $delimiter = 'U00';
  645. return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string)))));
  646. }
  647. if (preg_match('/U([0-9a-fA-F]{4})/', $string, $match)) {
  648. return Multibyte::ascii(array(hexdec($match[1])));
  649. }
  650. return $string;
  651. }
  652. /**
  653. * Returns a Time format definition from corresponding domain
  654. *
  655. * @param string $format Format to be translated
  656. * @param string $domain Domain where format is stored
  657. * @return mixed translated format string if only value or array of translated strings for corresponding format.
  658. */
  659. protected function _translateTime($format, $domain) {
  660. if (!empty($this->_domains[$domain][$this->_lang]['LC_TIME'][$format])) {
  661. if (($trans = $this->_domains[$domain][$this->_lang][$this->category][$format])) {
  662. return $trans;
  663. }
  664. }
  665. return $format;
  666. }
  667. }