Changeset 5266
- Timestamp:
- 04/13/2007 11:29:47 PM (18 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/wp-includes/gettext.php
r4953 r5266 1 1 <?php 2 2 /* 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 21 22 22 */ 23 23 24 24 /** 25 25 * Provides a simple gettext replacement that works independently from … … 27 27 * It can read MO files and use them for translating strings. 28 28 * The files are passed to gettext_reader as a Stream (see streams.php) 29 * 29 * 30 30 * This version has the ability to cache all strings and translations to 31 31 * speed up the string lookup. … … 35 35 */ 36 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 $low_end = unpack('V', $this->STREAM->read(4)); 67 return array_shift($low_end); 68 } else { 69 // big endian 70 $big_end = unpack('N', $this->STREAM->read(4)); 71 return array_shift($big_end); 72 } 73 } 74 75 /** 76 * Reads an array of Integers from the Stream 77 * 78 * @param int count How many elements should be read 79 * @return Array of Integers 80 */ 81 function readintarray($count) { 82 if ($this->BYTEORDER == 0) { 83 // low endian 84 return unpack('V'.$count, $this->STREAM->read(4 * $count)); 85 } else { 86 // big endian 87 return unpack('N'.$count, $this->STREAM->read(4 * $count)); 88 } 89 } 90 91 /** 92 * Constructor 93 * 94 * @param object Reader the StreamReader object 95 * @param boolean enable_cache Enable or disable caching of strings (default on) 96 */ 97 function gettext_reader($Reader, $enable_cache = true) { 98 // If there isn't a StreamReader, turn on short circuit mode. 99 if (! $Reader || isset($Reader->error) ) { 100 $this->short_circuit = true; 101 return; 102 } 103 104 // Caching can be turned off 105 $this->enable_cache = $enable_cache; 106 107 // $MAGIC1 = (int)0x950412de; //bug in PHP 5.0.2, see https://savannah.nongnu.org/bugs/?func=detailitem&item_id=10565 108 $MAGIC1 = (int) - 1794895138; 109 // $MAGIC2 = (int)0xde120495; //bug 110 $MAGIC2 = (int) - 569244523; 111 // 64-bit fix 112 $MAGIC3 = (int) 2500072158; 113 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 } 267 268 /** 269 * Get possible plural forms from MO header 270 * 271 * @access private 272 * @return string plural form header 273 */ 274 function get_plural_forms() { 275 // lets assume message number 0 is header 276 // this is true, right? 277 $this->load_tables(); 278 279 // cache header field for plural forms 280 if (! is_string($this->pluralheader)) { 281 if ($this->enable_cache) { 282 $header = $this->cache_translations[""]; 283 } else { 284 $header = $this->get_translation_string(0); 285 } 286 if (eregi("plural-forms: ([^\n]*)\n", $header, $regs)) 287 $expr = $regs[1]; 288 else 289 $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; 290 $this->pluralheader = $expr; 291 } 292 return $this->pluralheader; 293 } 294 295 /** 296 * Detects which plural form to take 297 * 298 * @access private 299 * @param n count 300 * @return int array index of the right plural form 301 */ 302 function select_string($n) { 303 $string = $this->get_plural_forms(); 304 $string = str_replace('nplurals',"\$total",$string); 305 $string = str_replace("n",$n,$string); 306 $string = str_replace('plural',"\$plural",$string); 307 308 # poEdit doesn't put any semicolons, which 309 # results in parse error in eval 310 $string .= ';'; 311 312 $total = 0; 313 $plural = 0; 314 315 eval("$string"); 316 if ($plural >= $total) $plural = $total - 1; 317 return $plural; 318 } 319 320 /** 321 * Plural version of gettext 322 * 323 * @access public 324 * @param string single 325 * @param string plural 326 * @param string number 327 * @return translated plural form 328 */ 329 function ngettext($single, $plural, $number) { 330 if ($this->short_circuit) { 331 if ($number != 1) 332 return $plural; 333 else 334 return $single; 335 } 336 337 // find out the appropriate form 338 $select = $this->select_string($number); 339 340 // this should contains all strings separated by NULLs 341 $key = $single.chr(0).$plural; 342 343 344 if ($this->enable_cache) { 345 if (! array_key_exists($key, $this->cache_translations)) { 346 return ($number != 1) ? $plural : $single; 347 } else { 348 $result = $this->cache_translations[$key]; 349 $list = explode(chr(0), $result); 350 return $list[$select]; 351 } 352 } else { 353 $num = $this->find_string($key); 354 if ($num == -1) { 355 return ($number != 1) ? $plural : $single; 356 } else { 357 $result = $this->get_translation_string($num); 358 $list = explode(chr(0), $result); 359 return $list[$select]; 360 } 361 } 362 } 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 $select_string_function = NULL; // cache function, which chooses plural forms 49 var $total = 0; // total string count 50 var $table_originals = NULL; // table for original strings (offsets) 51 var $table_translations = NULL; // table for translated strings (offsets) 52 var $cache_translations = NULL; // original -> translation mapping 53 54 55 /* Methods */ 56 57 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 } 75 76 /** 77 * Reads an array of Integers from the Stream 78 * 79 * @param int count How many elements should be read 80 * @return Array of Integers 81 */ 82 function readintarray($count) { 83 if ($this->BYTEORDER == 0) { 84 // low endian 85 return unpack('V'.$count, $this->STREAM->read(4 * $count)); 86 } else { 87 // big endian 88 return unpack('N'.$count, $this->STREAM->read(4 * $count)); 89 } 90 } 91 92 /** 93 * Constructor 94 * 95 * @param object Reader the StreamReader object 96 * @param boolean enable_cache Enable or disable caching of strings (default on) 97 */ 98 function gettext_reader($Reader, $enable_cache = true) { 99 // If there isn't a StreamReader, turn on short circuit mode. 100 if (! $Reader || isset($Reader->error) ) { 101 $this->short_circuit = true; 102 return; 103 } 104 105 // Caching can be turned off 106 $this->enable_cache = $enable_cache; 107 108 // $MAGIC1 = (int)0x950412de; //bug in PHP 5.0.2, see https://savannah.nongnu.org/bugs/?func=detailitem&item_id=10565 109 $MAGIC1 = (int) - 1794895138; 110 // $MAGIC2 = (int)0xde120495; //bug 111 $MAGIC2 = (int) - 569244523; 112 // 64-bit fix 113 $MAGIC3 = (int) 2500072158; 114 115 $this->STREAM = $Reader; 116 $magic = $this->readint(); 117 if ($magic == ($MAGIC1 & 0xFFFFFFFF) || $magic == ($MAGIC3 & 0xFFFFFFFF)) { // to make sure it works for 64-bit platforms 118 $this->BYTEORDER = 0; 119 } elseif ($magic == ($MAGIC2 & 0xFFFFFFFF)) { 120 $this->BYTEORDER = 1; 121 } else { 122 $this->error = 1; // not MO file 123 return false; 124 } 125 126 // FIXME: Do we care about revision? We should. 127 $revision = $this->readint(); 128 129 $this->total = $this->readint(); 130 $this->originals = $this->readint(); 131 $this->translations = $this->readint(); 132 } 133 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 } 363 391 364 392 }
Note: See TracChangeset
for help on using the changeset viewer.