Added helper for gettext domain switching.
[fa-stable.git] / includes / lang / gettext.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
3 //  
4 //  Copyright (c) 2003 Laurent Bedubourg
5 //  
6 //  This library is free software; you can redistribute it and/or
7 //  modify it under the terms of the GNU Lesser General Public
8 //  License as published by the Free Software Foundation; either
9 //  version 2.1 of the License, or (at your option) any later version.
10 //  
11 //  This library is distributed in the hope that it will be useful,
12 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 //  Lesser General Public License for more details.
15 //  
16 //  You should have received a copy of the GNU Lesser General Public
17 //  License along with this library; if not, write to the Free Software
18 //  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19 //  
20 //  Authors: Laurent Bedubourg <laurent.bedubourg@free.fr>
21 //  
22
23 //require_once "PEAR.php";
24
25 define('GETTEXT_NATIVE', 1);
26 define('GETTEXT_PHP', 2);
27
28 function get_text_init($managerType = GETTEXT_NATIVE) {
29
30         if (!isset($_SESSION['get_text'])) {
31
32         if ($managerType == GETTEXT_NATIVE) 
33         {
34             if (function_exists('gettext')) 
35             {
36                 $_SESSION['get_text'] = new gettext_native_support();
37                 return;
38             }
39         }
40         // fail back to php support 
41                 $_SESSION['get_text'] = new gettext_php_support();
42         }
43 }
44
45 function raise_error($str) {
46         error_log($str);
47         return 1;
48 }
49
50 function is_error($err) {
51     return $err > 0;
52 }
53
54 /**
55 * Interface to gettext native support.
56 *
57 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
58 * @access private
59 */
60 class gettext_native_support 
61 {
62     var $_interpolation_vars = array();
63
64     /**
65      * Set gettext language code.
66      * @throws GetText_Error
67      */
68     function set_language($lang_code, $encoding)
69     {
70         putenv("LANG=$lang_code");
71         putenv("LC_ALL=$lang_code");
72         putenv("LANGUAGE=$lang_code");
73
74         //$set = setlocale(LC_ALL, "$lang_code");
75         //$set = setlocale(LC_ALL, "$encoding");
76
77                 // cover a couple of country/encoding variants 
78                 $up = strtoupper($encoding);
79                 $low = strtolower($encoding);
80                 $lshort = strtr($up, '-','');
81                 $ushort = strtr($low, '-','');
82
83                 if ($lang_code == 'C')
84                         $set = setlocale(LC_ALL,'C');
85                 else
86                 $set = setlocale(LC_ALL, $lang_code.".".$encoding, 
87                                 $lang_code.".".$up, $lang_code.".".$low,
88                                 $lang_code.".".$ushort, $lang_code.".".$lshort);
89
90         setlocale(LC_NUMERIC, 'C'); // important for numeric presentation etc.
91         if ($set === false) 
92         {
93             $str = sprintf('language code "%s", encoding "%s" not supported by your system',
94                 $lang_code, $encoding);
95             //$err = new GetText_Error($str);
96             //return PEAR::raise_error($err);
97                         return raise_error("1 " . $str);
98         }
99                 //return 0;
100     }
101     /**
102          *      Check system support for given language nedded for gettext.
103          */
104         function check_support($lang_code, $encoding)
105     {
106
107                 $old = setlocale(LC_CTYPE, '0'); // LC_MESSAGES does not exist on Win
108                 $up = strtoupper($encoding);
109                 $low = strtolower($encoding);
110                 $lshort = strtr($up, '-','');
111                 $ushort = strtr($low, '-','');
112
113         $test = setlocale(LC_ALL,
114                         $lang_code.".".$encoding, 
115                         $lang_code.".".$up,
116                         $lang_code.".".$low,
117                         $lang_code.".".$ushort,
118                         $lang_code.".".$lshort) !== false;
119                 setlocale(LC_ALL, $old);
120                 setlocale(LC_NUMERIC, 'C');
121                 return $test;
122         }
123     /**
124      * Add a translation domain.
125      */
126     function add_domain($domain, $path=false, $version='')
127     {
128         if ($path === false) 
129                 $path = "./locale";
130             if ($domain == "")
131                 $domain = "?";
132                 if ($version) {
133         // To avoid need for apache server restart after change of *.mo file
134         // we have to include file version as part of filename.
135         // This is alternative naming convention: $domain = $version.'/'.$domain;
136                         $domain .= '-'.$version;
137                 }
138         bindtextdomain($domain, $path);
139         //bind_textdomain_codeset($domain, $encoding);
140         textdomain($domain);
141     }
142
143     /**
144      * Retrieve translation for specified key.
145      *
146      * @access private
147      */
148     function _get_translation($key)
149     {
150         return gettext($key);
151     }
152     
153
154     /**
155      * Reset interpolation variables.
156      */
157     function reset()
158     {
159         $this->_interpolation_vars = array();
160     }
161     
162     /**
163      * Set an interpolation variable.
164      */
165     function set_var($key, $value)
166     {
167         $this->_interpolation_vars[$key] = $value;
168     }
169
170     /**
171      * Set an associative array of interpolation variables.
172      */
173     function set_vars($hash)
174     {
175         $this->_interpolation_vars = array_merge($this->_interpolation_vars,
176                                                 $hash);
177     }
178     
179     /**
180      * Retrieve translation for specified key.
181      *
182      * @param  string $key  -- gettext msgid
183      * @throws GetText_Error
184      */
185     function gettext($key)
186     {
187         $value = $this->_get_translation($key);
188         if ($value === false) {
189             $str = sprintf('Unable to locate gettext key "%s"', $key);
190             //$err = new GetText_Error($str);
191             //return PEAR::raise_error($err);
192                         return raise_error("2 " . $str);
193         }
194         
195         while (preg_match('/\$\{(.*?)\}/sm', $value, $m)) {
196             list($src, $var) = $m;
197
198             // retrieve variable to interpolate in context, throw an exception
199             // if not found.
200             $var2 = $this->_get_var($var);
201             if ($var2 === false) {
202                 $str = sprintf('Interpolation error, var "%s" not set', $var);
203                 //$err = new GetText_Error($str);
204                 //return PEAR::raise_error($err);
205                 return raise_error("3 " . $str);
206             }
207             $value = str_replace($src, $var2, $value);
208         }
209         return $value;
210     }
211
212     /**
213      * Retrieve an interpolation variable value.
214      * 
215      * @return mixed
216      * @access private
217      */
218     function _get_var($name)
219     {
220         if (!array_key_exists($name, $this->_interpolation_vars)) {
221             return false;
222         }
223         return $this->_interpolation_vars[$name];
224     }
225 }
226
227
228 /**
229 * Implementation of get_text support for PHP.
230 *
231 * This implementation is abble to cache .po files into php files returning the
232 * domain translation hashtable.
233 *
234 * @access private
235 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
236 */
237 class gettext_php_support extends gettext_native_support
238 {
239     var $_path     = 'locale/';
240     var $_lang_code = false;
241     var $_domains  = array();
242     var $_end      = -1;
243     var $_jobs     = array();
244
245     /**
246      * Set the translation domain.
247      *
248      * @param  string $lang_code -- language code
249      * @throws GetText_Error
250      */
251     function set_language($lang_code, $encoding)
252     {
253         // if language already set, try to reload domains
254         if ($this->_lang_code !== false and $this->_lang_code != $lang_code) 
255         {
256             foreach ($this->_domains as $domain) 
257             {
258                 $this->_jobs[] = array($domain->name, $domain->path);
259             }
260             $this->_domains = array();
261             $this->_end = -1;
262         }
263         
264         $this->_lang_code = $lang_code;
265
266         // this allow us to set the language code after 
267         // domain list.
268         while (count($this->_jobs) > 0) 
269         {
270             list($domain, $path) = array_shift($this->_jobs);
271             $err = $this->add_domain($domain, $path);
272             // error raised, break jobs
273             /*if (PEAR::is_error($err)) {
274                 return $err;
275             }*/
276                         if (is_error($err)) 
277                         {
278                 return $err;
279             }            
280         }
281     }
282     /**
283          *      Check system support for given language (dummy).
284          */
285         function check_support($lang_code, $encoding)
286     {
287                 return true;
288     }
289     /**
290      * Add a translation domain.
291      *
292      * @param string $domain        -- Domain name
293      * @param string $path optional -- Repository path
294      * @throws GetText_Error
295      */
296     function add_domain($domain, $path = "./locale/", $version ='')
297     {   
298         if ($version) {
299                         $domain .= '-'.$version;
300                 }
301
302         if (array_key_exists($domain, $this->_domains)) 
303         { 
304             return; 
305         }
306         
307         if (!$this->_lang_code) 
308         { 
309             $this->_jobs[] = array($domain, $path); 
310             return;
311         }
312
313         $err = $this->_load_domain($domain, $path);
314         if ($err != 0) 
315         {
316             return $err;
317         }
318
319         $this->_end++;
320     }
321
322     /**
323      * Load a translation domain file.
324      *
325      * This method cache the translation hash into a php file unless
326      * GETTEXT_NO_CACHE is defined.
327      * 
328      * @param  string $domain        -- Domain name
329      * @param  string $path optional -- Repository
330      * @throws GetText_Error
331      * @access private
332      */
333     function _load_domain($domain, $path = "./locale")
334     {
335         $src_domain = $path . "/$this->_lang_code/LC_MESSAGES/$domain.po";
336         $php_domain = $path . "/$this->_lang_code/LC_MESSAGES/$domain.php";
337         
338         if (!file_exists($src_domain)) 
339         {
340             $str = sprintf('Domain file "%s" not found.', $src_domain);
341             //$err = new GetText_Error($str);
342             //return PEAR::raise_error($err);
343                         return raise_error("4 " . $str);
344         }
345         
346         $d = new gettext_domain();
347         $d->name = $domain;
348         $d->path = $path;
349         
350         if (!file_exists($php_domain) || (filemtime($php_domain) < filemtime($src_domain))) 
351         {
352             
353             // parse and compile translation table
354             $parser = new gettext_php_support_parser();
355             $hash   = $parser->parse($src_domain);
356             if (!defined('GETTEXT_NO_CACHE')) 
357             {
358                 $comp = new gettext_php_support_compiler();
359                 $err  = $comp->compile($hash, $src_domain);
360                 /*if (PEAR::is_error($err)) { 
361                     return $err; 
362                 }*/
363                         if (is_error($err)) 
364                         {
365                     return $err;
366                 } 
367             }
368             $d->_keys = $hash;
369         } 
370         else 
371         {
372             $d->_keys = include $php_domain;
373         }
374         $this->_domains[] = &$d;
375     }
376     
377     /**
378      * Implementation of gettext message retrieval.
379      */
380     function _get_translation($key)
381     {
382         for ($i = $this->_end; $i >= 0; $i--) 
383         {
384             if ($this->_domains[$i]->has_key($key)) 
385             {
386                 return $this->_domains[$i]->get($key);
387             }
388         }
389         return $key;
390     }
391 }
392
393 /**
394 * Class representing a domain file for a specified language.
395 *
396 * @access private
397 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
398 */
399 class gettext_domain
400 {
401     var $name;
402     var $path;
403
404     var $_keys = array();
405
406     function has_key($key)
407     {
408         return array_key_exists($key, $this->_keys);
409     }
410
411     function get($key)
412     {
413         return $this->_keys[$key];
414     }
415 }
416
417 /**
418 * This class is used to parse gettext '.po' files into php associative arrays.
419 *
420 * @access private
421 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
422 */
423 class gettext_php_support_parser 
424 {
425     var $_hash = array();
426     var $_current_key;
427     var $_current_value;
428     
429     /**
430      * Parse specified .po file.
431      *
432      * @return hashtable
433      * @throws GetText_Error
434      */
435     function parse($file)
436     {
437         $this->_hash = array();
438         $this->_current_key = false;
439         $this->_current_value = "";
440         
441         if (!file_exists($file)) 
442         {
443             $str = sprintf('Unable to locate file "%s"', $file);
444             //$err = new GetText_Error($str);
445             //return PEAR::raise_error($err);
446                         return raise_error($str);
447         }
448         $i = 0;
449         $lines = file($file);
450         foreach ($lines as $line) 
451         {
452             $this->_parse_line($line, ++$i);
453         }
454         $this->_store_key();
455
456         return $this->_hash;
457     }
458
459     /**
460      * Parse one po line.
461      *
462      * @access private
463      */
464     function _parse_line($line, $nbr)
465     {
466         if (preg_match('/^\s*?#/', $line)) { return; }
467         if (preg_match('/^\s*?msgid \"(.*?)(?!<\\\)\"/', $line, $m)) {
468             $this->_store_key();
469             $this->_current_key = $m[1];
470             return;
471         }
472         if (preg_match('/^\s*?msgstr \"(.*?)(?!<\\\)\"/', $line, $m)) {
473             $this->_current_value .= $m[1];
474             return;
475         }
476         if (preg_match('/^\s*?\"(.*?)(?!<\\\)\"/', $line, $m)) {
477             $this->_current_value .= $m[1];
478             return;
479         }
480     }
481
482     /**
483      * Store last key/value pair into building hashtable.
484      *
485      * @access private
486      */
487     function _store_key()
488     {
489         if ($this->_current_key === false) return;
490         $this->_current_value = str_replace('\\n', "\n", $this->_current_value);
491         $this->_hash[$this->_current_key] = $this->_current_value;
492         $this->_current_key = false;
493         $this->_current_value = "";
494     }
495 }
496
497
498 /**
499 * This class write a php file from a gettext hashtable.
500 *
501 * The produced file return the translation hashtable on include.
502
503 * @throws GetText_Error
504 * @access private
505 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
506 */
507 class gettext_php_support_compiler 
508 {
509     /**
510      * Write hash in an includable php file.
511      */
512     function compile(&$hash, $source_path)
513     {
514         $dest_path = preg_replace('/\.po$/', '.php', $source_path);
515         $fp = @fopen($dest_path, "w");
516         if (!$fp) 
517         {
518             $str = sprintf('Unable to open "%s" in write mode.', $dest_path);
519             //$err = new GetText_Error($str);
520             //return PEAR::raise_error($err);
521                         return raise_error($str);
522         }
523         fwrite($fp, '<?php' . "\n");
524         fwrite($fp, 'return array(' . "\n");
525         foreach ($hash as $key => $value) 
526         {
527             $key   = str_replace("'", "\\'", $key);
528             $value = str_replace("'", "\\'", $value);
529             fwrite($fp, '    \'' . $key . '\' => \'' . $value . "',\n");
530         }
531         fwrite($fp, ');' . "\n");
532         fwrite($fp, '?>');
533         fclose($fp);
534     }
535 }
536
537 /*
538         Set current gettext domain path
539 */
540 function set_ext_domain($path='') {
541         global $path_to_root;
542
543         $_SESSION['get_text']->add_domain($_SESSION['language']->code,
544                 $path_to_root . ($path ? '/' : '') .$path.'/lang',
545                 $path ? '' : $_SESSION['language']->version);
546 }
547 ?>