Projects : mp-wp : mp-wp_genesis
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 | ?> |