atelierspierrot/internationalization

View on GitHub
src/I18n/I18n.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * This file is part of the Internationalization package.
 *
 * Copyright (c) 2010-2016 Pierre Cassat <me@e-piwi.fr> and contributors
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The source code of this package is available online at 
 * <http://github.com/atelierspierrot/internationalization>.
 */

namespace I18n;

use \Locale;
use \NumberFormatter;
use \IntlDateFormatter;
use \DateTime;
use \Patterns\Abstracts\AbstractSingleton;
use \Patterns\Interfaces\TranslatableInterface;

/**
 * Internationalization class
 *
 * For more information, see:
 * -   <http://www.unicode.org/reports/tr35/>
 * -   <http://userguide.icu-project.org/locale>
 * -   <http://userguide.icu-project.org/formatparse/datetime>
 * -   <http://www.php.net/manual/en/book.intl.php>
 *
 * @author  piwi <me@e-piwi.fr>
 */
class I18n
    extends AbstractSingleton
    implements TranslatableInterface
{

    /**
     * @var \I18n\LoaderInterface The loader object
     */
    protected $loader;

    /**
     * @var string The current language code
     */
    protected $lang;

    /**
     * @var string The current timezone code
     */
    protected $timezone;

    /**
     * @var array The translated strings in the current language code
     */
    protected $language_strings;

    /**
     * An array to cache translated strings once they are loaded in a language
     * to avoid parsing the same db file more than once
     * @var array
     */
    private $language_strings_cache = array();

// --------------------
// Construct / Destruct / Clone
// --------------------

    /**
     * Initialization : the true constructor
     *
     * @param   \I18n\LoaderInterface  $loader
     * @param   string $lang A language code to use by default
     * @param   string $timezone A timezone code to use by default
     * @return  self
     * @throws  \I18n\I18nException if no default locale is defined and the `$lang` argument
     *          is empty, and if no default timezone is defined and the `$timezone` argument
     *          is empty
     */
    protected function init(LoaderInterface $loader, $lang = null, $timezone = null)
    {
        $this->setLoader($loader);
        if (is_null($lang)) {
            $lang = substr(Locale::getDefault(), 0, 2);
            if (empty($lang)) {
                throw new I18nException(
                    'No default locale defined in your system, please define the second argument of method "I18n::getInstance()"!'
                );
            }
        }

        $this->setLanguage($lang, false, $this->getLoader()->getOption('force_rebuild'));
        if (is_null($timezone)) {
            $timezone = @date_default_timezone_get();
            if (empty($timezone)) {
                throw new I18nException(
                    'No default timezone defined in your system, please define the third argument of method "I18n::getInstance()"!'
                );
            }
        }
        $this->setTimezone($timezone);

        if (empty($this->language_strings)) {
            $this->setLanguage(
                $this->getLoader()->getOption('default_language', 'en'),
                false,
                $this->getLoader()->getOption('force_rebuild')
            );
        }

        return $this;
    }

    /**
     * Load the current language strings
     *
     * This will parse the current language db files, or take it from the cache if so and load
     * the strings table in `$this->language_strings`.
     *
     * @param   bool $throw_errors Throw errors while re-creating the databases (default is `false`)
     * @param   bool $force_rebuild Force the system to rebuild the databases using `I18n\Generator` if it does not exist (default is `false`)
     * @param   bool $force_rebuild_on_update Force the system to rebuild the databases using `I18n\Generator` if it's more recent than language strings files (default is `false`)
     * @return  self
     * @throws  \I18n\I18nException if the database file seems to be malformed, and a
     * @throws  \I18n\I18nInvalidArgumentException it the file can't be found
     */
    protected function _loadLanguageStrings($throw_errors = true, $force_rebuild = false, $force_rebuild_on_update = false)
    {
        $_fn = $this->getLoader()->buildLanguageFileName($this->lang);
        $_f = $this->getLoader()->buildLanguageFilePath($this->lang);

        if (
            isset($this->language_strings_cache[$_f]) &&
            isset($this->language_strings_cache[$this->lang])
        ) {
            $this->language_strings = $this->language_strings_cache[$this->lang];
            return $this;
        }

        $db_f = $this->getLoader()->buildLanguageDBFilePath();
        if (@file_exists($_f)) {
            if ($force_rebuild_on_update && filemtime($db_f)>filemtime($_f)) {
                return $this
                    ->_rebuildLanguageStringsFiles()
                    ->_loadLanguageStrings();
            } else {
                $_v = $this->getLoader()->buildLanguageVarname($this->lang);
                include $_f;
                if (isset($$_v)) {
                    if (!isset($this->language_strings_cache[$this->lang])) {
                        $this->language_strings_cache[$this->lang] = array();
                    }
                    $this->language_strings_cache[$this->lang] =
                        array_merge($this->language_strings_cache[$this->lang], $$_v);
                    $this->language_strings_cache[$_f] = array(
                        'language'  => $this->lang,
                        'strings'   => $$_v
                    );
                    return $this->_loadLanguageStrings();
                } else {
                    throw new I18nException(
                        sprintf('Language strings seems to be malformed in file "%s" (must be a classic array like "string_index"=>"string in the language")!', $_fn)
                    );
                }
            }
        } elseif ($force_rebuild) {
            return $this
                ->_rebuildLanguageStringsFiles()
                ->_loadLanguageStrings();
        } elseif ($throw_errors) {
            throw new I18nInvalidArgumentException(
                sprintf('Language strings file for language code "%s" not found (searched in "%s")!', $this->lang, $_f)
            );
        }

        return $this;
    }

    /**
     * Rebuild the language strings databases with `I18n\Generator`
     *
     * @return self
     */
    protected function _rebuildLanguageStringsFiles()
    {
        $db_filepath = $this->getLoader()->buildLanguageDBFilePath();
        $generator = new Generator($db_filepath);
        $generator->generate();
        return $this;
    }

// --------------------
// Getters / Setters / Checkers / Builders
// --------------------

    /**
     * Store the loader
     *
     * @param   \I18n\LoaderInterface $loader
     * @return  self
     */
    public function setLoader(LoaderInterface $loader)
    {
        $this->loader = $loader;
        return $this;
    }

    /**
     * Gets the loader
     *
     * @return \I18n\LoaderInterface
     */
    public function getLoader()
    {
        return $this->loader;
    }

    /**
     * Load a new language file
     * @param   string          $file_name
     * @param   null|string     $dir_name
     * @throws  \I18n\I18nException
     * @throws  \Exception
     */
    public function loadFile($file_name, $dir_name = null)
    {
        try {
            $loader = $this->getLoader();
            $file_path = $loader->findLanguageDBFile(
                !empty($file_name) ? basename($file_name) : $file_name,
                !empty($dir_name) ? $dir_name : (
                    !empty($file_name) ? dirname($file_name) : $dir_name
                )
            );
            if ($file_path && file_exists($file_path)) {
                $loader
                    ->setOption('language_strings_db_directory', dirname($file_path))
                    ->setOption('language_strings_db_filename', basename($file_path))
                ;

                $this->_loadLanguageStrings(true, true, true);
            } else {
                throw new I18nInvalidArgumentException(
                    sprintf('Language file "%s" not found!', $file_name)
                );
            }
        } catch (I18nException $e) {
            throw $e;
        }
    }

    /**
     * Loads a new language
     *
     * This will actually define a new default Locale and load the language strings database.
     *
     * @param   string $lang A language code to use by default
     * @param   bool $throw_errors Throw errors while re-creating the databases (default is `false`)
     * @param   bool $force_rebuild Force the system to rebuild the databases using `I18n\Generator` (default is `false`)
     * @return  self Returns `$this` for method chaining
     * @throws  \I18n\I18nInvalidArgumentException it the language is not available
     */
    public function setLanguage($lang, $throw_errors = true, $force_rebuild = false)
    {
        if ($this->isAvailableLanguage($lang)) {
            $this->lang = $lang;
            $this->setLocale($this->getAvailableLocale($lang));
            $this->_loadLanguageStrings($throw_errors, $force_rebuild);
        } else {
            throw new I18nInvalidArgumentException(
                sprintf('Language "%s" is not available in the application (available languages are %s)!',
                    $lang, join(', ', $this->getLoader()->getOption('available_languages')))
            );
        }
        return $this;
    }

    /**
     * Get the current language code used
     *
     * @return string The current language code in use
     */
    public function getLanguage()
    {
        return $this->lang;
    }
    
    /**
     * Try to get the browser default locale and use it
     *
     * @return self
     */
    public function setDefaultFromHttp()
    {
        $http_locale = $this->getHttpHeaderLocale();
        if (!empty($http_locale) && $this->isAvailableLanguage($http_locale)) {
            $this->setLanguage($http_locale);
        }
        return $this;
    }
    
    /**
     * Check if a language code is available in the Loader
     *
     * @param string $lang A language code to use by default
     * @return bool Returns `true` if the language code exists, `false` otherwise
     */
    public function isAvailableLanguage($lang)
    {
        return array_key_exists($lang, $this->getLoader()->getOption('available_languages')) ||
            in_array($lang, $this->getLoader()->getOption('available_languages'));
    }
    
    /**
     * Get the full locale info for a language code
     *
     * This will look in the `Loader::available_languages` option to retrieve the full
     * locale string corresponding to a language code.
     *
     * @param string $lang A language code to use by default
     * @return null|string The full locale string if found
     */
    public function getAvailableLocale($lang)
    {
        $langs = $this->getLoader()->getOption('available_languages');
        if (array_key_exists($lang, $langs)) {
            return $langs[$lang];
        } elseif (in_array($lang, $langs)) {
            return $langs[array_search($lang, $langs)];
        }
        return null;
    }
    
    /**
     * Get the list of `Loader::available_languages`
     *
     * @return array The array defined in the Loader
     */
    public function getAvailableLanguages()
    {
        return $this->getLoader()->getOption(
            'available_languages',
            array($this->getLoader()->getOption('default_language', 'en'))
        );
    }
    
    /**
     * Define a new locale for the system
     *
     * @param string $locale The full locale string to define
     * @return self Returns `$this` for method chaining
     */
    public function setLocale($locale)
    {
        Locale::setDefault($locale);
        return $this;
    }
    
    /**
     * Get the current locale used by the system
     *
     * @return string The full locale string currently in use
     */
    public function getLocale()
    {
        return Locale::getDefault();
    }
    
    /**
     * Define a new timezone for the system
     *
     * @param string $timezone The full timezone string to define
     * @return self
     */
    public function setTimezone($timezone)
    {
        $this->timezone = $timezone;
        date_default_timezone_set($this->timezone);
        return $this;
    }
    
    /**
     * Get the current timezone used by the system
     *
     * @return string The full timezone string currently in use
     */
    public function getTimezone()
    {
        return $this->timezone;
    }
    
    /**
     * Check if a translation exists for an index
     *
     * @param string $index The original string or index to find translation of
     * @return bool Returns `true` if the translation exists, `false` otherwise
     */
    public function hasLocalizedString($index)
    {
        return isset($this->language_strings[$index]);
    }
    
    /**
     * Get the translation of an index
     *
     * @param string $index The original string or index to find translation of
     * @return string The translation found if so, `$index` otherwise
     */
    public function getLocalizedString($index)
    {
        return isset($this->language_strings[$index]) ?  $this->language_strings[$index] : $index;
    }

    /**
     * Parse a translated string making some parameters replacements
     *
     * @param string $str The original string to work on
     * @param array $arguments A table of arguments to replace, like `var=>val` pairs for
     *          the replacement of `%var%` by `val` in the final string (by default)
     * @return string The string with replacements
     */
    public function parseString($str, array $arguments = null)
    {
        if (empty($arguments)) {
            return $str;
        }
        $arg_mask = $this->getLoader()->getOption('arg_wrapper_mask');
        foreach ($arguments as $name=>$value) {
            if (is_string($name)) {
                $str = strtr($str, array( sprintf($arg_mask, $name) => $value));
            } else {
                $str = sprintf($str, self::translate($value));
            }
        }
        return $str;
    }

    /**
     * Get the meta-data of a language string
     *
     * @param   string  $str
     * @return  array   array( (string) $str , (array) $info )
     */
    public function parseStringMetadata($str)
    {
        $info = array();
        if (0!==preg_match('~^\[([^\]]+)\]~i', $str, $matches)) {
            $str = str_replace($matches[0], '', $str);
            $types = $matches[1];
            $types_items = explode(';', $types);
            foreach ($types_items as $item) {
                $parts = explode(':', $item);
                if (count($parts)>1) {
                    $info[$parts[0]] = $parts[1];
                } else {
                    $info[] = $parts[0];
                }
            }
        }
        return array($str, $info);
    }

    /**
     * Special function to show a string when its translation doesn't exist
     *
     * This will return the original string with its arguments if so.
     *
     * @param string $str The original string to work on
     * @param array $args A table of arguments to replace
     * @return string The string dumped to identify untranslated strings
     */
    protected function _showUntranslated($str = '', array $args = null)
    {
        $arguments = '';
        if (!empty($args)) {
            foreach ($args as $var=>$val) {
                $arguments .= '"'.$var.'" => '.str_replace(array("\n", "\t"), '', var_export($val, 1)).', ';
            }
        }
        return sprintf($this->getLoader()->getOption('show_untranslated_wrapper', '%s'), $str, $arguments);
    }

// --------------------
// Names, currencies and codes getters
// --------------------

    /**
     * Get the currency of the current locale
     *
     * @param string $lang The language to use
     * @return string The currency code for the current locale
     */
    public function getCurrency($lang = null)
    {
        if (!is_null($lang)) {
            $original_lang = $this->getLanguage();
            $this->setLanguage($lang);
        }
        $formatter = new NumberFormatter($this->getLocale(), NumberFormatter::CURRENCY);
        $currency = $formatter->getTextAttribute(NumberFormatter::CURRENCY_CODE);
        if (!empty($original_lang)) {
            $this->setLanguage($original_lang);
        }
        return $currency;
    }
    
    /**
     * Get the browser requested locale if so
     *
     * @return string The browser prefered locale
     */
    public function getHttpHeaderLocale()
    {
        return Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
    }

    /**
     * Get the full list of `Loader::available_languages` option like human readable names
     *
     * @param string $lang The language to use
     * @return array The available languages table with the value as the human readable description
     *          of the corresponding locale
     */
    public function getAvailableLanguagesNames($lang = null)
    {
        if (!is_null($lang)) {
            $original_lang = $this->getLanguage();
            $this->setLanguage($lang);
        }
        $db = $this->getLoader()->getOption('available_languages');
        $full_locales = array();
        if (!empty($db)) {
            foreach ($db as $ln=>$locale) {
                $full_locales[$ln] = Locale::getDisplayName($locale);
            }
        }
        if (!empty($original_lang)) {
            $this->setLanguage($original_lang);
        }
        return $full_locales;
    }
    
    /**
     * Get the language code of the current locale
     *
     * @param string $lang The language to use
     * @return string The language code for the current locale
     */
    public function getLanguageCode($lang = null)
    {
        $parts = $this->_callInternalLocale('parseLocale', null, $lang, false);
        return isset($parts['language']) ? $parts['language'] : null;
    }
    
    /**
     * Get the region code of the current locale
     *
     * @param string $lang The language to use
     * @return string The region code for the current locale
     */
    public function getRegionCode($lang = null)
    {
        return $this->_callInternalLocale('getRegion', null, $lang, false);
    }
    
    /**
     * Get the script code of the current locale
     *
     * @param string $lang The language to use
     * @return string The script code for the current locale
     */
    public function getScriptCode($lang = null)
    {
        return $this->_callInternalLocale('getScript', null, $lang, false);
    }
    
    /**
     * Get the keywords of the current locale
     *
     * @param string $lang The language to use
     * @return array The keywords for the current locale
     */
    public function getKeywords($lang = null)
    {
        return $this->_callInternalLocale('getKeywords', null, $lang, false);
    }
    
    /**
     * Get one keyword value of the current locale
     *
     * @param string $keyword The keyword to search
     * @param string $lang The language to use
     * @return null|string The keyword value for the current locale if found
     */
    public function getKeyword($keyword, $lang = null)
    {
        $parts = $this->_callInternalLocale('getKeywords', null, $lang, false);
        return isset($parts[$keyword]) ? $parts[$keyword] : null;
    }

    /**
     * Get the primary language of a locale
     *
     * @param string $for_locale The locale to work on, by default this will be the current locale
     * @param string $lang The language to use, by default this will be the current locale
     * @return string The primary language for the locale in the requested language
     */
    public function getPrimaryLanguage($for_locale = null, $lang = null)
    {
        return $this->_callInternalLocale('getPrimaryLanguage', $for_locale, $lang);
    }

    /**
     * Get the language name of a locale
     *
     * @param string $for_locale The locale to work on, by default this will be the current locale
     * @param string $lang The language to use, by default this will be the current locale
     * @return string The language name for the locale in the requested language
     */
    public function getLanguageName($for_locale = null, $lang = null)
    {
        return $this->_callInternalLocale('getDisplayLanguage', $for_locale, $lang);
    }
    
    /**
     * Get the country name of a locale
     *
     * @param string $for_locale The locale to work on, by default this will be the current locale
     * @param string $lang The language to use, by default this will be the current locale
     * @return string The country name for the locale in the requested language
     */
    public function getCountryName($for_locale = null, $lang = null)
    {
        return $this->_callInternalLocale('getDisplayRegion', $for_locale, $lang);
    }
    
    /**
     * Get the script name of a locale
     *
     * @param string $for_locale The locale to work on, by default this will be the current locale
     * @param string $lang The language to use, by default this will be the current locale
     * @return string The script name for the locale in the requested language
     */
    public function getLocaleScript($for_locale = null, $lang = null)
    {
        return $this->_callInternalLocale('getDisplayScript', $for_locale, $lang);
    }
    
    /**
     * Get the variant name of a locale
     *
     * @param string $for_locale The locale to work on, by default this will be the current locale
     * @param string $lang The language to use, by default this will be the current locale
     * @return string The variant name for the locale in the requested language
     */
    public function getLocaleVariant($for_locale = null, $lang = null)
    {
        return $this->_callInternalLocale('getDisplayVariant', $for_locale, $lang);
    }
    
    /**
     * Internally factorize the Locale methods
     *
     * @param   string $fct_name The name of the method to execute on `Locale`
     * @param   string $for_locale The locale to work on, by default this will be the current locale
     * @param   string $lang The language to use, by default this will be the current locale
     * @param   bool $do_array Do we have to pass both `$for_locale` and `$lang` arguments (default is `true`)
     * @return  mixed The result of `Locale:: $fct_name ( $for_locale , $lang )`
     */
    protected function _callInternalLocale($fct_name, $for_locale = null, $lang = null, $do_array = true)
    {
        if (!is_null($lang)) {
            $original_lang = $this->getLanguage();
            $this->setLanguage($lang);
        }
        $locale = !is_null($for_locale) ? $this->getAvailableLocale($for_locale) : $this->getLocale();
        $arguments = $do_array ? array($locale, $this->getLocale()) : array($locale);
        $str = call_user_func_array(array('Locale', $fct_name), $arguments);
        if (!empty($original_lang)) {
            $this->setLanguage($original_lang);
        }
        return $str;
    }

// --------------------
// Utilities formatter
// --------------------

    /**
     * Get a localized number value
     *
     * This is called by aliases `_N` and `numberify`.
     *
     * @param   int $number The number value to parse
     * @param   string $lang The language to use, by default this will be the current locale
     * @return  string The number value written in the locale
     * @see     _N()
     * @see     numberify()
     */
    public static function getLocalizedNumberString($number, $lang = null)
    {
        $_this = self::getInstance();
        if (!is_null($lang)) {
            $original_lang = $_this->getLanguage();
            $_this->setLanguage($lang);
        }
        $formatter = new NumberFormatter($_this->getLocale(), NumberFormatter::DEFAULT_STYLE);
        $str =  $formatter->format($number);
        if (!empty($original_lang)) {
            $_this->setLanguage($original_lang);
        }
        return $str;
    }
    
    /**
     * Get a localized price value
     *
     * This is called by aliases `_C` and `currencify`.
     *
     * @param   int $number The price value to parse
     * @param   string $lang The language to use, by default this will be the current locale
     * @return  string The price value written in the locale with a currency sign
     * @see     _C()
     * @see     currencify()
     */
    public static function getLocalizedPriceString($number, $lang = null)
    {
        $_this = self::getInstance();
        if (!is_null($lang)) {
            $original_lang = $_this->getLanguage();
            $_this->setLanguage($lang);
        }
        $formatter = new NumberFormatter($_this->getLocale(), NumberFormatter::CURRENCY);
        $str = $formatter->format($number);
        if (!empty($original_lang)) {
            $_this->setLanguage($original_lang);
        }
        return $str;
    }

    /**
     * Get a localized date value
     *
     * This is called by aliases `_D` and `datify`.
     *
     * @param   \DateTime $date The date value to parse as a `DateTime` object
     * @param   string $mask    A mask to use for date writing (by default, the `datetime_mask_icu` translation
     *                          string will be used, or no mask at all if it is not defined)
     * @param   string $charset The charset to use (default is `utf-8`)
     * @param   string $lang The language to use, by default this will be the current locale
     * @return  string The date value written in the locale
     * @see     _D()
     * @see     datify()
     */
    public static function getLocalizedDateString(DateTime $date, $mask = null, $charset = 'UTF-8', $lang = null)
    {
        $_this = self::getInstance();
        if (!is_null($lang)) {
            $original_lang = $_this->getLanguage();
            $_this->setLanguage($lang);
        }

        if (is_null($mask)) {
            $mask = $_this->getLocalizedString('datetime_mask_icu');
        }
        if (!empty($mask)) {
            $fmt = new IntlDateFormatter($_this->getLocale(), IntlDateFormatter::FULL, IntlDateFormatter::FULL,
                $_this->getTimezone(), IntlDateFormatter::GREGORIAN, $mask);
        } else {
            $fmt = new IntlDateFormatter($_this->getLocale(), IntlDateFormatter::FULL, IntlDateFormatter::FULL,
                $_this->getTimezone(), IntlDateFormatter::GREGORIAN);
        }
        $str = $fmt->format($date);

/*
        if (is_null($mask)) $mask = $_this->getLocalizedString('datetime_mask');
        if (empty($mask)) $mask = '%a %e %m %Y %H:%M:%S';        
        setlocale(LC_TIME, $_this->getLocale().'.'.strtoupper($charset));
        $str = strftime($mask , strtotime($date->format('Y-m-d H:i:s')));
*/
        if (!empty($original_lang)) {
            $_this->setLanguage($original_lang);
        }
        return $str;
    }

// --------------------
// Translator
// --------------------

    /**
     * Process a translation with arguments
     *
     * This is the core method of the class: it searches the translation of `$index` in the
     * current language, rebuilds it replacing the keys of `$args` by their values and returns
     * the corresponding localized translated string.
     *
     * This is called by aliases `_T` and `translate`.
     *
     * @param   string $index The index of the translation
     * @param   array $args The optional array of arguments for the final string replacements
     * @param   string $lang The language code to load translation from
     * @return  string Returns the translated string if it exists in the current language, with variables replacements
     * @see     _T()
     * @see     translate()
     */
    public static function translate($index, array $args = null, $lang = null)
    {
        $_this = self::getInstance();
        if (!is_null($lang)) {
            $original_lang = $_this->getLanguage();
            $_this->setLanguage($lang);
        }
        if ($_this->hasLocalizedString($index)) {
            $str = $_this->getLocalizedString($index);
            list($str_tmp, $meta) = $_this->parseStringMetadata($str);
            if (!empty($meta)) {
                $str = $str_tmp;
//                var_export($meta);
            }
            $str = $_this->parseString($str, $args);
        } elseif ($_this->loader->getOption('show_untranslated', false)) {
            $str = $_this->_showUntranslated($index, $args);
        } else {
            $str = $index;
        }
        if (!empty($original_lang)) {
            $_this->setLanguage($original_lang);
        }
        return $str;
    }

    /**
     * Process a translation with arguments depending on a counter
     *
     * This will first choose in `$indexes` the corresponding value for the counter `$number`
     * and then process the translation of the chosen string with arguments.
     *
     * This is called by aliases `_P` and `pluralize`.
     *
     * @param   array $indexes An array of translation strings indexes to choose in considering the counter value
     * @param   int $number The value of the counter to consider
     * @param   array $args The optional array of arguments for the final string replacements
     * @param   string $lang The language code to load translation from
     * @return  string Returns the translated string fot the counter value if it exists, in the current language and with variables replacements
     * @see     _P()
     * @see     pluralize()
     */
    public static function pluralize(array $indexes = array(), $number = 0, array $args = null, $lang = null)
    {
        $_this = self::getInstance();
        $args['nb'] = $number;
        if (isset($indexes[$number])) {
            return self::translate($indexes[$number], $args, $lang);
        }
        $str = ($number<=1) ? array_shift($indexes) : end($indexes);
        return self::translate($str, $args, $lang);
    }
}