Wordpress 2.0.2
[autoinstalls/wordpress.git] / wp-includes / gettext.php
1 <?php
2 /*
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
22 */
23  
24 /**
25  * Provides a simple gettext replacement that works independently from
26  * the system's gettext abilities.
27  * It can read MO files and use them for translating strings.
28  * The files are passed to gettext_reader as a Stream (see streams.php)
29  * 
30  * This version has the ability to cache all strings and translations to
31  * speed up the string lookup.
32  * While the cache is enabled by default, it can be switched off with the
33  * second parameter in the constructor (e.g. whenusing very large MO files
34  * that you don't want to keep in memory)
35  */
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       $stream = $this->STREAM->read(4);
65       if ($this->BYTEORDER == 0) {
66         // low endian
67         $unpacked = unpack('V',$stream);
68         return array_shift($unpacked);
69       } else {
70         // big endian
71         $unpacked = unpack('N',$stream);
72         return array_shift($unpacked);
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) {
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
109     $MAGIC1 = (int) - 1794895138;
110     // $MAGIC2 = (int)0xde120495; //bug
111     $MAGIC2 = (int) - 569244523;
112
113     $this->STREAM = $Reader;
114     $magic = $this->readint();
115     if ($magic == $MAGIC1) {
116       $this->BYTEORDER = 0;
117     } elseif ($magic == $MAGIC2) {
118       $this->BYTEORDER = 1;
119     } else {
120       $this->error = 1; // not MO file
121       return false;
122     }
123     
124     // FIXME: Do we care about revision? We should.
125     $revision = $this->readint();
126     
127     $this->total = $this->readint();
128     $this->originals = $this->readint();
129     $this->translations = $this->readint();
130   }
131   
132   /**
133    * Loads the translation tables from the MO file into the cache
134    * If caching is enabled, also loads all strings into a cache
135    * to speed up translation lookups
136    * 
137    * @access private
138    */
139   function load_tables() {
140     if (is_array($this->cache_translations) &&
141       is_array($this->table_originals) &&
142       is_array($this->table_translations))
143       return;
144     
145     /* get original and translations tables */
146     $this->STREAM->seekto($this->originals);
147     $this->table_originals = $this->readintarray($this->total * 2);
148     $this->STREAM->seekto($this->translations);
149     $this->table_translations = $this->readintarray($this->total * 2);
150     
151     if ($this->enable_cache) {
152       $this->cache_translations = array ();
153       /* read all strings in the cache */
154       for ($i = 0; $i < $this->total; $i++) {
155         $this->STREAM->seekto($this->table_originals[$i * 2 + 2]);
156         $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]);
157         $this->STREAM->seekto($this->table_translations[$i * 2 + 2]);
158         $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]);
159         $this->cache_translations[$original] = $translation;
160       }
161     }
162   }
163   
164   /**
165    * Returns a string from the "originals" table
166    * 
167    * @access private
168    * @param int num Offset number of original string
169    * @return string Requested string if found, otherwise ''
170    */
171   function get_original_string($num) {
172     $length = $this->table_originals[$num * 2 + 1];
173     $offset = $this->table_originals[$num * 2 + 2];
174     if (! $length)
175       return '';
176     $this->STREAM->seekto($offset);
177     $data = $this->STREAM->read($length);
178     return (string)$data;
179   }
180   
181   /**
182    * Returns a string from the "translations" table
183    * 
184    * @access private
185    * @param int num Offset number of original string
186    * @return string Requested string if found, otherwise ''
187    */
188   function get_translation_string($num) {
189     $length = $this->table_translations[$num * 2 + 1];
190     $offset = $this->table_translations[$num * 2 + 2];
191     if (! $length)
192       return '';
193     $this->STREAM->seekto($offset);
194     $data = $this->STREAM->read($length);
195     return (string)$data;
196   }
197   
198   /**
199    * Binary search for string
200    * 
201    * @access private
202    * @param string string
203    * @param int start (internally used in recursive function)
204    * @param int end (internally used in recursive function)
205    * @return int string number (offset in originals table)
206    */
207   function find_string($string, $start = -1, $end = -1) {
208     if (($start == -1) or ($end == -1)) {
209       // find_string is called with only one parameter, set start end end
210       $start = 0;
211       $end = $this->total;
212     }
213     if (abs($start - $end) <= 1) {
214       // We're done, now we either found the string, or it doesn't exist
215       $txt = $this->get_original_string($start);
216       if ($string == $txt)
217         return $start;
218       else
219         return -1;
220     } else if ($start > $end) {
221       // start > end -> turn around and start over
222       return $this->find_string($string, $end, $start);
223     } else {
224       // Divide table in two parts
225       $half = (int)(($start + $end) / 2);
226       $cmp = strcmp($string, $this->get_original_string($half));
227       if ($cmp == 0)
228         // string is exactly in the middle => return it
229         return $half;
230       else if ($cmp < 0)
231         // The string is in the upper half
232         return $this->find_string($string, $start, $half);
233       else
234         // The string is in the lower half
235         return $this->find_string($string, $half, $end);
236     }
237   }
238   
239   /**
240    * Translates a string
241    * 
242    * @access public
243    * @param string string to be translated
244    * @return string translated string (or original, if not found)
245    */
246   function translate($string) {
247     if ($this->short_circuit)
248       return $string;
249     $this->load_tables();     
250     
251     if ($this->enable_cache) {
252       // Caching enabled, get translated string from cache
253       if (array_key_exists($string, $this->cache_translations))
254         return $this->cache_translations[$string];
255       else
256         return $string;
257     } else {
258       // Caching not enabled, try to find string
259       $num = $this->find_string($string);
260       if ($num == -1)
261         return $string;
262       else
263         return $this->get_translation_string($num);
264     }
265   }
266
267   /**
268    * Get possible plural forms from MO header
269    * 
270    * @access private
271    * @return string plural form header
272    */
273   function get_plural_forms() {
274     // lets assume message number 0 is header  
275     // this is true, right?
276     $this->load_tables();
277     
278     // cache header field for plural forms
279     if (! is_string($this->pluralheader)) {
280       if ($this->enable_cache) {
281         $header = $this->cache_translations[""];
282       } else {
283         $header = $this->get_translation_string(0);
284       }
285       if (eregi("plural-forms: (.*)\n", $header, $regs))
286         $expr = $regs[1];
287       else
288         $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
289       $this->pluralheader = $expr;
290     }
291     return $this->pluralheader;
292   }
293
294   /**
295    * Detects which plural form to take
296    * 
297    * @access private
298    * @param n count
299    * @return int array index of the right plural form
300    */
301   function select_string($n) {
302     $string = $this->get_plural_forms();
303     $string = str_replace('nplurals',"\$total",$string);
304     $string = str_replace("n",$n,$string);
305     $string = str_replace('plural',"\$plural",$string);
306     
307     $total = 0;
308     $plural = 0;
309
310     eval("$string");
311     if ($plural >= $total) $plural = 0;
312     return $plural;
313   }
314
315   /**
316    * Plural version of gettext
317    * 
318    * @access public
319    * @param string single
320    * @param string plural
321    * @param string number
322    * @return translated plural form
323    */
324   function ngettext($single, $plural, $number) {
325     if ($this->short_circuit) {
326       if ($number != 1)
327         return $plural;
328       else
329         return $single;
330     }
331
332     // find out the appropriate form
333     $select = $this->select_string($number); 
334     
335     // this should contains all strings separated by NULLs
336     $key = $single.chr(0).$plural;
337     
338     
339     if ($this->enable_cache) {
340       if (! array_key_exists($key, $this->cache_translations)) {
341         return ($number != 1) ? $plural : $single;
342       } else {
343         $result = $this->cache_translations[$key];
344         $list = explode(chr(0), $result);
345         return $list[$select];
346       }
347     } else {
348       $num = $this->find_string($key);
349       if ($num == -1) {
350         return ($number != 1) ? $plural : $single;
351       } else {
352         $result = $this->get_translation_string($num);
353         $list = explode(chr(0), $result);
354         return $list[$select];
355       }
356     }
357   }
358
359 }
360
361 ?>