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
25define('GETTEXT_NATIVE', 1);
26define('GETTEXT_PHP', 2);
27
28function 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
45function raise_error($str) {
46	error_log($str);
47	return 1;
48}
49
50function 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*/
60class 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*/
243class 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*/
412class 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*/
436class 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*/
521class 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*/
554function 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?>
573