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

Benjamin Mako Hill || Want to submit a patch?