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