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