]>
Commit | Line | Data |
---|---|---|
1 | <?php | |
2 | /* | |
3 | Copyright (c) 2003 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 | return array_shift(unpack('V', $this->STREAM->read(4))); | |
67 | } else { | |
68 | // big endian | |
69 | return array_shift(unpack('N', $this->STREAM->read(4))); | |
70 | } | |
71 | } | |
72 | ||
73 | /** | |
74 | * Reads an array of Integers from the Stream | |
75 | * | |
76 | * @param int count How many elements should be read | |
77 | * @return Array of Integers | |
78 | */ | |
79 | function readintarray($count) { | |
80 | if ($this->BYTEORDER == 0) { | |
81 | // low endian | |
82 | return unpack('V'.$count, $this->STREAM->read(4 * $count)); | |
83 | } else { | |
84 | // big endian | |
85 | return unpack('N'.$count, $this->STREAM->read(4 * $count)); | |
86 | } | |
87 | } | |
88 | ||
89 | /** | |
90 | * Constructor | |
91 | * | |
92 | * @param object Reader the StreamReader object | |
93 | * @param boolean enable_cache Enable or disable caching of strings (default on) | |
94 | */ | |
95 | function gettext_reader($Reader, $enable_cache = true) { | |
96 | // If there isn't a StreamReader, turn on short circuit mode. | |
97 | if (! $Reader || isset($Reader->error) ) { | |
98 | $this->short_circuit = true; | |
99 | return; | |
100 | } | |
101 | ||
102 | // Caching can be turned off | |
103 | $this->enable_cache = $enable_cache; | |
104 | ||
105 | // $MAGIC1 = (int)0x950412de; //bug in PHP 5 | |
106 | $MAGIC1 = (int) - 1794895138; | |
107 | // $MAGIC2 = (int)0xde120495; //bug | |
108 | $MAGIC2 = (int) - 569244523; | |
109 | ||
110 | $this->STREAM = $Reader; | |
111 | $magic = $this->readint(); | |
112 | if ($magic == $MAGIC1) { | |
113 | $this->BYTEORDER = 0; | |
114 | } elseif ($magic == $MAGIC2) { | |
115 | $this->BYTEORDER = 1; | |
116 | } else { | |
117 | $this->error = 1; // not MO file | |
118 | return false; | |
119 | } | |
120 | ||
121 | // FIXME: Do we care about revision? We should. | |
122 | $revision = $this->readint(); | |
123 | ||
124 | $this->total = $this->readint(); | |
125 | $this->originals = $this->readint(); | |
126 | $this->translations = $this->readint(); | |
127 | } | |
128 | ||
129 | /** | |
130 | * Loads the translation tables from the MO file into the cache | |
131 | * If caching is enabled, also loads all strings into a cache | |
132 | * to speed up translation lookups | |
133 | * | |
134 | * @access private | |
135 | */ | |
136 | function load_tables() { | |
137 | if (is_array($this->cache_translations) && | |
138 | is_array($this->table_originals) && | |
139 | is_array($this->table_translations)) | |
140 | return; | |
141 | ||
142 | /* get original and translations tables */ | |
143 | $this->STREAM->seekto($this->originals); | |
144 | $this->table_originals = $this->readintarray($this->total * 2); | |
145 | $this->STREAM->seekto($this->translations); | |
146 | $this->table_translations = $this->readintarray($this->total * 2); | |
147 | ||
148 | if ($this->enable_cache) { | |
149 | $this->cache_translations = array (); | |
150 | /* read all strings in the cache */ | |
151 | for ($i = 0; $i < $this->total; $i++) { | |
152 | $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); | |
153 | $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); | |
154 | $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); | |
155 | $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); | |
156 | $this->cache_translations[$original] = $translation; | |
157 | } | |
158 | } | |
159 | } | |
160 | ||
161 | /** | |
162 | * Returns a string from the "originals" table | |
163 | * | |
164 | * @access private | |
165 | * @param int num Offset number of original string | |
166 | * @return string Requested string if found, otherwise '' | |
167 | */ | |
168 | function get_original_string($num) { | |
169 | $length = $this->table_originals[$num * 2 + 1]; | |
170 | $offset = $this->table_originals[$num * 2 + 2]; | |
171 | if (! $length) | |
172 | return ''; | |
173 | $this->STREAM->seekto($offset); | |
174 | $data = $this->STREAM->read($length); | |
175 | return (string)$data; | |
176 | } | |
177 | ||
178 | /** | |
179 | * Returns a string from the "translations" table | |
180 | * | |
181 | * @access private | |
182 | * @param int num Offset number of original string | |
183 | * @return string Requested string if found, otherwise '' | |
184 | */ | |
185 | function get_translation_string($num) { | |
186 | $length = $this->table_translations[$num * 2 + 1]; | |
187 | $offset = $this->table_translations[$num * 2 + 2]; | |
188 | if (! $length) | |
189 | return ''; | |
190 | $this->STREAM->seekto($offset); | |
191 | $data = $this->STREAM->read($length); | |
192 | return (string)$data; | |
193 | } | |
194 | ||
195 | /** | |
196 | * Binary search for string | |
197 | * | |
198 | * @access private | |
199 | * @param string string | |
200 | * @param int start (internally used in recursive function) | |
201 | * @param int end (internally used in recursive function) | |
202 | * @return int string number (offset in originals table) | |
203 | */ | |
204 | function find_string($string, $start = -1, $end = -1) { | |
205 | if (($start == -1) or ($end == -1)) { | |
206 | // find_string is called with only one parameter, set start end end | |
207 | $start = 0; | |
208 | $end = $this->total; | |
209 | } | |
210 | if (abs($start - $end) <= 1) { | |
211 | // We're done, now we either found the string, or it doesn't exist | |
212 | $txt = $this->get_original_string($start); | |
213 | if ($string == $txt) | |
214 | return $start; | |
215 | else | |
216 | return -1; | |
217 | } else if ($start > $end) { | |
218 | // start > end -> turn around and start over | |
219 | return $this->find_string($string, $end, $start); | |
220 | } else { | |
221 | // Divide table in two parts | |
222 | $half = (int)(($start + $end) / 2); | |
223 | $cmp = strcmp($string, $this->get_original_string($half)); | |
224 | if ($cmp == 0) | |
225 | // string is exactly in the middle => return it | |
226 | return $half; | |
227 | else if ($cmp < 0) | |
228 | // The string is in the upper half | |
229 | return $this->find_string($string, $start, $half); | |
230 | else | |
231 | // The string is in the lower half | |
232 | return $this->find_string($string, $half, $end); | |
233 | } | |
234 | } | |
235 | ||
236 | /** | |
237 | * Translates a string | |
238 | * | |
239 | * @access public | |
240 | * @param string string to be translated | |
241 | * @return string translated string (or original, if not found) | |
242 | */ | |
243 | function translate($string) { | |
244 | if ($this->short_circuit) | |
245 | return $string; | |
246 | $this->load_tables(); | |
247 | ||
248 | if ($this->enable_cache) { | |
249 | // Caching enabled, get translated string from cache | |
250 | if (array_key_exists($string, $this->cache_translations)) | |
251 | return $this->cache_translations[$string]; | |
252 | else | |
253 | return $string; | |
254 | } else { | |
255 | // Caching not enabled, try to find string | |
256 | $num = $this->find_string($string); | |
257 | if ($num == -1) | |
258 | return $string; | |
259 | else | |
260 | return $this->get_translation_string($num); | |
261 | } | |
262 | } | |
263 | ||
264 | /** | |
265 | * Get possible plural forms from MO header | |
266 | * | |
267 | * @access private | |
268 | * @return string plural form header | |
269 | */ | |
270 | function get_plural_forms() { | |
271 | // lets assume message number 0 is header | |
272 | // this is true, right? | |
273 | $this->load_tables(); | |
274 | ||
275 | // cache header field for plural forms | |
276 | if (! is_string($this->pluralheader)) { | |
277 | if ($this->enable_cache) { | |
278 | $header = $this->cache_translations[""]; | |
279 | } else { | |
280 | $header = $this->get_translation_string(0); | |
281 | } | |
282 | if (eregi("plural-forms: ([^\n]*)\n", $header, $regs)) | |
283 | $expr = $regs[1]; | |
284 | else | |
285 | $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; | |
286 | $this->pluralheader = $expr; | |
287 | } | |
288 | return $this->pluralheader; | |
289 | } | |
290 | ||
291 | /** | |
292 | * Detects which plural form to take | |
293 | * | |
294 | * @access private | |
295 | * @param n count | |
296 | * @return int array index of the right plural form | |
297 | */ | |
298 | function select_string($n) { | |
299 | $string = $this->get_plural_forms(); | |
300 | $string = str_replace('nplurals',"\$total",$string); | |
301 | $string = str_replace("n",$n,$string); | |
302 | $string = str_replace('plural',"\$plural",$string); | |
303 | ||
304 | $total = 0; | |
305 | $plural = 0; | |
306 | ||
307 | eval("$string"); | |
308 | if ($plural >= $total) $plural = $total - 1; | |
309 | return $plural; | |
310 | } | |
311 | ||
312 | /** | |
313 | * Plural version of gettext | |
314 | * | |
315 | * @access public | |
316 | * @param string single | |
317 | * @param string plural | |
318 | * @param string number | |
319 | * @return translated plural form | |
320 | */ | |
321 | function ngettext($single, $plural, $number) { | |
322 | if ($this->short_circuit) { | |
323 | if ($number != 1) | |
324 | return $plural; | |
325 | else | |
326 | return $single; | |
327 | } | |
328 | ||
329 | // find out the appropriate form | |
330 | $select = $this->select_string($number); | |
331 | ||
332 | // this should contains all strings separated by NULLs | |
333 | $key = $single.chr(0).$plural; | |
334 | ||
335 | ||
336 | if ($this->enable_cache) { | |
337 | if (! array_key_exists($key, $this->cache_translations)) { | |
338 | return ($number != 1) ? $plural : $single; | |
339 | } else { | |
340 | $result = $this->cache_translations[$key]; | |
341 | $list = explode(chr(0), $result); | |
342 | return $list[$select]; | |
343 | } | |
344 | } else { | |
345 | $num = $this->find_string($key); | |
346 | if ($num == -1) { | |
347 | return ($number != 1) ? $plural : $single; | |
348 | } else { | |
349 | $result = $this->get_translation_string($num); | |
350 | $list = explode(chr(0), $result); | |
351 | return $list[$select]; | |
352 | } | |
353 | } | |
354 | } | |
355 | ||
356 | } | |
357 | ||
358 | ?> |