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