1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4/**
5 * Contains the Translation2 base class
6 *
7 * PHP versions 4 and 5
8 *
9 * LICENSE: Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions are met:
11 * 1. Redistributions of source code must retain the above copyright
12 *    notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 *    notice, this list of conditions and the following disclaimer in the
15 *    documentation and/or other materials provided with the distribution.
16 * 3. The name of the author may not be used to endorse or promote products
17 *    derived from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
20 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
21 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 * IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 *
30 * @category  Internationalization
31 * @package   Translation2
32 * @author    Lorenzo Alberton <l.alberton@quipo.it>
33 * @copyright 2004-2008 Lorenzo Alberton
34 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
35 * @version   CVS: $Id: Translation2.php 268999 2008-11-14 16:18:50Z quipo $
36 * @link      http://pear.php.net/package/Translation2
37 */
38
39/**
40 * require PEAR base class
41 */
42require_once 'PEAR.php';
43
44/**
45 * Allows redefinition of the default pageID.
46 * This constant is needed to allow both NULL and EMPTY pageID values
47 * and to have them match
48 */
49if (!defined('TRANSLATION2_DEFAULT_PAGEID')) {
50    define('TRANSLATION2_DEFAULT_PAGEID', 'translation2_default_pageID');
51}
52/**
53 * Class Error codes
54 */
55define('TRANSLATION2_ERROR',                      -1);
56define('TRANSLATION2_ERROR_METHOD_NOT_SUPPORTED', -2);
57define('TRANSLATION2_ERROR_CANNOT_CONNECT',       -3);
58define('TRANSLATION2_ERROR_CANNOT_FIND_FILE',     -4);
59define('TRANSLATION2_ERROR_DOMAIN_NOT_SET',       -5);
60define('TRANSLATION2_ERROR_INVALID_PATH',         -6);
61define('TRANSLATION2_ERROR_CANNOT_CREATE_DIR',    -7);
62define('TRANSLATION2_ERROR_CANNOT_WRITE_FILE',    -8);
63define('TRANSLATION2_ERROR_UNKNOWN_LANG',         -9);
64define('TRANSLATION2_ERROR_ENCODING_CONVERSION', -10);
65define('TRANSLATION2_ERROR_UNSUPPORTED',         -11);
66
67/**
68 * Translation2 base class
69 *
70 * This class provides an easy way to retrieve all the strings
71 * for a multilingual site or application from a data source
72 * (i.e. a db, an xml file or a gettext file).
73 *
74 * @category  Internationalization
75 * @package   Translation2
76 * @author    Lorenzo Alberton <l.alberton@quipo.it>
77 * @copyright 2004-2008 Lorenzo Alberton
78 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
79 * @link      http://pear.php.net/package/Translation2
80 */
81class Translation2
82{
83    // {{{ class vars
84
85    /**
86     * Storage object
87     * @var object
88     * @access protected
89     */
90    var $storage = '';
91
92    /**
93     * Class options
94     * @var array
95     */
96    var $options = array();
97
98    /**
99     * Default lang
100     * @var array
101     * @access protected
102     */
103    var $lang = array();
104
105    /**
106     * Current pageID
107     * @var string
108     * @access protected
109     */
110    var $currentPageID = null;
111
112    /**
113     * Array of parameters for the adapter class
114     * @var array
115     * @access protected
116     */
117    var $params = array();
118
119    // }}}
120    // {{{ Constructor
121
122    /**
123     * Constructor
124     */
125    function Translation2()
126    {
127        if (func_num_args()) {
128            $msg = '<b>Translation2 error:</b>'
129                  .' Don\'t use the constructor - use factory()';
130            trigger_error($msg, E_USER_ERROR);
131        }
132    }
133
134    // }}}
135    // {{{ factory()
136
137    /**
138     * Return a Translation2 instance already initialized
139     *
140     * @param string $driver  Type of the storage driver
141     * @param mixed  $options Additional options for the storage driver
142     *                        (example: if you are using DB as the storage
143     *                        driver, you have to pass the dsn string here)
144     * @param array  $params  Array of parameters for the adapter class
145     *                        (i.e. you can set here the mappings between your
146     *                        table/field names and the ones used by this class)
147     *
148     * @return object Translation2 instance or PEAR_Error on failure
149     * @static
150     */
151    function & factory($driver, $options = '', $params = array())
152    {
153        $tr = new Translation2;
154        $tr->storage = Translation2::_storageFactory($driver, $options);
155        if (PEAR::isError($tr->storage)) {
156            return $tr->storage;
157        }
158        $tr->_setDefaultOptions();
159        $tr->_parseOptions($params);
160        $tr->storage->_parseOptions($params);
161        return $tr;
162    }
163
164    // }}}
165    // {{{ _storageFactory()
166
167    /**
168     * Return a storage driver based on $driver and $options
169     *
170     * @param string $driver  Type of storage class to return
171     * @param string $options Optional parameters for the storage class
172     *
173     * @return object Object   Storage object
174     * @static
175     * @access private
176     */
177    function & _storageFactory($driver, $options = '')
178    {
179        $storage_path  = 'Translation2/Container/'.strtolower($driver).'.php';
180        $storage_class = 'Translation2_Container_'.strtolower($driver);
181        include_once $storage_path;
182        $storage = new $storage_class;
183        $err = $storage->init($options);
184        if (PEAR::isError($err)) {
185            return $err;
186        }
187        return $storage;
188    }
189
190    // }}}
191    // {{{ setContainerOptions()
192
193    /**
194     * Set some storage driver options
195     *
196     * @param array $options array of options
197     *
198     * @return void
199     * @access protected
200     */
201    function setContainerOptions($options)
202    {
203        $this->storage->_parseOptions($options);
204    }
205
206    // }}}
207    // {{{ _setDefaultOptions()
208
209    /**
210     * Set some default options
211     *
212     * @return void
213     * @access private
214     */
215    function _setDefaultOptions()
216    {
217        $this->options['ParameterPrefix']   = '&&';
218        $this->options['ParameterPostfix']  = '&&';
219        $this->options['ParameterAutoFree'] = true;
220        $this->options['prefetch']          = true;
221    }
222
223    // }}}
224    // {{{ _parseOptions()
225
226    /**
227     * Parse options passed to the base class
228     *
229     * @param array $array options
230     *
231     * @return void
232     * @access private
233     */
234    function _parseOptions($array)
235    {
236        foreach ($array as $key => $value) {
237            if (isset($this->options[$key])) {
238                $this->options[$key] = $value;
239            }
240        }
241    }
242
243    // }}}
244    // {{{ getDecorator()
245
246    /**
247     * Return an instance of a decorator
248     *
249     * This method is used to get a decorator instance.
250     * A decorator can be seen as a filter, i.e. something that can change
251     * or handle the values of the objects/vars that pass through.
252     *
253     * @param string $decorator Name of the decorator
254     *
255     * @return object Decorator object reference
256     */
257    function & getDecorator($decorator)
258    {
259        $decorator_path  = 'Translation2/Decorator/'.$decorator.'.php';
260        $decorator_class = 'Translation2_Decorator_'.$decorator;
261        include_once $decorator_path;
262        if (func_num_args() > 1) {
263            $obj = func_get_arg(1);
264            $new_decorator = new $decorator_class($obj);
265        } else {
266            $new_decorator = new $decorator_class($this);
267        }
268        return $new_decorator;
269    }
270
271    // }}}
272    // {{{ setCharset()
273
274    /**
275     * Set charset used to read/store the translations
276     *
277     * @param string $charset character set (encoding)
278     *
279     * @return void|PEAR_Error
280     */
281    function setCharset($charset)
282    {
283        $res = $this->storage->setCharset($charset);
284        if (PEAR::isError($res)) {
285            return $res;
286        }
287    }
288
289    // }}}
290    // {{{ setLang()
291
292    /**
293     * Set default lang
294     *
295     * Set the language that shall be used when retrieving strings.
296     *
297     * @param string $langID language code (for instance, 'en' or 'it')
298     *
299     * @return true|PEAR_Error
300     */
301    function setLang($langID)
302    {
303        $res = $this->storage->setLang($langID);
304        if (PEAR::isError($res)) {
305            return $res;
306        }
307        $this->lang = $res;
308        return true;
309    }
310
311    // }}}
312    // {{{ setPageID($pageID)
313
314    /**
315     * Set default page
316     *
317     * Set the page (aka "group of strings") that shall be used when retrieving strings.
318     * If you set it, you don't have to state it in each get() call.
319     *
320     * @param string $pageID ID of the default page
321     *
322     * @return self
323     */
324    function setPageID($pageID = null)
325    {
326        $this->currentPageID = $pageID;
327        return $this;
328    }
329
330    // }}}
331    // {{{ getLang()
332
333    /**
334     * get lang info
335     *
336     * Get some extra information about the language (its full name,
337     * the localized error text, ...)
338     *
339     * @param string $langID language ID
340     * @param string $format ['name', 'meta', 'error_text', 'array']
341     *
342     * @return mixed [string | array], depending on $format
343     */
344    function getLang($langID = null, $format = 'name')
345    {
346        if (is_null($langID)) {
347            if (!isset($this->lang['id'])) {
348                $msg = 'Translation2::getLang(): unknown language "'.$langID.'".'
349                      .' Use Translation2::setLang() to set a default language.';
350                return $this->storage->raiseError($msg, TRANSLATION2_ERROR_UNKNOWN_LANG);
351            }
352            $langID = $this->lang['id'];
353        }
354        $lang = $this->storage->getLangData($langID);
355        if ($format == 'array') {
356            return $lang;
357        } elseif (isset($lang[$format])) {
358            return $lang[$format];
359        } elseif (isset($lang['name'])) {
360            return $lang['name'];
361        }
362        $msg = 'Translation2::getLang(): unknown language "'.$langID.'".'
363              .' Use Translation2::setLang() to set a default language.';
364        return $this->storage->raiseError($msg, TRANSLATION2_ERROR_UNKNOWN_LANG);
365    }
366
367    // }}}
368    // {{{ getLangs()
369
370    /**
371     * get langs
372     *
373     * Get some extra information about the languages (their full names,
374     * the localized error text, their codes, ...)
375     *
376     * @param string $format ['ids', 'names', 'array']
377     *
378     * @return array|PEAR_Error
379     */
380    function getLangs($format = 'name')
381    {
382        return $this->storage->getLangs($format);
383    }
384
385    // }}}
386    // {{{ setParams()
387
388    /**
389     * Set parameters for next string
390     *
391     * Set the replacement for the parameters in the string(s).
392     * Parameter delimiters are customizable.
393     *
394     * @param array $params array of replacement parameters
395     *
396     * @return self
397     */
398    function setParams($params = null)
399    {
400        if (empty($params)) {
401            $this->params = array();
402        } elseif (is_array($params)) {
403            $this->params = $params;
404        } else {
405            $this->params = array($params);
406        }
407        return $this;
408    }
409
410    // }}}
411    // {{{ _replaceParams()
412
413    /**
414     * Replace parameters in strings
415     *
416     * @param mixed $strings strings where the replacements must occur
417     *
418     * @return mixed
419     * @access protected
420     */
421    function _replaceParams($strings)
422    {
423        if (empty($strings) || is_object($strings) || !count($this->params)) {
424            return $strings;
425        }
426        if (is_array($strings)) {
427            foreach ($strings as $key => $string) {
428                $strings[$key] = $this->_replaceParams($string);
429            }
430        } else {
431            if (strpos($strings, $this->options['ParameterPrefix']) !== false) {
432                foreach ($this->params as $name => $value) {
433                    $strings = str_replace($this->options['ParameterPrefix']
434                                           . $name . $this->options['ParameterPostfix'],
435                                           $value,
436                                           $strings);
437                }
438                if ($this->options['ParameterAutoFree']) {
439                    $this->params = array();
440                }
441            }
442        }
443        return $strings;
444    }
445
446    // }}}
447    // {{{ replaceEmptyStringsWithKeys()
448
449    /**
450     * Replace empty strings with their stringID
451     *
452     * @param array $strings array of strings to be replaced if empty
453     *
454     * @return array
455     * @static
456     */
457    function replaceEmptyStringsWithKeys($strings)
458    {
459        if (!is_array($strings)) {
460            return $strings;
461        }
462        foreach ($strings as $key => $string) {
463            if (empty($string)) {
464                $strings[$key] = $key;
465            }
466        }
467        return $strings;
468    }
469
470    // }}}
471    // {{{ getRaw()
472
473    /**
474     * Get translated string (as-is)
475     *
476     * @param string $stringID    ID of the string to be translated
477     * @param string $pageID      ID of the page/group containing the string
478     * @param string $langID      ID of the language
479     * @param string $defaultText Text to display when the string is empty
480     *
481     * @return string|PEAR_Error
482     */
483    function getRaw($stringID, $pageID = TRANSLATION2_DEFAULT_PAGEID, $langID = null, $defaultText = '')
484    {
485        $pageID = ($pageID == TRANSLATION2_DEFAULT_PAGEID ? $this->currentPageID : $pageID);
486        $str = $this->storage->getOne($stringID, $pageID, $langID);
487        if (empty($str)) {
488            $str = $defaultText;
489        }
490        return $str;
491    }
492
493    // }}}
494    // {{{ get()
495
496    /**
497     * Get translated string
498     *
499     * First check if the string is cached, if not => fetch the page
500     * from the container and cache it for later use.
501     * If the string is empty, check the fallback language; if
502     * the latter is empty too, then return the $defaultText.
503     *
504     * @param string $stringID    ID of the string
505     * @param string $pageID      ID of the page/group containing the string
506     * @param string $langID      ID of the language
507     * @param string $defaultText Text to display when the string is empty
508     *               NB: This parameter is only used in the DefaultText decorator
509     *
510     * @return string
511     */
512    function get($stringID, $pageID = TRANSLATION2_DEFAULT_PAGEID, $langID = null, $defaultText = '')
513    {
514        $str = $this->getRaw($stringID, $pageID, $langID);
515        if (PEAR::isError($str)) {
516            return $str;
517        }
518        return $this->_replaceParams($str);
519    }
520
521    // }}}
522    // {{{ getRawPage()
523
524    /**
525     * Get the array of strings in a page
526     *
527     * Fetch the page (aka "group of strings) from the container,
528     * without applying any formatting and without replacing the parameters
529     *
530     * @param string $pageID ID of the page/group containing the string
531     * @param string $langID ID of the language
532     *
533     * @return array
534     */
535    function getRawPage($pageID = TRANSLATION2_DEFAULT_PAGEID, $langID = null)
536    {
537        $pageID = ($pageID == TRANSLATION2_DEFAULT_PAGEID ? $this->currentPageID : $pageID);
538        return $this->storage->getPage($pageID, $langID);
539    }
540
541    // }}}
542    // {{{ getPage()
543
544    /**
545     * Get an entire group of strings
546     *
547     * Same as getRawPage, but resort to fallback language and
548     * replace parameters when needed
549     *
550     * @param string $pageID ID of the page/group containing the string
551     * @param string $langID ID of the language
552     *
553     * @return array
554     */
555    function getPage($pageID = TRANSLATION2_DEFAULT_PAGEID, $langID = null)
556    {
557        $pageData = $this->getRawPage($pageID, $langID);
558        return $this->_replaceParams($pageData);
559    }
560
561    // }}}
562    // {{{ getStringID()
563
564    /**
565     * Get the stringID for the given string. This method is the reverse of get().
566     *
567     * @param string $string This is NOT the stringID, this is a real string.
568     *               The method will search for its matching stringID, and then
569     *               it will return the associate string in the selected language.
570     * @param string $pageID ID of the page/group containing the string
571     *
572     * @return string
573     */
574    function getStringID($string, $pageID = TRANSLATION2_DEFAULT_PAGEID)
575    {
576        $pageID = ($pageID == TRANSLATION2_DEFAULT_PAGEID ? $this->currentPageID : $pageID);
577        return $this->storage->getStringID($string, $pageID);
578    }
579
580    // }}}
581    // {{{ __clone()
582
583    /**
584     * Clone internal object references
585     *
586     * This method is called automatically by PHP5
587     *
588     * @return void
589     * @access protected
590     */
591    function __clone()
592    {
593        $this->storage = clone($this->storage);
594    }
595
596    // }}}
597}
598?>