Fix gettext's plural forms for more than 2 plural forms. Props moeffju and nbachiyski. fixes #4005

git-svn-id: http://svn.automattic.com/wordpress/trunk@5266 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
rob1n 2007-04-13 23:29:47 +00:00
parent 24e1d66471
commit 62f5c944dc
1 changed files with 359 additions and 331 deletions

View File

@ -1,23 +1,23 @@
<?php <?php
/* /*
Copyright (c) 2003 Danilo Segan <danilo@kvota.net>. Copyright (c) 2003 Danilo Segan <danilo@kvota.net>.
Copyright (c) 2005 Nico Kaiser <nico@siriux.net> Copyright (c) 2005 Nico Kaiser <nico@siriux.net>
This file is part of PHP-gettext. This file is part of PHP-gettext.
PHP-gettext is free software; you can redistribute it and/or modify PHP-gettext is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or the Free Software Foundation; either version 2 of the License, or
(at your option) any later version. (at your option) any later version.
PHP-gettext is distributed in the hope that it will be useful, PHP-gettext is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with PHP-gettext; if not, write to the Free Software along with PHP-gettext; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/ */
@ -34,332 +34,360 @@
* that you don't want to keep in memory) * that you don't want to keep in memory)
*/ */
class gettext_reader { class gettext_reader {
//public: //public:
var $error = 0; // public variable that holds error code (0 if no error) var $error = 0; // public variable that holds error code (0 if no error)
//private: //private:
var $BYTEORDER = 0; // 0: low endian, 1: big endian var $BYTEORDER = 0; // 0: low endian, 1: big endian
var $STREAM = NULL; var $STREAM = NULL;
var $short_circuit = false; var $short_circuit = false;
var $enable_cache = false; var $enable_cache = false;
var $originals = NULL; // offset of original table var $originals = NULL; // offset of original table
var $translations = NULL; // offset of translation table var $translations = NULL; // offset of translation table
var $pluralheader = NULL; // cache header field for plural forms var $pluralheader = NULL; // cache header field for plural forms
var $total = 0; // total string count var $select_string_function = NULL; // cache function, which chooses plural forms
var $table_originals = NULL; // table for original strings (offsets) var $total = 0; // total string count
var $table_translations = NULL; // table for translated strings (offsets) var $table_originals = NULL; // table for original strings (offsets)
var $cache_translations = NULL; // original -> translation mapping var $table_translations = NULL; // table for translated strings (offsets)
var $cache_translations = NULL; // original -> translation mapping
/* Methods */ /* Methods */
/** /**
* Reads a 32bit Integer from the Stream * Reads a 32bit Integer from the Stream
* *
* @access private * @access private
* @return Integer from the Stream * @return Integer from the Stream
*/ */
function readint() { function readint() {
if ($this->BYTEORDER == 0) { if ($this->BYTEORDER == 0) {
// low endian // low endian
$low_end = unpack('V', $this->STREAM->read(4)); $low_end = unpack('V', $this->STREAM->read(4));
return array_shift($low_end); return array_shift($low_end);
} else { } else {
// big endian // big endian
$big_end = unpack('N', $this->STREAM->read(4)); $big_end = unpack('N', $this->STREAM->read(4));
return array_shift($big_end); return array_shift($big_end);
} }
} }
/** /**
* Reads an array of Integers from the Stream * Reads an array of Integers from the Stream
* *
* @param int count How many elements should be read * @param int count How many elements should be read
* @return Array of Integers * @return Array of Integers
*/ */
function readintarray($count) { function readintarray($count) {
if ($this->BYTEORDER == 0) { if ($this->BYTEORDER == 0) {
// low endian // low endian
return unpack('V'.$count, $this->STREAM->read(4 * $count)); return unpack('V'.$count, $this->STREAM->read(4 * $count));
} else { } else {
// big endian // big endian
return unpack('N'.$count, $this->STREAM->read(4 * $count)); return unpack('N'.$count, $this->STREAM->read(4 * $count));
} }
} }
/** /**
* Constructor * Constructor
* *
* @param object Reader the StreamReader object * @param object Reader the StreamReader object
* @param boolean enable_cache Enable or disable caching of strings (default on) * @param boolean enable_cache Enable or disable caching of strings (default on)
*/ */
function gettext_reader($Reader, $enable_cache = true) { function gettext_reader($Reader, $enable_cache = true) {
// If there isn't a StreamReader, turn on short circuit mode. // If there isn't a StreamReader, turn on short circuit mode.
if (! $Reader || isset($Reader->error) ) { if (! $Reader || isset($Reader->error) ) {
$this->short_circuit = true; $this->short_circuit = true;
return; return;
} }
// Caching can be turned off // Caching can be turned off
$this->enable_cache = $enable_cache; $this->enable_cache = $enable_cache;
// $MAGIC1 = (int)0x950412de; //bug in PHP 5.0.2, see https://savannah.nongnu.org/bugs/?func=detailitem&item_id=10565 // $MAGIC1 = (int)0x950412de; //bug in PHP 5.0.2, see https://savannah.nongnu.org/bugs/?func=detailitem&item_id=10565
$MAGIC1 = (int) - 1794895138; $MAGIC1 = (int) - 1794895138;
// $MAGIC2 = (int)0xde120495; //bug // $MAGIC2 = (int)0xde120495; //bug
$MAGIC2 = (int) - 569244523; $MAGIC2 = (int) - 569244523;
// 64-bit fix // 64-bit fix
$MAGIC3 = (int) 2500072158; $MAGIC3 = (int) 2500072158;
$this->STREAM = $Reader; $this->STREAM = $Reader;
$magic = $this->readint(); $magic = $this->readint();
if ($magic == ($MAGIC1 & 0xFFFFFFFF) || $magic == ($MAGIC3 & 0xFFFFFFFF)) { // to make sure it works for 64-bit platforms if ($magic == ($MAGIC1 & 0xFFFFFFFF) || $magic == ($MAGIC3 & 0xFFFFFFFF)) { // to make sure it works for 64-bit platforms
$this->BYTEORDER = 0; $this->BYTEORDER = 0;
} elseif ($magic == ($MAGIC2 & 0xFFFFFFFF)) { } elseif ($magic == ($MAGIC2 & 0xFFFFFFFF)) {
$this->BYTEORDER = 1; $this->BYTEORDER = 1;
} else { } else {
$this->error = 1; // not MO file $this->error = 1; // not MO file
return false; return false;
} }
// FIXME: Do we care about revision? We should. // FIXME: Do we care about revision? We should.
$revision = $this->readint(); $revision = $this->readint();
$this->total = $this->readint(); $this->total = $this->readint();
$this->originals = $this->readint(); $this->originals = $this->readint();
$this->translations = $this->readint(); $this->translations = $this->readint();
} }
/** /**
* Loads the translation tables from the MO file into the cache * Loads the translation tables from the MO file into the cache
* If caching is enabled, also loads all strings into a cache * If caching is enabled, also loads all strings into a cache
* to speed up translation lookups * to speed up translation lookups
* *
* @access private * @access private
*/ */
function load_tables() { function load_tables() {
if (is_array($this->cache_translations) && if (is_array($this->cache_translations) &&
is_array($this->table_originals) && is_array($this->table_originals) &&
is_array($this->table_translations)) is_array($this->table_translations))
return; return;
/* get original and translations tables */ /* get original and translations tables */
$this->STREAM->seekto($this->originals); $this->STREAM->seekto($this->originals);
$this->table_originals = $this->readintarray($this->total * 2); $this->table_originals = $this->readintarray($this->total * 2);
$this->STREAM->seekto($this->translations); $this->STREAM->seekto($this->translations);
$this->table_translations = $this->readintarray($this->total * 2); $this->table_translations = $this->readintarray($this->total * 2);
if ($this->enable_cache) { if ($this->enable_cache) {
$this->cache_translations = array (); $this->cache_translations = array ();
/* read all strings in the cache */ /* read all strings in the cache */
for ($i = 0; $i < $this->total; $i++) { for ($i = 0; $i < $this->total; $i++) {
$this->STREAM->seekto($this->table_originals[$i * 2 + 2]); $this->STREAM->seekto($this->table_originals[$i * 2 + 2]);
$original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]);
$this->STREAM->seekto($this->table_translations[$i * 2 + 2]); $this->STREAM->seekto($this->table_translations[$i * 2 + 2]);
$translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]);
$this->cache_translations[$original] = $translation; $this->cache_translations[$original] = $translation;
} }
} }
} }
/** /**
* Returns a string from the "originals" table * Returns a string from the "originals" table
* *
* @access private * @access private
* @param int num Offset number of original string * @param int num Offset number of original string
* @return string Requested string if found, otherwise '' * @return string Requested string if found, otherwise ''
*/ */
function get_original_string($num) { function get_original_string($num) {
$length = $this->table_originals[$num * 2 + 1]; $length = $this->table_originals[$num * 2 + 1];
$offset = $this->table_originals[$num * 2 + 2]; $offset = $this->table_originals[$num * 2 + 2];
if (! $length) if (! $length)
return ''; return '';
$this->STREAM->seekto($offset); $this->STREAM->seekto($offset);
$data = $this->STREAM->read($length); $data = $this->STREAM->read($length);
return (string)$data; return (string)$data;
} }
/** /**
* Returns a string from the "translations" table * Returns a string from the "translations" table
* *
* @access private * @access private
* @param int num Offset number of original string * @param int num Offset number of original string
* @return string Requested string if found, otherwise '' * @return string Requested string if found, otherwise ''
*/ */
function get_translation_string($num) { function get_translation_string($num) {
$length = $this->table_translations[$num * 2 + 1]; $length = $this->table_translations[$num * 2 + 1];
$offset = $this->table_translations[$num * 2 + 2]; $offset = $this->table_translations[$num * 2 + 2];
if (! $length) if (! $length)
return ''; return '';
$this->STREAM->seekto($offset); $this->STREAM->seekto($offset);
$data = $this->STREAM->read($length); $data = $this->STREAM->read($length);
return (string)$data; return (string)$data;
} }
/** /**
* Binary search for string * Binary search for string
* *
* @access private * @access private
* @param string string * @param string string
* @param int start (internally used in recursive function) * @param int start (internally used in recursive function)
* @param int end (internally used in recursive function) * @param int end (internally used in recursive function)
* @return int string number (offset in originals table) * @return int string number (offset in originals table)
*/ */
function find_string($string, $start = -1, $end = -1) { function find_string($string, $start = -1, $end = -1) {
if (($start == -1) or ($end == -1)) { if (($start == -1) or ($end == -1)) {
// find_string is called with only one parameter, set start end end // find_string is called with only one parameter, set start end end
$start = 0; $start = 0;
$end = $this->total; $end = $this->total;
} }
if (abs($start - $end) <= 1) { if (abs($start - $end) <= 1) {
// We're done, now we either found the string, or it doesn't exist // We're done, now we either found the string, or it doesn't exist
$txt = $this->get_original_string($start); $txt = $this->get_original_string($start);
if ($string == $txt) if ($string == $txt)
return $start; return $start;
else else
return -1; return -1;
} else if ($start > $end) { } else if ($start > $end) {
// start > end -> turn around and start over // start > end -> turn around and start over
return $this->find_string($string, $end, $start); return $this->find_string($string, $end, $start);
} else { } else {
// Divide table in two parts // Divide table in two parts
$half = (int)(($start + $end) / 2); $half = (int)(($start + $end) / 2);
$cmp = strcmp($string, $this->get_original_string($half)); $cmp = strcmp($string, $this->get_original_string($half));
if ($cmp == 0) if ($cmp == 0)
// string is exactly in the middle => return it // string is exactly in the middle => return it
return $half; return $half;
else if ($cmp < 0) else if ($cmp < 0)
// The string is in the upper half // The string is in the upper half
return $this->find_string($string, $start, $half); return $this->find_string($string, $start, $half);
else else
// The string is in the lower half // The string is in the lower half
return $this->find_string($string, $half, $end); return $this->find_string($string, $half, $end);
} }
} }
/** /**
* Translates a string * Translates a string
* *
* @access public * @access public
* @param string string to be translated * @param string string to be translated
* @return string translated string (or original, if not found) * @return string translated string (or original, if not found)
*/ */
function translate($string) { function translate($string) {
if ($this->short_circuit) if ($this->short_circuit)
return $string; return $string;
$this->load_tables(); $this->load_tables();
if ($this->enable_cache) { if ($this->enable_cache) {
// Caching enabled, get translated string from cache // Caching enabled, get translated string from cache
if (array_key_exists($string, $this->cache_translations)) if (array_key_exists($string, $this->cache_translations))
return $this->cache_translations[$string]; return $this->cache_translations[$string];
else else
return $string; return $string;
} else { } else {
// Caching not enabled, try to find string // Caching not enabled, try to find string
$num = $this->find_string($string); $num = $this->find_string($string);
if ($num == -1) if ($num == -1)
return $string; return $string;
else else
return $this->get_translation_string($num); return $this->get_translation_string($num);
} }
} }
/** /**
* Get possible plural forms from MO header * Get possible plural forms from MO header
* *
* @access private * @access private
* @return string plural form header * @return string plural form header
*/ */
function get_plural_forms() { function get_plural_forms() {
// lets assume message number 0 is header // lets assume message number 0 is header
// this is true, right? // this is true, right?
$this->load_tables(); $this->load_tables();
// cache header field for plural forms // cache header field for plural forms
if (! is_string($this->pluralheader)) { if (! is_string($this->pluralheader)) {
if ($this->enable_cache) { if ($this->enable_cache) {
$header = $this->cache_translations[""]; $header = $this->cache_translations[""];
} else { } else {
$header = $this->get_translation_string(0); $header = $this->get_translation_string(0);
} }
if (eregi("plural-forms: ([^\n]*)\n", $header, $regs)) $header .= "\n"; //make sure our regex matches
$expr = $regs[1]; if (eregi("plural-forms: ([^\n]*)\n", $header, $regs))
else $expr = $regs[1];
$expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; else
$this->pluralheader = $expr; $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
}
return $this->pluralheader;
}
/** // add parentheses
* Detects which plural form to take // important since PHP's ternary evaluates from left to right
* $expr.= ';';
* @access private $res= '';
* @param n count $p= 0;
* @return int array index of the right plural form for ($i= 0; $i < strlen($expr); $i++) {
*/ $ch= $expr[$i];
function select_string($n) { switch ($ch) {
$string = $this->get_plural_forms(); case '?':
$string = str_replace('nplurals',"\$total",$string); $res.= ' ? (';
$string = str_replace("n",$n,$string); $p++;
$string = str_replace('plural',"\$plural",$string); break;
case ':':
$res.= ') : (';
break;
case ';':
$res.= str_repeat( ')', $p) . ';';
$p= 0;
break;
default:
$res.= $ch;
}
}
$this->pluralheader = $res;
}
# poEdit doesn't put any semicolons, which return $this->pluralheader;
# results in parse error in eval }
$string .= ';';
$total = 0; /**
$plural = 0; * Detects which plural form to take
*
* @access private
* @param n count
* @return int array index of the right plural form
*/
function select_string($n) {
if (is_null($this->select_string_function)) {
$string = $this->get_plural_forms();
if (preg_match("/nplurals\s*=\s*(\d+)\s*\;\s*plural\s*=\s*(.*?)\;+/", $string, $matches)) {
$nplurals = $matches[1];
$expression = $matches[2];
$expression = str_replace("n", '$n', $expression);
} else {
$nplurals = 2;
$expression = ' $n == 1 ? 0 : 1 ';
}
$func_body = "
\$plural = ($expression);
return (\$plural <= $nplurals)? \$plural : \$plural - 1;";
$this->select_string_function = create_function('$n', $func_body);
}
return call_user_func($this->select_string_function, $n);
}
eval("$string"); /**
if ($plural >= $total) $plural = $total - 1; * Plural version of gettext
return $plural; *
} * @access public
* @param string single
* @param string plural
* @param string number
* @return translated plural form
*/
function ngettext($single, $plural, $number) {
if ($this->short_circuit) {
if ($number != 1)
return $plural;
else
return $single;
}
/** // find out the appropriate form
* Plural version of gettext $select = $this->select_string($number);
*
* @access public
* @param string single
* @param string plural
* @param string number
* @return translated plural form
*/
function ngettext($single, $plural, $number) {
if ($this->short_circuit) {
if ($number != 1)
return $plural;
else
return $single;
}
// find out the appropriate form // this should contains all strings separated by NULLs
$select = $this->select_string($number); $key = $single.chr(0).$plural;
// this should contains all strings separated by NULLs
$key = $single.chr(0).$plural;
if ($this->enable_cache) { if ($this->enable_cache) {
if (! array_key_exists($key, $this->cache_translations)) { if (! array_key_exists($key, $this->cache_translations)) {
return ($number != 1) ? $plural : $single; return ($number != 1) ? $plural : $single;
} else { } else {
$result = $this->cache_translations[$key]; $result = $this->cache_translations[$key];
$list = explode(chr(0), $result); $list = explode(chr(0), $result);
return $list[$select]; return $list[$select];
} }
} else { } else {
$num = $this->find_string($key); $num = $this->find_string($key);
if ($num == -1) { if ($num == -1) {
return ($number != 1) ? $plural : $single; return ($number != 1) ? $plural : $single;
} else { } else {
$result = $this->get_translation_string($num); $result = $this->get_translation_string($num);
$list = explode(chr(0), $result); $list = explode(chr(0), $result);
return $list[$select]; return $list[$select];
} }
} }
} }
} }