Merging version 2.1 RC to main trunk.
[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 /**
29 * Generic gettext static class.
30 *
31 * This class allows gettext usage with php even if the gettext support is 
32 * not compiled in php.
33 *
34 * The developper can choose between the GETTEXT_NATIVE support and the
35 * GETTEXT_PHP support on initialisation. If native is not supported, the
36 * system will fall back to PHP support.
37 *
38 * On both systems, this package add a variable interpolation system so you can
39 * translate entire dynamic sentences in stead of peace of sentences.
40 *
41 * Small example without pear error lookup :
42
43 * <?php
44 * require_once "get_text.php";
45 *
46 * get_text::init();
47 * get_text::set_language('fr_Fr');      // may throw GetText_Error
48 * get_text::add_domain('myAppDomain');  // may throw GetText_Error
49 * get_text::set_var('login', $login);   
50 * get_text::set_var('name', $name);
51
52 * // may throw GetText_Error
53 * echo get_text::gettext('Welcome ${name}, you\'re connected with login ${login}');
54
55 * // should echo something like :
56 * //
57 * // "Bienvenue Jean-Claude, vous ĂȘtes connectĂ© en tant qu'utilisateur jcaccount"
58 * // 
59 * // or if fr_FR translation does not exists
60 * //
61 * // "Welcome Jean-Claude, you're connected with login jcaccount"
62
63 * ?>
64 *
65 * A gettext mini-howto should be provided with this package, if you're new 
66 * to gettext usage, please read it to learn how to build a gettext 
67 * translation directory (locale).
68
69 * @todo    Tools to manage gettext files in php.
70
71 *          - non traducted domains / keys
72 *          - modification of keys
73 *          - domain creation, preparation, delete, ...
74 *          - tool to extract required messages from TOF templates
75 *
76 * @version 0.5
77 * @author  Laurent Bedubourg <laurent.bedubourg@free.fr>
78 */
79 class get_text
80 {
81     /**
82      * This method returns current gettext support class.
83      *
84      * @return GetText_Support
85      * @static 1
86      * @access private
87      */
88     function &_support($set=false)
89     { 
90         static $support_obj;
91         if ($set !== false) 
92         { 
93             $support_obj = $set; 
94         } 
95         elseif (!isset($support_obj)) 
96         {
97             trigger_error("get_text not initialized !". '\n'.
98                 "Please call get_text::init() before calling ".
99                 "any get_text function !" . '\n' , E_USER_ERROR);
100         }
101         return $support_obj;
102     }
103     
104     /**
105      * Initialize gettext package.
106      *
107      * This method instantiate the gettext support depending on managerType
108      * value. 
109      *
110      * GETTEXT_NATIVE try to use gettext php support and fall back to PHP
111      * support if not installed.
112      *
113      * GETTEXT_PHP explicitely request the usage of PHP support.
114      *
115      * @param  int $managerType
116      *         Gettext support type.
117      *         
118      * @access public
119      * @static 1
120      */
121     function init($managerType = GETTEXT_NATIVE)
122     {
123         if ($managerType == GETTEXT_NATIVE) 
124         {
125             if (function_exists('gettext')) 
126             {
127                 return get_text::_support(new gettext_native_support());
128             }
129         }
130         // fail back to php support 
131         return get_text::_support(new gettext_php_support());
132     }
133     
134     /**
135      * Set the language to use for traduction.
136      *
137      * @param string $lang_code
138      *        The language code usually defined as ll_CC, ll is the two letter
139      *        language code and CC is the two letter country code.
140      *
141      * @throws GetText_Error if language is not supported by your system.
142      */
143     function set_language($lang_code, $encoding)
144     {
145         $support = &get_text::_support();
146         return $support->set_language($lang_code, $encoding);
147     }
148     
149     /**
150      * Add a translation domain.
151      *
152      * The domain name is usually the name of the .po file you wish to use. 
153      * For example, if you created a file 'lang/ll_CC/LC_MESSAGES/myapp.po',
154      * you'll use 'myapp' as the domain name.
155      *
156      * @param string $domain
157      *        The domain name.
158      *
159      * @param string $path optional
160      *        The path to the locale directory (ie: /path/to/locale/) which
161      *        contains ll_CC directories.
162      */
163     function add_domain($domain, $path=false)
164     {
165         $support =& get_text::_support();
166         return $support->add_domain($domain, $path);
167     }
168     
169     /**
170      * Retrieve the translation for specified key.
171      *
172      * @param string $key
173      *        String to translate using gettext support.
174      */
175     function gettext($key)
176     { 
177         $support = &get_text::_support();
178         return $support->gettext($key);
179     }
180    
181     /**
182      * Add a variable to gettext interpolation system.
183      *
184      * @param string $key
185      *        The variable name.
186      *
187      * @param string $value
188      *        The variable value.
189      */
190     function set_var($key, $value)
191     {
192         $support =& get_text::_support();
193         return $support->set_var($key, $value);
194     }
195
196     /**
197      * Add an hashtable of variables.
198      *
199      * @param hashtable $hash 
200      *        PHP associative array of variables.
201      */
202     function set_vars($hash)
203     {
204         $support =& get_text::_support();
205         return $support->set_vars($hash);
206     }
207
208     /**
209      * Reset interpolation variables.
210      */
211     function reset()
212     {
213         $support =& get_text::_support();
214         return $support->reset();
215     }
216 }
217
218 function raise_error($str) {
219 //      echo "$str";
220         return 1;
221 }
222
223 function is_error($err) {
224     return $err > 0;
225 }
226
227 /**
228 * Interface to gettext native support.
229 *
230 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
231 * @access private
232 */
233 class gettext_native_support 
234 {
235     var $_interpolation_vars = array();
236
237     /**
238      * Set gettext language code.
239      * @throws GetText_Error
240      */
241     function set_language($lang_code, $encoding)
242     {
243         putenv("LANG=$lang_code");
244         putenv("LC_ALL=$lang_code");
245         putenv("LANGUAGE=$lang_code");
246         
247         //$set = setlocale(LC_ALL, "$lang_code");
248         //$set = setlocale(LC_ALL, "$encoding");
249         $set = setlocale(LC_ALL, $lang_code.".".$encoding);
250         setlocale(LC_NUMERIC, 'C'); // important for numeric presentation etc.
251         if ($set === false) 
252         {
253             $str = sprintf('language code "%s", encoding "%s" not supported by your system',
254                 $lang_code, $encoding);
255             //$err = new GetText_Error($str);
256             //return PEAR::raise_error($err);
257                         return raise_error("1 " . $str);
258         }
259                 //return 0;
260     }
261     
262     /**
263      * Add a translation domain.
264      */
265     function add_domain($domain, $path=false)
266     {
267         if ($path === false) 
268         {
269             bindtextdomain($domain, "./locale/");
270         } 
271         else 
272         { 
273             bindtextdomain($domain, $path);
274         }
275         //bind_textdomain_codeset($domain, $encoding);
276         textdomain($domain);
277     }
278     
279     /**
280      * Retrieve translation for specified key.
281      *
282      * @access private
283      */
284     function _get_translation($key)
285     {
286         return gettext($key);
287     }
288     
289
290     /**
291      * Reset interpolation variables.
292      */
293     function reset()
294     {
295         $this->_interpolation_vars = array();
296     }
297     
298     /**
299      * Set an interpolation variable.
300      */
301     function set_var($key, $value)
302     {
303         $this->_interpolation_vars[$key] = $value;
304     }
305
306     /**
307      * Set an associative array of interpolation variables.
308      */
309     function set_vars($hash)
310     {
311         $this->_interpolation_vars = array_merge($this->_interpolation_vars,
312                                                 $hash);
313     }
314     
315     /**
316      * Retrieve translation for specified key.
317      *
318      * @param  string $key  -- gettext msgid
319      * @throws GetText_Error
320      */
321     function gettext($key)
322     {
323         $value = $this->_get_translation($key);
324         if ($value === false) {
325             $str = sprintf('Unable to locate gettext key "%s"', $key);
326             //$err = new GetText_Error($str);
327             //return PEAR::raise_error($err);
328                         return raise_error("2 " . $str);
329         }
330         
331         while (preg_match('/\$\{(.*?)\}/sm', $value, $m)) {
332             list($src, $var) = $m;
333
334             // retrieve variable to interpolate in context, throw an exception
335             // if not found.
336             $var2 = $this->_get_var($var);
337             if ($var2 === false) {
338                 $str = sprintf('Interpolation error, var "%s" not set', $var);
339                 //$err = new GetText_Error($str);
340                 //return PEAR::raise_error($err);
341                 return raise_error("3 " . $str);
342             }
343             $value = str_replace($src, $var2, $value);
344         }
345         return $value;
346     }
347
348     /**
349      * Retrieve an interpolation variable value.
350      * 
351      * @return mixed
352      * @access private
353      */
354     function _get_var($name)
355     {
356         if (!array_key_exists($name, $this->_interpolation_vars)) {
357             return false;
358         }
359         return $this->_interpolation_vars[$name];
360     }
361 }
362
363
364 /**
365 * Implementation of get_text support for PHP.
366 *
367 * This implementation is abble to cache .po files into php files returning the
368 * domain translation hashtable.
369 *
370 * @access private
371 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
372 */
373 class gettext_php_support extends gettext_native_support
374 {
375     var $_path     = 'locale/';
376     var $_lang_code = false;
377     var $_domains  = array();
378     var $_end      = -1;
379     var $_jobs     = array();
380
381     /**
382      * Set the translation domain.
383      *
384      * @param  string $lang_code -- language code
385      * @throws GetText_Error
386      */
387     function set_language($lang_code, $encoding)
388     {
389         // if language already set, try to reload domains
390         if ($this->_lang_code !== false and $this->_lang_code != $lang_code) 
391         {
392             foreach ($this->_domains as $domain) 
393             {
394                 $this->_jobs[] = array($domain->name, $domain->path);
395             }
396             $this->_domains = array();
397             $this->_end = -1;
398         }
399         
400         $this->_lang_code = $lang_code;
401
402         // this allow us to set the language code after 
403         // domain list.
404         while (count($this->_jobs) > 0) 
405         {
406             list($domain, $path) = array_shift($this->_jobs);
407             $err = $this->add_domain($domain, $path);
408             // error raised, break jobs
409             /*if (PEAR::is_error($err)) {
410                 return $err;
411             }*/
412                         if (is_error($err)) 
413                         {
414                 return $err;
415             }            
416         }
417     }
418     
419     /**
420      * Add a translation domain.
421      *
422      * @param string $domain        -- Domain name
423      * @param string $path optional -- Repository path
424      * @throws GetText_Error
425      */
426     function add_domain($domain, $path = "./locale/")
427     {   
428         if (array_key_exists($domain, $this->_domains)) 
429         { 
430             return; 
431         }
432         
433         if (!$this->_lang_code) 
434         { 
435             $this->_jobs[] = array($domain, $path); 
436             return;
437         }
438
439         $err = $this->_load_domain($domain, $path);
440         if ($err != 0) 
441         {
442             return $err;
443         }
444
445         $this->_end++;
446     }
447
448     /**
449      * Load a translation domain file.
450      *
451      * This method cache the translation hash into a php file unless
452      * GETTEXT_NO_CACHE is defined.
453      * 
454      * @param  string $domain        -- Domain name
455      * @param  string $path optional -- Repository
456      * @throws GetText_Error
457      * @access private
458      */
459     function _load_domain($domain, $path = "./locale")
460     {
461         $src_domain = $path . "/$this->_lang_code/LC_MESSAGES/$domain.po";
462         $php_domain = $path . "/$this->_lang_code/LC_MESSAGES/$domain.php";
463         
464         if (!file_exists($src_domain)) 
465         {
466             $str = sprintf('Domain file "%s" not found.', $src_domain);
467             //$err = new GetText_Error($str);
468             //return PEAR::raise_error($err);
469                         return raise_error("4 " . $str);
470         }
471         
472         $d = new gettext_domain();
473         $d->name = $domain;
474         $d->path = $path;
475         
476         if (!file_exists($php_domain) || (filemtime($php_domain) < filemtime($src_domain))) 
477         {
478             
479             // parse and compile translation table
480             $parser = new gettext_php_support_parser();
481             $hash   = $parser->parse($src_domain);
482             if (!defined('GETTEXT_NO_CACHE')) 
483             {
484                 $comp = new gettext_php_support_compiler();
485                 $err  = $comp->compile($hash, $src_domain);
486                 /*if (PEAR::is_error($err)) { 
487                     return $err; 
488                 }*/
489                         if (is_error($err)) 
490                         {
491                     return $err;
492                 } 
493             }
494             $d->_keys = $hash;
495         } 
496         else 
497         {
498             $d->_keys = include $php_domain;
499         }
500         $this->_domains[] = &$d;
501     }
502     
503     /**
504      * Implementation of gettext message retrieval.
505      */
506     function _get_translation($key)
507     {
508         for ($i = $this->_end; $i >= 0; $i--) 
509         {
510             if ($this->_domains[$i]->has_key($key)) 
511             {
512                 return $this->_domains[$i]->get($key);
513             }
514         }
515         return $key;
516     }
517 }
518
519 /**
520 * Class representing a domain file for a specified language.
521 *
522 * @access private
523 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
524 */
525 class gettext_domain
526 {
527     var $name;
528     var $path;
529
530     var $_keys = array();
531
532     function has_key($key)
533     {
534         return array_key_exists($key, $this->_keys);
535     }
536
537     function get($key)
538     {
539         return $this->_keys[$key];
540     }
541 }
542
543 /**
544 * This class is used to parse gettext '.po' files into php associative arrays.
545 *
546 * @access private
547 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
548 */
549 class gettext_php_support_parser 
550 {
551     var $_hash = array();
552     var $_current_key;
553     var $_current_value;
554     
555     /**
556      * Parse specified .po file.
557      *
558      * @return hashtable
559      * @throws GetText_Error
560      */
561     function parse($file)
562     {
563         $this->_hash = array();
564         $this->_current_key = false;
565         $this->_current_value = "";
566         
567         if (!file_exists($file)) 
568         {
569             $str = sprintf('Unable to locate file "%s"', $file);
570             //$err = new GetText_Error($str);
571             //return PEAR::raise_error($err);
572                         return raise_error($str);
573         }
574         $i = 0;
575         $lines = file($file);
576         foreach ($lines as $line) 
577         {
578             $this->_parse_line($line, ++$i);
579         }
580         $this->_store_key();
581
582         return $this->_hash;
583     }
584
585     /**
586      * Parse one po line.
587      *
588      * @access private
589      */
590     function _parse_line($line, $nbr)
591     {
592         if (preg_match('/^\s*?#/', $line)) { return; }
593         if (preg_match('/^\s*?msgid \"(.*?)(?!<\\\)\"/', $line, $m)) {
594             $this->_store_key();
595             $this->_current_key = $m[1];
596             return;
597         }
598         if (preg_match('/^\s*?msgstr \"(.*?)(?!<\\\)\"/', $line, $m)) {
599             $this->_current_value .= $m[1];
600             return;
601         }
602         if (preg_match('/^\s*?\"(.*?)(?!<\\\)\"/', $line, $m)) {
603             $this->_current_value .= $m[1];
604             return;
605         }
606     }
607
608     /**
609      * Store last key/value pair into building hashtable.
610      *
611      * @access private
612      */
613     function _store_key()
614     {
615         if ($this->_current_key === false) return;
616         $this->_current_value = str_replace('\\n', "\n", $this->_current_value);
617         $this->_hash[$this->_current_key] = $this->_current_value;
618         $this->_current_key = false;
619         $this->_current_value = "";
620     }
621 }
622
623
624 /**
625 * This class write a php file from a gettext hashtable.
626 *
627 * The produced file return the translation hashtable on include.
628
629 * @throws GetText_Error
630 * @access private
631 * @author Laurent Bedubourg <laurent.bedubourg@free.fr>
632 */
633 class gettext_php_support_compiler 
634 {
635     /**
636      * Write hash in an includable php file.
637      */
638     function compile(&$hash, $source_path)
639     {
640         $dest_path = preg_replace('/\.po$/', '.php', $source_path);
641         $fp = @fopen($dest_path, "w");
642         if (!$fp) 
643         {
644             $str = sprintf('Unable to open "%s" in write mode.', $dest_path);
645             //$err = new GetText_Error($str);
646             //return PEAR::raise_error($err);
647                         return raise_error($str);
648         }
649         fwrite($fp, '<?php' . "\n");
650         fwrite($fp, 'return array(' . "\n");
651         foreach ($hash as $key => $value) 
652         {
653             $key   = str_replace("'", "\\'", $key);
654             $value = str_replace("'", "\\'", $value);
655             fwrite($fp, '    \'' . $key . '\' => \'' . $value . "',\n");
656         }
657         fwrite($fp, ');' . "\n");
658         fwrite($fp, '?>');
659         fclose($fp);
660     }
661 }
662
663 /**
664 * get_text related error.
665 */
666 //class GetText_Error extends PEAR_Error {}
667
668 ?>