root/tags/1.3/wp-includes/gettext.php

Revision 972, 11.0 kB (checked in by donncha, 2 years ago)

WP Merge to rev 5499, this is a big one! Test it before you put it live!
Test only, not for production use yet

  • Property svn:eol-style set to native
Line 
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 $select_string_function = NULL; // cache function, which chooses plural forms
49     var $total = 0;          // total string count
50     var $table_originals = NULL// table for original strings (offsets)
51     var $table_translations = NULL// table for translated strings (offsets)
52     var $cache_translations = NULL// original -> translation mapping
53
54
55     /* Methods */
56
57
58     /**
59      * Reads a 32bit Integer from the Stream
60      *
61      * @access private
62      * @return Integer from the Stream
63      */
64     function readint() {
65         if ($this->BYTEORDER == 0) {
66             // low endian
67             $low_end = unpack('V', $this->STREAM->read(4));
68             return array_shift($low_end);
69         } else {
70             // big endian
71             $big_end = unpack('N', $this->STREAM->read(4));
72             return array_shift($big_end);
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 || isset($Reader->error) ) {
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.0.2, see https://savannah.nongnu.org/bugs/?func=detailitem&item_id=10565
109         $MAGIC1 = (int) - 1794895138;
110         // $MAGIC2 = (int)0xde120495; //bug
111         $MAGIC2 = (int) - 569244523;
112         // 64-bit fix
113         $MAGIC3 = (int) 2500072158;
114
115         $this->STREAM = $Reader;
116         $magic = $this->readint();
117         if ($magic == ($MAGIC1 & 0xFFFFFFFF) || $magic == ($MAGIC3 & 0xFFFFFFFF)) { // to make sure it works for 64-bit platforms
118             $this->BYTEORDER = 0;
119         } elseif ($magic == ($MAGIC2 & 0xFFFFFFFF)) {
120             $this->BYTEORDER = 1;
121         } else {
122             $this->error = 1; // not MO file
123             return false;
124         }
125
126         // FIXME: Do we care about revision? We should.
127         $revision = $this->readint();
128
129         $this->total = $this->readint();
130         $this->originals = $this->readint();
131         $this->translations = $this->readint();
132     }
133
134     /**
135      * Loads the translation tables from the MO file into the cache
136      * If caching is enabled, also loads all strings into a cache
137      * to speed up translation lookups
138      *
139      * @access private
140      */
141     function load_tables() {
142         if (is_array($this->cache_translations) &&
143             is_array($this->table_originals) &&
144             is_array($this->table_translations))
145             return;
146
147         /* get original and translations tables */
148         $this->STREAM->seekto($this->originals);
149         $this->table_originals = $this->readintarray($this->total * 2);
150         $this->STREAM->seekto($this->translations);
151         $this->table_translations = $this->readintarray($this->total * 2);
152
153         if ($this->enable_cache) {
154             $this->cache_translations = array ();
155             /* read all strings in the cache */
156             for ($i = 0; $i < $this->total; $i++) {
157                 $this->STREAM->seekto($this->table_originals[$i * 2 + 2]);
158                 $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]);
159                 $this->STREAM->seekto($this->table_translations[$i * 2 + 2]);
160                 $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]);
161                 $this->cache_translations[$original] = $translation;
162             }
163         }
164     }
165
166     /**
167      * Returns a string from the "originals" table
168      *
169      * @access private
170      * @param int num Offset number of original string
171      * @return string Requested string if found, otherwise ''
172      */
173     function get_original_string($num) {
174         $length = $this->table_originals[$num * 2 + 1];
175         $offset = $this->table_originals[$num * 2 + 2];
176         if (! $length)
177             return '';
178         $this->STREAM->seekto($offset);
179         $data = $this->STREAM->read($length);
180         return (string)$data;
181     }
182
183     /**
184      * Returns a string from the "translations" table
185      *
186      * @access private
187      * @param int num Offset number of original string
188      * @return string Requested string if found, otherwise ''
189      */
190     function get_translation_string($num) {
191         $length = $this->table_translations[$num * 2 + 1];
192         $offset = $this->table_translations[$num * 2 + 2];
193         if (! $length)
194             return '';
195         $this->STREAM->seekto($offset);
196         $data = $this->STREAM->read($length);
197         return (string)$data;
198     }
199
200     /**
201      * Binary search for string
202      *
203      * @access private
204      * @param string string
205      * @param int start (internally used in recursive function)
206      * @param int end (internally used in recursive function)
207      * @return int string number (offset in originals table)
208      */
209     function find_string($string, $start = -1, $end = -1) {
210         if (($start == -1) or ($end == -1)) {
211             // find_string is called with only one parameter, set start end end
212             $start = 0;
213             $end = $this->total;
214         }
215         if (abs($start - $end) <= 1) {
216             // We're done, now we either found the string, or it doesn't exist
217             $txt = $this->get_original_string($start);
218             if ($string == $txt)
219                 return $start;
220             else
221                 return -1;
222         } else if ($start > $end) {
223             // start > end -> turn around and start over
224             return $this->find_string($string, $end, $start);
225         } else {
226             // Divide table in two parts
227             $half = (int)(($start + $end) / 2);
228             $cmp = strcmp($string, $this->get_original_string($half));
229             if ($cmp == 0)
230                 // string is exactly in the middle => return it
231                 return $half;
232             else if ($cmp < 0)
233                 // The string is in the upper half
234                 return $this->find_string($string, $start, $half);
235             else
236                 // The string is in the lower half
237                 return $this->find_string($string, $half, $end);
238         }
239     }
240
241     /**
242      * Translates a string
243      *
244      * @access public
245      * @param string string to be translated
246      * @return string translated string (or original, if not found)
247      */
248     function translate($string) {
249         if ($this->short_circuit)
250             return $string;
251         $this->load_tables();
252
253         if ($this->enable_cache) {
254             // Caching enabled, get translated string from cache
255             if (array_key_exists($string, $this->cache_translations))
256                 return $this->cache_translations[$string];
257             else
258                 return $string;
259         } else {
260             // Caching not enabled, try to find string
261             $num = $this->find_string($string);
262             if ($num == -1)
263                 return $string;
264             else
265                 return $this->get_translation_string($num);
266         }
267     }
268
269     /**
270      * Get possible plural forms from MO header
271      *
272      * @access private
273      * @return string plural form header
274      */
275     function get_plural_forms() {
276         // lets assume message number 0 is header
277         // this is true, right?
278         $this->load_tables();
279
280         // cache header field for plural forms
281         if (! is_string($this->pluralheader)) {
282             if ($this->enable_cache) {
283                 $header = $this->cache_translations[""];
284             } else {
285                 $header = $this->get_translation_string(0);
286             }
287             $header .= "\n"; //make sure our regex matches
288             if (eregi("plural-forms: ([^\n]*)\n", $header, $regs))
289                 $expr = $regs[1];
290             else
291                 $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
292
293             // add parentheses
294              // important since PHP's ternary evaluates from left to right
295              $expr.= ';';
296              $res= '';
297              $p= 0;
298              for ($i= 0; $i < strlen($expr); $i++) {
299                 $ch= $expr[$i];
300                 switch ($ch) {
301                     case '?':
302                         $res.= ' ? (';
303                         $p++;
304                         break;
305                     case ':':
306                         $res.= ') : (';
307                         break;
308                     case ';':
309                         $res.= str_repeat( ')', $p) . ';';
310                         $p= 0;
311                         break;
312                     default:
313                         $res.= $ch;
314                 }
315             }
316             $this->pluralheader = $res;
317         }
318
319         return $this->pluralheader;
320     }
321
322     /**
323      * Detects which plural form to take
324      *
325      * @access private
326      * @param n count
327      * @return int array index of the right plural form
328      */
329     function select_string($n) {
330         if (is_null($this->select_string_function)) {
331             $string = $this->get_plural_forms();
332             if (preg_match("/nplurals\s*=\s*(\d+)\s*\;\s*plural\s*=\s*(.*?)\;+/", $string, $matches)) {
333                 $nplurals = $matches[1];
334                 $expression = $matches[2];
335                 $expression = str_replace("n", '$n', $expression);
336             } else {
337                 $nplurals = 2;
338                 $expression = ' $n == 1 ? 0 : 1 ';
339             }
340             $func_body = "
341                 \$plural = ($expression);
342                 return (\$plural <= $nplurals)? \$plural : \$plural - 1;";
343             $this->select_string_function = create_function('$n', $func_body);
344         }
345         return call_user_func($this->select_string_function, $n);
346     }
347
348     /**
349      * Plural version of gettext
350      *
351      * @access public
352      * @param string single
353      * @param string plural
354      * @param string number
355      * @return translated plural form
356      */
357     function ngettext($single, $plural, $number) {
358         if ($this->short_circuit) {
359             if ($number != 1)
360                 return $plural;
361             else
362                 return $single;
363         }
364
365         // find out the appropriate form
366         $select = $this->select_string($number);
367
368         // this should contains all strings separated by NULLs
369         $key = $single.chr(0).$plural;
370
371
372         if ($this->enable_cache) {
373             if (! array_key_exists($key, $this->cache_translations)) {
374                 return ($number != 1) ? $plural : $single;
375             } else {
376                 $result = $this->cache_translations[$key];
377                 $list = explode(chr(0), $result);
378                 return $list[$select];
379             }
380         } else {
381             $num = $this->find_string($key);
382             if ($num == -1) {
383                 return ($number != 1) ? $plural : $single;
384             } else {
385                 $result = $this->get_translation_string($num);
386                 $list = explode(chr(0), $result);
387                 return $list[$select];
388             }
389         }
390     }
391
392 }
393
394 ?>
395
Note: See TracBrowser for help on using the browser.