114 | | $this->STREAM = $Reader; |
115 | | $magic = $this->readint(); |
116 | | if ($magic == ($MAGIC1 & 0xFFFFFFFF) || $magic == ($MAGIC3 & 0xFFFFFFFF)) { // to make sure it works for 64-bit platforms |
117 | | $this->BYTEORDER = 0; |
118 | | } elseif ($magic == ($MAGIC2 & 0xFFFFFFFF)) { |
119 | | $this->BYTEORDER = 1; |
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 | | $this->STREAM->seekto($this->originals); |
148 | | $this->table_originals = $this->readintarray($this->total * 2); |
149 | | $this->STREAM->seekto($this->translations); |
150 | | $this->table_translations = $this->readintarray($this->total * 2); |
151 | | |
152 | | if ($this->enable_cache) { |
153 | | $this->cache_translations = array (); |
154 | | /* read all strings in the cache */ |
155 | | for ($i = 0; $i < $this->total; $i++) { |
156 | | $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); |
157 | | $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); |
158 | | $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); |
159 | | $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); |
160 | | $this->cache_translations[$original] = $translation; |
161 | | } |
162 | | } |
163 | | } |
164 | | |
165 | | /** |
166 | | * Returns a string from the "originals" table |
167 | | * |
168 | | * @access private |
169 | | * @param int num Offset number of original string |
170 | | * @return string Requested string if found, otherwise '' |
171 | | */ |
172 | | function get_original_string($num) { |
173 | | $length = $this->table_originals[$num * 2 + 1]; |
174 | | $offset = $this->table_originals[$num * 2 + 2]; |
175 | | if (! $length) |
176 | | return ''; |
177 | | $this->STREAM->seekto($offset); |
178 | | $data = $this->STREAM->read($length); |
179 | | return (string)$data; |
180 | | } |
181 | | |
182 | | /** |
183 | | * Returns a string from the "translations" table |
184 | | * |
185 | | * @access private |
186 | | * @param int num Offset number of original string |
187 | | * @return string Requested string if found, otherwise '' |
188 | | */ |
189 | | function get_translation_string($num) { |
190 | | $length = $this->table_translations[$num * 2 + 1]; |
191 | | $offset = $this->table_translations[$num * 2 + 2]; |
192 | | if (! $length) |
193 | | return ''; |
194 | | $this->STREAM->seekto($offset); |
195 | | $data = $this->STREAM->read($length); |
196 | | return (string)$data; |
197 | | } |
198 | | |
199 | | /** |
200 | | * Binary search for string |
201 | | * |
202 | | * @access private |
203 | | * @param string string |
204 | | * @param int start (internally used in recursive function) |
205 | | * @param int end (internally used in recursive function) |
206 | | * @return int string number (offset in originals table) |
207 | | */ |
208 | | function find_string($string, $start = -1, $end = -1) { |
209 | | if (($start == -1) or ($end == -1)) { |
210 | | // find_string is called with only one parameter, set start end end |
211 | | $start = 0; |
212 | | $end = $this->total; |
213 | | } |
214 | | if (abs($start - $end) <= 1) { |
215 | | // We're done, now we either found the string, or it doesn't exist |
216 | | $txt = $this->get_original_string($start); |
217 | | if ($string == $txt) |
218 | | return $start; |
219 | | else |
220 | | return -1; |
221 | | } else if ($start > $end) { |
222 | | // start > end -> turn around and start over |
223 | | return $this->find_string($string, $end, $start); |
224 | | } else { |
225 | | // Divide table in two parts |
226 | | $half = (int)(($start + $end) / 2); |
227 | | $cmp = strcmp($string, $this->get_original_string($half)); |
228 | | if ($cmp == 0) |
229 | | // string is exactly in the middle => return it |
230 | | return $half; |
231 | | else if ($cmp < 0) |
232 | | // The string is in the upper half |
233 | | return $this->find_string($string, $start, $half); |
234 | | else |
235 | | // The string is in the lower half |
236 | | return $this->find_string($string, $half, $end); |
237 | | } |
238 | | } |
239 | | |
240 | | /** |
241 | | * Translates a string |
242 | | * |
243 | | * @access public |
244 | | * @param string string to be translated |
245 | | * @return string translated string (or original, if not found) |
246 | | */ |
247 | | function translate($string) { |
248 | | if ($this->short_circuit) |
249 | | return $string; |
250 | | $this->load_tables(); |
251 | | |
252 | | if ($this->enable_cache) { |
253 | | // Caching enabled, get translated string from cache |
254 | | if (array_key_exists($string, $this->cache_translations)) |
255 | | return $this->cache_translations[$string]; |
256 | | else |
257 | | return $string; |
258 | | } else { |
259 | | // Caching not enabled, try to find string |
260 | | $num = $this->find_string($string); |
261 | | if ($num == -1) |
262 | | return $string; |
263 | | else |
264 | | return $this->get_translation_string($num); |
265 | | } |
266 | | } |
| 58 | /** |
| 59 | * Reads a 32bit Integer from the Stream |
| 60 | * |
| 61 | * @access private |
| 62 | * @return Integer from the Stream |
| 63 | */ |
| 64 | function readint() { |
| 65 | if ($this->BYTEORDER == 0) { |
| 66 | // low endian |
| 67 | $low_end = unpack('V', $this->STREAM->read(4)); |
| 68 | return array_shift($low_end); |
| 69 | } else { |
| 70 | // big endian |
| 71 | $big_end = unpack('N', $this->STREAM->read(4)); |
| 72 | return array_shift($big_end); |
| 73 | } |
| 74 | } |
| 134 | /** |
| 135 | * Loads the translation tables from the MO file into the cache |
| 136 | * If caching is enabled, also loads all strings into a cache |
| 137 | * to speed up translation lookups |
| 138 | * |
| 139 | * @access private |
| 140 | */ |
| 141 | function load_tables() { |
| 142 | if (is_array($this->cache_translations) && |
| 143 | is_array($this->table_originals) && |
| 144 | is_array($this->table_translations)) |
| 145 | return; |
| 146 | |
| 147 | /* get original and translations tables */ |
| 148 | $this->STREAM->seekto($this->originals); |
| 149 | $this->table_originals = $this->readintarray($this->total * 2); |
| 150 | $this->STREAM->seekto($this->translations); |
| 151 | $this->table_translations = $this->readintarray($this->total * 2); |
| 152 | |
| 153 | if ($this->enable_cache) { |
| 154 | $this->cache_translations = array (); |
| 155 | /* read all strings in the cache */ |
| 156 | for ($i = 0; $i < $this->total; $i++) { |
| 157 | $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); |
| 158 | $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); |
| 159 | $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); |
| 160 | $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); |
| 161 | $this->cache_translations[$original] = $translation; |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | /** |
| 167 | * Returns a string from the "originals" table |
| 168 | * |
| 169 | * @access private |
| 170 | * @param int num Offset number of original string |
| 171 | * @return string Requested string if found, otherwise '' |
| 172 | */ |
| 173 | function get_original_string($num) { |
| 174 | $length = $this->table_originals[$num * 2 + 1]; |
| 175 | $offset = $this->table_originals[$num * 2 + 2]; |
| 176 | if (! $length) |
| 177 | return ''; |
| 178 | $this->STREAM->seekto($offset); |
| 179 | $data = $this->STREAM->read($length); |
| 180 | return (string)$data; |
| 181 | } |
| 182 | |
| 183 | /** |
| 184 | * Returns a string from the "translations" table |
| 185 | * |
| 186 | * @access private |
| 187 | * @param int num Offset number of original string |
| 188 | * @return string Requested string if found, otherwise '' |
| 189 | */ |
| 190 | function get_translation_string($num) { |
| 191 | $length = $this->table_translations[$num * 2 + 1]; |
| 192 | $offset = $this->table_translations[$num * 2 + 2]; |
| 193 | if (! $length) |
| 194 | return ''; |
| 195 | $this->STREAM->seekto($offset); |
| 196 | $data = $this->STREAM->read($length); |
| 197 | return (string)$data; |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Binary search for string |
| 202 | * |
| 203 | * @access private |
| 204 | * @param string string |
| 205 | * @param int start (internally used in recursive function) |
| 206 | * @param int end (internally used in recursive function) |
| 207 | * @return int string number (offset in originals table) |
| 208 | */ |
| 209 | function find_string($string, $start = -1, $end = -1) { |
| 210 | if (($start == -1) or ($end == -1)) { |
| 211 | // find_string is called with only one parameter, set start end end |
| 212 | $start = 0; |
| 213 | $end = $this->total; |
| 214 | } |
| 215 | if (abs($start - $end) <= 1) { |
| 216 | // We're done, now we either found the string, or it doesn't exist |
| 217 | $txt = $this->get_original_string($start); |
| 218 | if ($string == $txt) |
| 219 | return $start; |
| 220 | else |
| 221 | return -1; |
| 222 | } else if ($start > $end) { |
| 223 | // start > end -> turn around and start over |
| 224 | return $this->find_string($string, $end, $start); |
| 225 | } else { |
| 226 | // Divide table in two parts |
| 227 | $half = (int)(($start + $end) / 2); |
| 228 | $cmp = strcmp($string, $this->get_original_string($half)); |
| 229 | if ($cmp == 0) |
| 230 | // string is exactly in the middle => return it |
| 231 | return $half; |
| 232 | else if ($cmp < 0) |
| 233 | // The string is in the upper half |
| 234 | return $this->find_string($string, $start, $half); |
| 235 | else |
| 236 | // The string is in the lower half |
| 237 | return $this->find_string($string, $half, $end); |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * Translates a string |
| 243 | * |
| 244 | * @access public |
| 245 | * @param string string to be translated |
| 246 | * @return string translated string (or original, if not found) |
| 247 | */ |
| 248 | function translate($string) { |
| 249 | if ($this->short_circuit) |
| 250 | return $string; |
| 251 | $this->load_tables(); |
| 252 | |
| 253 | if ($this->enable_cache) { |
| 254 | // Caching enabled, get translated string from cache |
| 255 | if (array_key_exists($string, $this->cache_translations)) |
| 256 | return $this->cache_translations[$string]; |
| 257 | else |
| 258 | return $string; |
| 259 | } else { |
| 260 | // Caching not enabled, try to find string |
| 261 | $num = $this->find_string($string); |
| 262 | if ($num == -1) |
| 263 | return $string; |
| 264 | else |
| 265 | return $this->get_translation_string($num); |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Get possible plural forms from MO header |
| 271 | * |
| 272 | * @access private |
| 273 | * @return string plural form header |
| 274 | */ |
| 275 | function get_plural_forms() { |
| 276 | // lets assume message number 0 is header |
| 277 | // this is true, right? |
| 278 | $this->load_tables(); |
| 279 | |
| 280 | // cache header field for plural forms |
| 281 | if (! is_string($this->pluralheader)) { |
| 282 | if ($this->enable_cache) { |
| 283 | $header = $this->cache_translations[""]; |
| 284 | } else { |
| 285 | $header = $this->get_translation_string(0); |
| 286 | } |
| 287 | $header .= "\n"; //make sure our regex matches |
| 288 | if (eregi("plural-forms: ([^\n]*)\n", $header, $regs)) |
| 289 | $expr = $regs[1]; |
| 290 | else |
| 291 | $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; |
| 292 | |
| 293 | // add parentheses |
| 294 | // important since PHP's ternary evaluates from left to right |
| 295 | $expr.= ';'; |
| 296 | $res= ''; |
| 297 | $p= 0; |
| 298 | for ($i= 0; $i < strlen($expr); $i++) { |
| 299 | $ch= $expr[$i]; |
| 300 | switch ($ch) { |
| 301 | case '?': |
| 302 | $res.= ' ? ('; |
| 303 | $p++; |
| 304 | break; |
| 305 | case ':': |
| 306 | $res.= ') : ('; |
| 307 | break; |
| 308 | case ';': |
| 309 | $res.= str_repeat( ')', $p) . ';'; |
| 310 | $p= 0; |
| 311 | break; |
| 312 | default: |
| 313 | $res.= $ch; |
| 314 | } |
| 315 | } |
| 316 | $this->pluralheader = $res; |
| 317 | } |
| 318 | |
| 319 | return $this->pluralheader; |
| 320 | } |
| 321 | |
| 322 | /** |
| 323 | * Detects which plural form to take |
| 324 | * |
| 325 | * @access private |
| 326 | * @param n count |
| 327 | * @return int array index of the right plural form |
| 328 | */ |
| 329 | function select_string($n) { |
| 330 | if (is_null($this->select_string_function)) { |
| 331 | $string = $this->get_plural_forms(); |
| 332 | if (preg_match("/nplurals\s*=\s*(\d+)\s*\;\s*plural\s*=\s*(.*?)\;+/", $string, $matches)) { |
| 333 | $nplurals = $matches[1]; |
| 334 | $expression = $matches[2]; |
| 335 | $expression = str_replace("n", '$n', $expression); |
| 336 | } else { |
| 337 | $nplurals = 2; |
| 338 | $expression = ' $n == 1 ? 0 : 1 '; |
| 339 | } |
| 340 | $func_body = " |
| 341 | \$plural = ($expression); |
| 342 | return (\$plural <= $nplurals)? \$plural : \$plural - 1;"; |
| 343 | $this->select_string_function = create_function('$n', $func_body); |
| 344 | } |
| 345 | return call_user_func($this->select_string_function, $n); |
| 346 | } |
| 347 | |
| 348 | /** |
| 349 | * Plural version of gettext |
| 350 | * |
| 351 | * @access public |
| 352 | * @param string single |
| 353 | * @param string plural |
| 354 | * @param string number |
| 355 | * @return translated plural form |
| 356 | */ |
| 357 | function ngettext($single, $plural, $number) { |
| 358 | if ($this->short_circuit) { |
| 359 | if ($number != 1) |
| 360 | return $plural; |
| 361 | else |
| 362 | return $single; |
| 363 | } |
| 364 | |
| 365 | // find out the appropriate form |
| 366 | $select = $this->select_string($number); |
| 367 | |
| 368 | // this should contains all strings separated by NULLs |
| 369 | $key = $single.chr(0).$plural; |
| 370 | |
| 371 | |
| 372 | if ($this->enable_cache) { |
| 373 | if (! array_key_exists($key, $this->cache_translations)) { |
| 374 | return ($number != 1) ? $plural : $single; |
| 375 | } else { |
| 376 | $result = $this->cache_translations[$key]; |
| 377 | $list = explode(chr(0), $result); |
| 378 | return $list[$select]; |
| 379 | } |
| 380 | } else { |
| 381 | $num = $this->find_string($key); |
| 382 | if ($num == -1) { |
| 383 | return ($number != 1) ? $plural : $single; |
| 384 | } else { |
| 385 | $result = $this->get_translation_string($num); |
| 386 | $list = explode(chr(0), $result); |
| 387 | return $list[$select]; |
| 388 | } |
| 389 | } |
| 390 | } |
| 391 | |