]>
Commit | Line | Data |
---|---|---|
1 | <?php | |
2 | /* | |
3 | Copyright (c) 2003, 2009 Danilo Segan <danilo@kvota.net>. | |
4 | Copyright (c) 2005 Nico Kaiser <nico@siriux.net> | |
5 | ||
6 | This file is part of PHP-gettext. | |
7 | ||
8 | PHP-gettext is free software; you can redistribute it and/or modify | |
9 | it under the terms of the GNU General Public License as published by | |
10 | the Free Software Foundation; either version 2 of the License, or | |
11 | (at your option) any later version. | |
12 | ||
13 | PHP-gettext is distributed in the hope that it will be useful, | |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | GNU General Public License for more details. | |
17 | ||
18 | You should have received a copy of the GNU General Public License | |
19 | along with PHP-gettext; if not, write to the Free Software | |
20 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
21 | ||
22 | */ | |
23 | ||
24 | /** | |
25 | * Provides a simple gettext replacement that works independently from | |
26 | * the system's gettext abilities. | |
27 | * It can read MO files and use them for translating strings. | |
28 | * The files are passed to gettext_reader as a Stream (see streams.php) | |
29 | * | |
30 | * This version has the ability to cache all strings and translations to | |
31 | * speed up the string lookup. | |
32 | * While the cache is enabled by default, it can be switched off with the | |
33 | * second parameter in the constructor (e.g. whenusing very large MO files | |
34 | * that you don't want to keep in memory) | |
35 | */ | |
36 | class gettext_reader { | |
37 | //public: | |
38 | var $error = 0; // public variable that holds error code (0 if no error) | |
39 | ||
40 | //private: | |
41 | var $BYTEORDER = 0; // 0: low endian, 1: big endian | |
42 | var $STREAM = NULL; | |
43 | var $short_circuit = false; | |
44 | var $enable_cache = false; | |
45 | var $originals = NULL; // offset of original table | |
46 | var $translations = NULL; // offset of translation table | |
47 | var $pluralheader = NULL; // cache header field for plural forms | |
48 | var $total = 0; // total string count | |
49 | var $table_originals = NULL; // table for original strings (offsets) | |
50 | var $table_translations = NULL; // table for translated strings (offsets) | |
51 | var $cache_translations = NULL; // original -> translation mapping | |
52 | ||
53 | ||
54 | /* Methods */ | |
55 | ||
56 | ||
57 | /** | |
58 | * Reads a 32bit Integer from the Stream | |
59 | * | |
60 | * @access private | |
61 | * @return Integer from the Stream | |
62 | */ | |
63 | function readint() { | |
64 | if ($this->BYTEORDER == 0) { | |
65 | // low endian | |
66 | $input=unpack('V', $this->STREAM->read(4)); | |
67 | return array_shift($input); | |
68 | } else { | |
69 | // big endian | |
70 | $input=unpack('N', $this->STREAM->read(4)); | |
71 | return array_shift($input); | |
72 | } | |
73 | } | |
74 | ||
75 | function read($bytes) { | |
76 | return $this->STREAM->read($bytes); | |
77 | } | |
78 | ||
79 | /** | |
80 | * Reads an array of Integers from the Stream | |
81 | * | |
82 | * @param int count How many elements should be read | |
83 | * @return Array of Integers | |
84 | */ | |
85 | function readintarray($count) { | |
86 | if ($this->BYTEORDER == 0) { | |
87 | // low endian | |
88 | return unpack('V'.$count, $this->STREAM->read(4 * $count)); | |
89 | } else { | |
90 | // big endian | |
91 | return unpack('N'.$count, $this->STREAM->read(4 * $count)); | |
92 | } | |
93 | } | |
94 | ||
95 | /** | |
96 | * Constructor | |
97 | * | |
98 | * @param object Reader the StreamReader object | |
99 | * @param boolean enable_cache Enable or disable caching of strings (default on) | |
100 | */ | |
101 | function __construct($Reader, $enable_cache = true) { | |
102 | // If there isn't a StreamReader, turn on short circuit mode. | |
103 | if (! $Reader || isset($Reader->error) ) { | |
104 | $this->short_circuit = true; | |
105 | return; | |
106 | } | |
107 | ||
108 | // Caching can be turned off | |
109 | $this->enable_cache = $enable_cache; | |
110 | ||
111 | $MAGIC1 = "\x95\x04\x12\xde"; | |
112 | $MAGIC2 = "\xde\x12\x04\x95"; | |
113 | ||
114 | $this->STREAM = $Reader; | |
115 | $magic = $this->read(4); | |
116 | if ($magic == $MAGIC1) { | |
117 | $this->BYTEORDER = 1; | |
118 | } elseif ($magic == $MAGIC2) { | |
119 | $this->BYTEORDER = 0; | |
120 | } else { | |
121 | $this->error = 1; // not MO file | |
122 | return false; | |
123 | } | |
124 | ||
125 | // FIXME: Do we care about revision? We should. | |
126 | $revision = $this->readint(); | |
127 | ||
128 | $this->total = $this->readint(); | |
129 | $this->originals = $this->readint(); | |
130 | $this->translations = $this->readint(); | |
131 | } | |
132 | ||
133 | /** | |
134 | * Loads the translation tables from the MO file into the cache | |
135 | * If caching is enabled, also loads all strings into a cache | |
136 | * to speed up translation lookups | |
137 | * | |
138 | * @access private | |
139 | */ | |
140 | function load_tables() { | |
141 | if (is_array($this->cache_translations) && | |
142 | is_array($this->table_originals) && | |
143 | is_array($this->table_translations)) | |
144 | return; | |
145 | ||
146 | /* get original and translations tables */ | |
147 | if (!is_array($this->table_originals)) { | |
148 | $this->STREAM->seekto($this->originals); | |
149 | $this->table_originals = $this->readintarray($this->total * 2); | |
150 | } | |
151 | if (!is_array($this->table_translations)) { | |
152 | $this->STREAM->seekto($this->translations); | |
153 | $this->table_translations = $this->readintarray($this->total * 2); | |
154 | } | |
155 | ||
156 | if ($this->enable_cache) { | |
157 | $this->cache_translations = array (); | |
158 | /* read all strings in the cache */ | |
159 | for ($i = 0; $i < $this->total; $i++) { | |
160 | $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); | |
161 | $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); | |
162 | $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); | |
163 | $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); | |
164 | $this->cache_translations[$original] = $translation; | |
165 | } | |
166 | } | |
167 | } | |
168 | ||
169 | /** | |
170 | * Returns a string from the "originals" table | |
171 | * | |
172 | * @access private | |
173 | * @param int num Offset number of original string | |
174 | * @return string Requested string if found, otherwise '' | |
175 | */ | |
176 | function get_original_string($num) { | |
177 | $length = $this->table_originals[$num * 2 + 1]; | |
178 | $offset = $this->table_originals[$num * 2 + 2]; | |
179 | if (! $length) | |
180 | return ''; | |
181 | $this->STREAM->seekto($offset); | |
182 | $data = $this->STREAM->read($length); | |
183 | return (string)$data; | |
184 | } | |
185 | ||
186 | /** | |
187 | * Returns a string from the "translations" table | |
188 | * | |
189 | * @access private | |
190 | * @param int num Offset number of original string | |
191 | * @return string Requested string if found, otherwise '' | |
192 | */ | |
193 | function get_translation_string($num) { | |
194 | $length = $this->table_translations[$num * 2 + 1]; | |
195 | $offset = $this->table_translations[$num * 2 + 2]; | |
196 | if (! $length) | |
197 | return ''; | |
198 | $this->STREAM->seekto($offset); | |
199 | $data = $this->STREAM->read($length); | |
200 | return (string)$data; | |
201 | } | |
202 | ||
203 | /** | |
204 | * Binary search for string | |
205 | * | |
206 | * @access private | |
207 | * @param string string | |
208 | * @param int start (internally used in recursive function) | |
209 | * @param int end (internally used in recursive function) | |
210 | * @return int string number (offset in originals table) | |
211 | */ | |
212 | function find_string($string, $start = -1, $end = -1) { | |
213 | if (($start == -1) or ($end == -1)) { | |
214 | // find_string is called with only one parameter, set start end end | |
215 | $start = 0; | |
216 | $end = $this->total; | |
217 | } | |
218 | if (abs($start - $end) <= 1) { | |
219 | // We're done, now we either found the string, or it doesn't exist | |
220 | $txt = $this->get_original_string($start); | |
221 | if ($string == $txt) | |
222 | return $start; | |
223 | else | |
224 | return -1; | |
225 | } else if ($start > $end) { | |
226 | // start > end -> turn around and start over | |
227 | return $this->find_string($string, $end, $start); | |
228 | } else { | |
229 | // Divide table in two parts | |
230 | $half = (int)(($start + $end) / 2); | |
231 | $cmp = strcmp($string, $this->get_original_string($half)); | |
232 | if ($cmp == 0) | |
233 | // string is exactly in the middle => return it | |
234 | return $half; | |
235 | else if ($cmp < 0) | |
236 | // The string is in the upper half | |
237 | return $this->find_string($string, $start, $half); | |
238 | else | |
239 | // The string is in the lower half | |
240 | return $this->find_string($string, $half, $end); | |
241 | } | |
242 | } | |
243 | ||
244 | /** | |
245 | * Translates a string | |
246 | * | |
247 | * @access public | |
248 | * @param string string to be translated | |
249 | * @return string translated string (or original, if not found) | |
250 | */ | |
251 | function translate($string) { | |
252 | if ($this->short_circuit) | |
253 | return $string; | |
254 | $this->load_tables(); | |
255 | ||
256 | if ($this->enable_cache) { | |
257 | // Caching enabled, get translated string from cache | |
258 | if (array_key_exists($string, $this->cache_translations)) | |
259 | return $this->cache_translations[$string]; | |
260 | else | |
261 | return $string; | |
262 | } else { | |
263 | // Caching not enabled, try to find string | |
264 | $num = $this->find_string($string); | |
265 | if ($num == -1) | |
266 | return $string; | |
267 | else | |
268 | return $this->get_translation_string($num); | |
269 | } | |
270 | } | |
271 | ||
272 | /** | |
273 | * Sanitize plural form expression for use in PHP eval call. | |
274 | * | |
275 | * @access private | |
276 | * @return string sanitized plural form expression | |
277 | */ | |
278 | function sanitize_plural_expression($expr) { | |
279 | // Get rid of disallowed characters. | |
280 | $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); | |
281 | ||
282 | // Add parenthesis for tertiary '?' operator. | |
283 | $expr .= ';'; | |
284 | $res = ''; | |
285 | $p = 0; | |
286 | for ($i = 0; $i < strlen($expr); $i++) { | |
287 | $ch = $expr[$i]; | |
288 | switch ($ch) { | |
289 | case '?': | |
290 | $res .= ' ? ('; | |
291 | $p++; | |
292 | break; | |
293 | case ':': | |
294 | $res .= ') : ('; | |
295 | break; | |
296 | case ';': | |
297 | $res .= str_repeat( ')', $p) . ';'; | |
298 | $p = 0; | |
299 | break; | |
300 | default: | |
301 | $res .= $ch; | |
302 | } | |
303 | } | |
304 | return $res; | |
305 | } | |
306 | ||
307 | /** | |
308 | * Parse full PO header and extract only plural forms line. | |
309 | * | |
310 | * @access private | |
311 | * @return string verbatim plural form header field | |
312 | */ | |
313 | function extract_plural_forms_header_from_po_header($header) { | |
314 | if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) | |
315 | $expr = $regs[2]; | |
316 | else | |
317 | $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; | |
318 | return $expr; | |
319 | } | |
320 | ||
321 | /** | |
322 | * Get possible plural forms from MO header | |
323 | * | |
324 | * @access private | |
325 | * @return string plural form header | |
326 | */ | |
327 | function get_plural_forms() { | |
328 | // lets assume message number 0 is header | |
329 | // this is true, right? | |
330 | $this->load_tables(); | |
331 | ||
332 | // cache header field for plural forms | |
333 | if (! is_string($this->pluralheader)) { | |
334 | if ($this->enable_cache) { | |
335 | $header = $this->cache_translations[""]; | |
336 | } else { | |
337 | $header = $this->get_translation_string(0); | |
338 | } | |
339 | $expr = $this->extract_plural_forms_header_from_po_header($header); | |
340 | $this->pluralheader = $this->sanitize_plural_expression($expr); | |
341 | } | |
342 | return $this->pluralheader; | |
343 | } | |
344 | ||
345 | /** | |
346 | * Detects which plural form to take | |
347 | * | |
348 | * @access private | |
349 | * @param n count | |
350 | * @return int array index of the right plural form | |
351 | */ | |
352 | function select_string($n) { | |
353 | if (!is_int($n)) { | |
354 | throw new InvalidArgumentException( | |
355 | "Select_string only accepts integers: " . $n); | |
356 | } | |
357 | $string = $this->get_plural_forms(); | |
358 | $string = str_replace('nplurals',"\$total",$string); | |
359 | $string = str_replace("n",$n,$string); | |
360 | $string = str_replace('plural',"\$plural",$string); | |
361 | ||
362 | $total = 0; | |
363 | $plural = 0; | |
364 | ||
365 | eval("$string"); | |
366 | if ($plural >= $total) $plural = $total - 1; | |
367 | return $plural; | |
368 | } | |
369 | ||
370 | /** | |
371 | * Plural version of gettext | |
372 | * | |
373 | * @access public | |
374 | * @param string single | |
375 | * @param string plural | |
376 | * @param string number | |
377 | * @return translated plural form | |
378 | */ | |
379 | function ngettext($single, $plural, $number) { | |
380 | if ($this->short_circuit) { | |
381 | if ($number != 1) | |
382 | return $plural; | |
383 | else | |
384 | return $single; | |
385 | } | |
386 | ||
387 | // find out the appropriate form | |
388 | $select = $this->select_string($number); | |
389 | ||
390 | // this should contains all strings separated by NULLs | |
391 | $key = $single . chr(0) . $plural; | |
392 | ||
393 | ||
394 | if ($this->enable_cache) { | |
395 | if (! array_key_exists($key, $this->cache_translations)) { | |
396 | return ($number != 1) ? $plural : $single; | |
397 | } else { | |
398 | $result = $this->cache_translations[$key]; | |
399 | $list = explode(chr(0), $result); | |
400 | return $list[$select]; | |
401 | } | |
402 | } else { | |
403 | $num = $this->find_string($key); | |
404 | if ($num == -1) { | |
405 | return ($number != 1) ? $plural : $single; | |
406 | } else { | |
407 | $result = $this->get_translation_string($num); | |
408 | $list = explode(chr(0), $result); | |
409 | return $list[$select]; | |
410 | } | |
411 | } | |
412 | } | |
413 | ||
414 | function pgettext($context, $msgid) { | |
415 | $key = $context . chr(4) . $msgid; | |
416 | $ret = $this->translate($key); | |
417 | if (strpos($ret, "\004") !== FALSE) { | |
418 | return $msgid; | |
419 | } else { | |
420 | return $ret; | |
421 | } | |
422 | } | |
423 | ||
424 | function npgettext($context, $singular, $plural, $number) { | |
425 | $key = $context . chr(4) . $singular; | |
426 | $ret = $this->ngettext($key, $plural, $number); | |
427 | if (strpos($ret, "\004") !== FALSE) { | |
428 | return $singular; | |
429 | } else { | |
430 | return $ret; | |
431 | } | |
432 | ||
433 | } | |
434 | } | |
435 | ||
436 | ?> |