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