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