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