PageRenderTime 44ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/lib/i18n/ZMO.php

https://github.com/ThiloWitt/core
PHP | 548 lines | 251 code | 49 blank | 248 comment | 69 complexity | 4d57a8b6aa0440567acd831e0d00e0c0 MD5 | raw file
  1. <?php
  2. /**
  3. * Zikula Application Framework.
  4. *
  5. * Copyright (c) 2003 Danilo Segan <danilo@kvota.net>
  6. * Copyright (c) 2005 Nico Kaiser <nico@siriux.net>
  7. * Copyright (c) 2009 Zikula Development Team
  8. *
  9. * @link http://www.zikula.org
  10. * @license GNU/GPLv3 (or at your option, any later version).
  11. *
  12. * @package I18n
  13. */
  14. /**
  15. * Provides a simple gettext replacement that works independently from the system's gettext abilities.
  16. *
  17. * It can read MO files and use them for translating strings.
  18. * The files are passed to gettext_reader as a Stream (see streams.php)
  19. *
  20. * This version has the ability to cache all strings and translations to
  21. * speed up the string lookup.
  22. * While the cache is enabled by default, it can be switched off with the
  23. * second parameter in the constructor (e.g. whenusing very large MO files
  24. * that you don't want to keep in memory)
  25. */
  26. class ZMO
  27. {
  28. /**
  29. * Public variable that holds error code (0 if no error).
  30. *
  31. * @var integer
  32. */
  33. public $error = 0;
  34. /**
  35. * Byte order.
  36. *
  37. * Possible values:
  38. * 0: low endian
  39. * 1: big endian.
  40. *
  41. * @var integer
  42. */
  43. private $byteorder = 0;
  44. /**
  45. * Stream.
  46. *
  47. * @var StreamReader_Abstract
  48. */
  49. private $stream = null;
  50. /**
  51. * Short circuit.
  52. *
  53. * @var boolean
  54. */
  55. private $short_circuit = false;
  56. /**
  57. * Enable cache.
  58. *
  59. * @var boolean
  60. */
  61. private $enable_cache = false;
  62. /**
  63. * Offset of original table.
  64. *
  65. * @var integer
  66. */
  67. private $originals = null;
  68. /**
  69. * Offset of translation table.
  70. *
  71. * @var integer
  72. */
  73. private $translations = null;
  74. /**
  75. * Cache header field for plural forms.
  76. *
  77. * @var string
  78. */
  79. private $pluralheader = null;
  80. /**
  81. * Total string count.
  82. *
  83. * @var integer
  84. */
  85. private $total = 0;
  86. /**
  87. * Table for original strings (offsets).
  88. *
  89. * @var array
  90. */
  91. private $table_originals = null;
  92. /**
  93. * Table for translated strings (offsets).
  94. *
  95. * @var array
  96. */
  97. private $table_translations = null;
  98. /**
  99. * Cache translations.
  100. *
  101. * Original -> translation mapping.
  102. *
  103. * @var array
  104. */
  105. private $cache_translations = null;
  106. /**
  107. * Encoding.
  108. *
  109. * @var string
  110. */
  111. private $encoding;
  112. // Methods
  113. /**
  114. * Constructor.
  115. *
  116. * @param StreamReader_Abstract $reader The StreamReader object.
  117. * @param boolean $enable_cache Enable or disable caching of strings (default on).
  118. */
  119. public function __construct(StreamReader_Abstract $reader, $enable_cache = true)
  120. {
  121. // If there isn't a StreamReader, turn on short circuit mode.
  122. if ($reader->getError()) {
  123. $this->short_circuit = true;
  124. return;
  125. }
  126. // Caching can be turned off
  127. $this->enable_cache = $enable_cache;
  128. $this->stream = $reader;
  129. $magic = $this->readint();
  130. if ($magic == -1794895138 || $magic == 2500072158) {
  131. // (int)0x950412de; PHP 5.2 wont convert this properly
  132. $this->byteorder = 0;
  133. } elseif ($magic == -569244523 || $magic == 3725722773) {
  134. // (int)0xde120495; PHP 5.2 wont convert this properly
  135. $this->byteorder = 1;
  136. } else {
  137. // not MO file
  138. $this->error = 1;
  139. return false;
  140. }
  141. // TODO D Do we care about revision?
  142. $revision = $this->readint();
  143. $this->total = $this->readint();
  144. $this->originals = $this->readint();
  145. $this->translations = $this->readint();
  146. $this->encoding = ini_get('mbstring.internal_encoding');
  147. }
  148. /**
  149. * Error getter.
  150. *
  151. * @return string Error.
  152. */
  153. public function getError()
  154. {
  155. return $this->error;
  156. }
  157. /**
  158. * Error getter.
  159. *
  160. * @return string Error.
  161. */
  162. public function getOriginals()
  163. {
  164. return $this->originals;
  165. }
  166. /**
  167. * Translations getter.
  168. *
  169. * @return integer Translations.
  170. */
  171. public function getTranslations()
  172. {
  173. return $this->translations;
  174. }
  175. /**
  176. * Plural header getter.
  177. *
  178. * @return string Plural header.
  179. */
  180. public function getPluralheader()
  181. {
  182. return $this->pluralheader;
  183. }
  184. /**
  185. * Total getter.
  186. *
  187. * @return integer Total.
  188. */
  189. public function getTotal()
  190. {
  191. return $this->total;
  192. }
  193. /**
  194. * Cache translations getter.
  195. *
  196. * @return array Cache translations.
  197. */
  198. public function getCache_translations()
  199. {
  200. return $this->cache_translations;
  201. }
  202. /**
  203. * Encoding getter.
  204. *
  205. * @return string Encoding.
  206. */
  207. public function getEncoding()
  208. {
  209. return $this->encoding;
  210. }
  211. /**
  212. * Set encoding.
  213. *
  214. * @param string $encoding Encoding.
  215. *
  216. * @return void
  217. */
  218. public function setEncoding($encoding)
  219. {
  220. $this->encoding = $encoding;
  221. }
  222. /**
  223. * Encodes text.
  224. *
  225. * @param string $text Text.
  226. *
  227. * @return string
  228. */
  229. public function encode($text)
  230. {
  231. $source_encoding = mb_detect_encoding($text);
  232. if ($source_encoding != $this->encoding) {
  233. return mb_convert_encoding($text, $this->encoding, $source_encoding);
  234. } else {
  235. return $text;
  236. }
  237. }
  238. /**
  239. * Reads a 32bit Integer from the Stream.
  240. *
  241. * @return Integer from the Stream
  242. */
  243. private function readint()
  244. {
  245. if ($this->byteorder == 0) {
  246. // low endian
  247. $data = unpack('V', $this->stream->read(4));
  248. } else {
  249. // big endian
  250. $data = unpack('N', $this->stream->read(4));
  251. }
  252. return array_shift($data);
  253. }
  254. /**
  255. * Reads an array of Integers from the Stream.
  256. *
  257. * @param integer $count How many elements should be read.
  258. *
  259. * @return array Array of Integers.
  260. */
  261. public function readintarray($count)
  262. {
  263. if ($this->byteorder == 0) {
  264. // low endian
  265. return unpack('V' . $count, $this->stream->read(4 * $count));
  266. } else {
  267. // big endian
  268. return unpack('N' . $count, $this->stream->read(4 * $count));
  269. }
  270. }
  271. /**
  272. * Loads the translation tables from the MO file into the cache.
  273. *
  274. * If caching is enabled, also loads all strings into a cache
  275. * to speed up translation lookups.
  276. *
  277. * @return void
  278. */
  279. private function load_tables()
  280. {
  281. if (is_array($this->cache_translations) && is_array($this->table_originals) && is_array($this->table_translations)) {
  282. return;
  283. }
  284. // get original and translations tables
  285. $this->stream->seekto($this->originals);
  286. $this->table_originals = $this->readintarray($this->total * 2);
  287. $this->stream->seekto($this->translations);
  288. $this->table_translations = $this->readintarray($this->total * 2);
  289. if ($this->enable_cache) {
  290. $this->cache_translations = array();
  291. // read all strings in the cache
  292. for ($i = 0; $i < $this->total; $i++) {
  293. $this->stream->seekto($this->table_originals[$i * 2 + 2]);
  294. $original = $this->stream->read($this->table_originals[$i * 2 + 1]);
  295. $this->stream->seekto($this->table_translations[$i * 2 + 2]);
  296. $translation = $this->stream->read($this->table_translations[$i * 2 + 1]);
  297. $this->cache_translations[$original] = $translation;
  298. }
  299. }
  300. }
  301. /**
  302. * Returns a string from the "originals" table.
  303. *
  304. * @param integer $num Offset number of original string.
  305. *
  306. * @return string Requested string if found, otherwise ''.
  307. */
  308. private function get_original_string($num)
  309. {
  310. $length = $this->table_originals[$num * 2 + 1];
  311. $offset = $this->table_originals[$num * 2 + 2];
  312. if (!$length) {
  313. return '';
  314. }
  315. $this->stream->seekto($offset);
  316. $data = $this->stream->read($length);
  317. return (string)$data;
  318. }
  319. /**
  320. * Returns a string from the "translations" table.
  321. *
  322. * @param integer $num Offset number of original string.
  323. *
  324. * @return string Requested string if found, otherwise ''.
  325. */
  326. private function get_translation_string($num)
  327. {
  328. $length = $this->table_translations[$num * 2 + 1];
  329. $offset = $this->table_translations[$num * 2 + 2];
  330. if (!$length) {
  331. return '';
  332. }
  333. $this->stream->seekto($offset);
  334. $data = $this->stream->read($length);
  335. $data = $this->encode($data);
  336. return (string)$data;
  337. }
  338. /**
  339. * Binary search for string
  340. *
  341. * @param string $string String.
  342. * @param integer $start Internally used in recursive function.
  343. * @param integer $end Internally used in recursive function.
  344. *
  345. * @return integer String number (offset in originals table).
  346. */
  347. private function find_string($string, $start = -1, $end = -1)
  348. {
  349. if (($start == -1) || ($end == -1)) {
  350. // find_string is called with only one parameter, set start end end
  351. $start = 0;
  352. $end = $this->total;
  353. }
  354. if (abs($start - $end) <= 1) {
  355. // We're done, now we either found the string, or it doesn't exist
  356. $txt = $this->get_original_string($start);
  357. if ($string == $txt) {
  358. return $start;
  359. } else {
  360. return -1;
  361. }
  362. } else if ($start > $end) {
  363. // start > end -> turn around and start over
  364. return $this->find_string($string, $end, $start);
  365. } else {
  366. // Divide table in two parts
  367. $half = (int)(($start + $end) / 2);
  368. $cmp = strcmp($string, $this->get_original_string($half));
  369. if ($cmp == 0) {
  370. // string is exactly in the middle => return it
  371. return $half;
  372. } else if ($cmp < 0) {
  373. // The string is in the upper half
  374. return $this->find_string($string, $start, $half);
  375. } else {
  376. // The string is in the lower half
  377. return $this->find_string($string, $half, $end);
  378. }
  379. }
  380. }
  381. /**
  382. * Translates a string.
  383. *
  384. * @param string $string Strint to be translated.
  385. *
  386. * @return string Translated string (or original, if not found).
  387. */
  388. public function translate($string)
  389. {
  390. if ($this->short_circuit) {
  391. return $string;
  392. }
  393. $this->load_tables();
  394. if ($this->enable_cache) {
  395. // Caching enabled, get translated string from cache
  396. if (array_key_exists($string, $this->cache_translations)) {
  397. return $this->cache_translations[$string];
  398. } else {
  399. return $string;
  400. }
  401. } else {
  402. // Caching not enabled, try to find string
  403. $num = $this->find_string($string);
  404. if ($num == -1) {
  405. return $string;
  406. } else {
  407. return $this->get_translation_string($num);
  408. }
  409. }
  410. }
  411. /**
  412. * Get possible plural forms from MO header.
  413. *
  414. * @return string plural form header.
  415. */
  416. private function get_plural_forms()
  417. {
  418. // lets assume message number 0 is header
  419. // this is true, right?
  420. $this->load_tables();
  421. // cache header field for plural forms
  422. if (!is_string($this->pluralheader)) {
  423. if ($this->enable_cache) {
  424. $header = $this->cache_translations[''];
  425. } else {
  426. $header = $this->get_translation_string(0);
  427. }
  428. if (preg_match('#(nplurals=\d+;\s{0,}plural=[\s\d\w\(\)\?:%><=!&\|]+)\s{0,};\s{0,}\\n#', $header, $regs)) {
  429. $expr = $regs[1];
  430. } else {
  431. $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
  432. }
  433. $this->pluralheader = $expr .';';
  434. }
  435. return $this->pluralheader;
  436. }
  437. /**
  438. * Detects which plural form to take
  439. *
  440. * @param integer $n Count.
  441. *
  442. * @return integer Array index of the right plural form.
  443. */
  444. private function select_string($n)
  445. {
  446. $string = $this->get_plural_forms();
  447. $string = str_replace('nplurals', "\$total", $string);
  448. $string = str_replace("n", $n, $string);
  449. $string = str_replace('plural', "\$plural", $string);
  450. $total = 0;
  451. $plural = 0;
  452. eval($string);
  453. if ($plural >= $total) {
  454. $plural = $total - 1;
  455. }
  456. return $plural;
  457. }
  458. /**
  459. * Plural version of gettext.
  460. *
  461. * @param string $single Single.
  462. * @param string $plural Plural.
  463. * @param string $number Number.
  464. *
  465. * @return string Translated plural form.
  466. */
  467. public function ngettext($single, $plural, $number)
  468. {
  469. if ($this->short_circuit) {
  470. if ($number != 1) {
  471. return $plural;
  472. } else {
  473. return $single;
  474. }
  475. }
  476. // find out the appropriate form
  477. $select = $this->select_string($number);
  478. // this should contains all strings separated by nulls
  479. $key = $single . chr(0) . $plural;
  480. if ($this->enable_cache) {
  481. if (!array_key_exists($key, $this->cache_translations)) {
  482. return ($number != 1) ? $plural : $single;
  483. } else {
  484. $result = $this->cache_translations[$key];
  485. $list = explode(chr(0), $result);
  486. return $list[$select];
  487. }
  488. } else {
  489. $num = $this->find_string($key);
  490. if ($num == -1) {
  491. return ($number != 1) ? $plural : $single;
  492. } else {
  493. $result = $this->get_translation_string($num);
  494. $list = explode(chr(0), $result);
  495. return $list[$select];
  496. }
  497. }
  498. }
  499. }