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