You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
393 lines
12 KiB
393 lines
12 KiB
<?php |
|
/* |
|
Copyright (c) 2003 Danilo Segan <danilo@kvota.net>. |
|
Copyright (c) 2005 Nico Kaiser <nico@siriux.net> |
|
|
|
This file is part of PHP-gettext. |
|
|
|
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 |
|
the Free Software Foundation; either version 2 of the License, or |
|
(at your option) any later version. |
|
|
|
PHP-gettext is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with PHP-gettext; if not, write to the Free Software |
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|
|
|
*/ |
|
|
|
/** |
|
* Provides a simple gettext replacement that works independently from |
|
* the system's gettext abilities. |
|
* It can read MO files and use them for translating strings. |
|
* The files are passed to gettext_reader as a Stream (see streams.php) |
|
* |
|
* This version has the ability to cache all strings and translations to |
|
* speed up the string lookup. |
|
* While the cache is enabled by default, it can be switched off with the |
|
* second parameter in the constructor (e.g. whenusing very large MO files |
|
* that you don't want to keep in memory) |
|
*/ |
|
|
|
//reload by 70 (typecho group) |
|
/** |
|
* This file is part of PHP-gettext |
|
* |
|
* @author Danilo Segan <danilo@kvota.net>, Nico Kaiser <nico@siriux.net> |
|
* @category typecho |
|
* @package I18n |
|
*/ |
|
class Typecho_I18n_GetText |
|
{ |
|
//public: |
|
public $error = 0; // public variable that holds error code (0 if no error) |
|
|
|
//private: |
|
private $BYTEORDER = 0; // 0: low endian, 1: big endian |
|
private $STREAM = NULL; |
|
private $short_circuit = false; |
|
private $enable_cache = false; |
|
private $originals = NULL; // offset of original table |
|
private $translations = NULL; // offset of translation table |
|
private $pluralheader = NULL; // cache header field for plural forms |
|
private $total = 0; // total string count |
|
private $table_originals = NULL; // table for original strings (offsets) |
|
private $table_translations = NULL; // table for translated strings (offsets) |
|
private $cache_translations = NULL; // original -> translation mapping |
|
|
|
|
|
/* Methods */ |
|
/** |
|
* Constructor |
|
* |
|
* @param string $file file name |
|
* @param boolean enable_cache Enable or disable caching of strings (default on) |
|
*/ |
|
public function __construct($file, $enable_cache = true) |
|
{ |
|
// If there isn't a StreamReader, turn on short circuit mode. |
|
if (!file_exists($file)) { |
|
$this->short_circuit = true; |
|
return; |
|
} |
|
|
|
// Caching can be turned off |
|
$this->enable_cache = $enable_cache; |
|
$this->STREAM = @fopen($file, 'rb'); |
|
|
|
$unpacked = unpack('c', $this->read(4)); |
|
$magic = array_shift($unpacked); |
|
|
|
if (-34 == $magic) { |
|
$this->BYTEORDER = 0; |
|
} elseif (-107 == $magic) { |
|
$this->BYTEORDER = 1; |
|
} else { |
|
$this->error = 1; // not MO file |
|
return false; |
|
} |
|
|
|
// FIXME: Do we care about revision? We should. |
|
$revision = $this->readint(); |
|
|
|
$this->total = $this->readint(); |
|
$this->originals = $this->readint(); |
|
$this->translations = $this->readint(); |
|
} |
|
|
|
/** |
|
* read |
|
* |
|
* @param mixed $count |
|
* @access private |
|
* @return void |
|
*/ |
|
private function read($count) |
|
{ |
|
$count = abs($count); |
|
|
|
if ($count > 0) { |
|
return fread($this->STREAM, $count); |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
/** |
|
* Reads a 32bit Integer from the Stream |
|
* |
|
* @access private |
|
* @return Integer from the Stream |
|
*/ |
|
private function readint() |
|
{ |
|
$end = unpack($this->BYTEORDER == 0 ? 'V' : 'N', $this->read(4)); |
|
return array_shift($end); |
|
} |
|
|
|
/** |
|
* Reads an array of Integers from the Stream |
|
* |
|
* @param int count How many elements should be read |
|
* @return Array of Integers |
|
*/ |
|
private function readintarray($count) |
|
{ |
|
return unpack(($this->BYTEORDER == 0 ? 'V' : 'N') . $count, $this->read(4 * $count)); |
|
} |
|
|
|
/** |
|
* Loads the translation tables from the MO file into the cache |
|
* If caching is enabled, also loads all strings into a cache |
|
* to speed up translation lookups |
|
* |
|
* @access private |
|
*/ |
|
private function load_tables() |
|
{ |
|
if (is_array($this->cache_translations) && |
|
is_array($this->table_originals) && |
|
is_array($this->table_translations)) |
|
return; |
|
|
|
/* get original and translations tables */ |
|
fseek($this->STREAM, $this->originals); |
|
$this->table_originals = $this->readintarray($this->total * 2); |
|
fseek($this->STREAM, $this->translations); |
|
$this->table_translations = $this->readintarray($this->total * 2); |
|
|
|
if ($this->enable_cache) { |
|
$this->cache_translations = array ('' => NULL); |
|
/* read all strings in the cache */ |
|
for ($i = 0; $i < $this->total; $i++) { |
|
if ($this->table_originals[$i * 2 + 1] > 0) { |
|
fseek($this->STREAM, $this->table_originals[$i * 2 + 2]); |
|
$original = fread($this->STREAM, $this->table_originals[$i * 2 + 1]); |
|
fseek($this->STREAM, $this->table_translations[$i * 2 + 2]); |
|
$translation = fread($this->STREAM, $this->table_translations[$i * 2 + 1]); |
|
$this->cache_translations[$original] = $translation; |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Returns a string from the "originals" table |
|
* |
|
* @access private |
|
* @param int num Offset number of original string |
|
* @return string Requested string if found, otherwise '' |
|
*/ |
|
private function get_original_string($num) |
|
{ |
|
$length = $this->table_originals[$num * 2 + 1]; |
|
$offset = $this->table_originals[$num * 2 + 2]; |
|
if (! $length) |
|
return ''; |
|
fseek($this->STREAM, $offset); |
|
$data = fread($this->STREAM, $length); |
|
return (string)$data; |
|
} |
|
|
|
/** |
|
* Returns a string from the "translations" table |
|
* |
|
* @access private |
|
* @param int num Offset number of original string |
|
* @return string Requested string if found, otherwise '' |
|
*/ |
|
private function get_translation_string($num) |
|
{ |
|
$length = $this->table_translations[$num * 2 + 1]; |
|
$offset = $this->table_translations[$num * 2 + 2]; |
|
if (! $length) |
|
return ''; |
|
fseek($this->STREAM, $offset); |
|
$data = fread($this->STREAM, $length); |
|
return (string)$data; |
|
} |
|
|
|
/** |
|
* Binary search for string |
|
* |
|
* @access private |
|
* @param string string |
|
* @param int start (internally used in recursive function) |
|
* @param int end (internally used in recursive function) |
|
* @return int string number (offset in originals table) |
|
*/ |
|
private function find_string($string, $start = -1, $end = -1) |
|
{ |
|
if (($start == -1) or ($end == -1)) { |
|
// find_string is called with only one parameter, set start end end |
|
$start = 0; |
|
$end = $this->total; |
|
} |
|
if (abs($start - $end) <= 1) { |
|
// We're done, now we either found the string, or it doesn't exist |
|
$txt = $this->get_original_string($start); |
|
if ($string == $txt) |
|
return $start; |
|
else |
|
return -1; |
|
} else if ($start > $end) { |
|
// start > end -> turn around and start over |
|
return $this->find_string($string, $end, $start); |
|
} else { |
|
// Divide table in two parts |
|
$half = (int)(($start + $end) / 2); |
|
$cmp = strcmp($string, $this->get_original_string($half)); |
|
if ($cmp == 0) |
|
// string is exactly in the middle => return it |
|
return $half; |
|
else if ($cmp < 0) |
|
// The string is in the upper half |
|
return $this->find_string($string, $start, $half); |
|
else |
|
// The string is in the lower half |
|
return $this->find_string($string, $half, $end); |
|
} |
|
} |
|
|
|
/** |
|
* Translates a string |
|
* |
|
* @access public |
|
* @param string string to be translated |
|
* @param integer $num found string number |
|
* @return string translated string (or original, if not found) |
|
*/ |
|
public function translate($string, &$num) |
|
{ |
|
if ($this->short_circuit) |
|
return $string; |
|
$this->load_tables(); |
|
|
|
if ($this->enable_cache) { |
|
// Caching enabled, get translated string from cache |
|
if (array_key_exists($string, $this->cache_translations)) |
|
return $this->cache_translations[$string]; |
|
else |
|
return $string; |
|
} else { |
|
// Caching not enabled, try to find string |
|
$num = $this->find_string($string); |
|
if ($num == -1) |
|
return $string; |
|
else |
|
return $this->get_translation_string($num); |
|
} |
|
} |
|
|
|
/** |
|
* Get possible plural forms from MO header |
|
* |
|
* @access private |
|
* @return string plural form header |
|
*/ |
|
private function get_plural_forms() |
|
{ |
|
// lets assume message number 0 is header |
|
// this is true, right? |
|
$this->load_tables(); |
|
|
|
// cache header field for plural forms |
|
if (! is_string($this->pluralheader)) { |
|
if ($this->enable_cache) { |
|
$header = $this->cache_translations[""]; |
|
} else { |
|
$header = $this->get_translation_string(0); |
|
} |
|
if (preg_match("/plural\-forms: ([^\n]*)\n/i", $header, $regs)) |
|
$expr = $regs[1]; |
|
else |
|
$expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; |
|
$this->pluralheader = $expr; |
|
} |
|
return $this->pluralheader; |
|
} |
|
|
|
/** |
|
* Detects which plural form to take |
|
* |
|
* @access private |
|
* @param n count |
|
* @return int array index of the right plural form |
|
*/ |
|
private function select_string($n) |
|
{ |
|
$string = $this->get_plural_forms(); |
|
$string = str_replace('nplurals',"\$total",$string); |
|
$string = str_replace("n",$n,$string); |
|
$string = str_replace('plural',"\$plural",$string); |
|
|
|
$total = 0; |
|
$plural = 0; |
|
|
|
eval("$string"); |
|
if ($plural >= $total) $plural = $total - 1; |
|
return $plural; |
|
} |
|
|
|
/** |
|
* Plural version of gettext |
|
* |
|
* @access public |
|
* @param string single |
|
* @param string plural |
|
* @param string number |
|
* @param integer $num found string number |
|
* @return translated plural form |
|
*/ |
|
public function ngettext($single, $plural, $number, &$num) |
|
{ |
|
if ($this->short_circuit) { |
|
if ($number != 1) |
|
return $plural; |
|
else |
|
return $single; |
|
} |
|
|
|
// find out the appropriate form |
|
$select = $this->select_string($number); |
|
|
|
// this should contains all strings separated by NULLs |
|
$key = $single.chr(0).$plural; |
|
|
|
|
|
if ($this->enable_cache) { |
|
if (! array_key_exists($key, $this->cache_translations)) { |
|
return ($number != 1) ? $plural : $single; |
|
} else { |
|
$result = $this->cache_translations[$key]; |
|
$list = explode(chr(0), $result); |
|
return $list[$select]; |
|
} |
|
} else { |
|
$num = $this->find_string($key); |
|
if ($num == -1) { |
|
return ($number != 1) ? $plural : $single; |
|
} else { |
|
$result = $this->get_translation_string($num); |
|
$list = explode(chr(0), $result); |
|
return $list[$select]; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* 关闭文件句柄 |
|
* |
|
* @access public |
|
* @return void |
|
*/ |
|
public function __destruct() |
|
{ |
|
fclose($this->STREAM); |
|
} |
|
}
|
|
|