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