web/wp-includes/gettext.php
branchwordpress
changeset 109 03b0d1493584
equal deleted inserted replaced
-1:000000000000 109:03b0d1493584
       
     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 ?>