Skip to content
Snippets Groups Projects
Translate.php 14.9 KiB
Newer Older
 * The translation-relevant bits from our original minimalistic XHTML PHP based template system.
 * @package SimpleSAMLphp
declare(strict_types=1);

namespace SimpleSAML\Locale;

use Gettext\BaseTranslator;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Configuration;
use SimpleSAML\Logger;
use SimpleSAML\Module;

Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    /**
     * The configuration to be used for this translator.
     *
     * @var \SimpleSAML\Configuration
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     */
    private Configuration $configuration;
Tim van Dijen's avatar
Tim van Dijen committed
    /**
     * Associative array of languages.
     *
     * @var array
     */
    private array $langtext = [];

    /**
     * Associative array of dictionaries.
Tim van Dijen's avatar
Tim van Dijen committed
     *
     * @var array
    private array $dictionaries = [];

    /**
     * The default dictionary.
Tim van Dijen's avatar
Tim van Dijen committed
     *
Tim van Dijen's avatar
Tim van Dijen committed
     * @var string|null
    private ?string $defaultDictionary = null;
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    /**
     * The language object we'll use internally.
     *
     * @var \SimpleSAML\Locale\Language
     */
    private Language $language;
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed

Tim van Dijen's avatar
Tim van Dijen committed

    /**
     * Constructor
     *
     * @param \SimpleSAML\Configuration $configuration Configuration object
     * @param string|null $defaultDictionary The default dictionary where tags will come from.
    public function __construct(Configuration $configuration, ?string $defaultDictionary = null)
        $this->configuration = $configuration;
        $this->language = new Language($configuration);
Tim van Dijen's avatar
Tim van Dijen committed
        $this->defaultDictionary = $defaultDictionary;
    /**
     * Return the internal language object used by this translator.
     *
     * @return \SimpleSAML\Locale\Language
     */
    public function getLanguage(): Language
     * This method retrieves a dictionary with the name given.
     * @param string $name The name of the dictionary, as the filename in the dictionary directory, without the
     * '.php' ending.
     * @return array An associative array with the dictionary.
    private function getDictionary(string $name): array
    {
        if (!array_key_exists($name, $this->dictionaries)) {
            $sepPos = strpos($name, ':');
            if ($sepPos !== false) {
                $module = substr($name, 0, $sepPos);
                $fileName = substr($name, $sepPos + 1);
                $dictDir = Module::getModuleDir($module) . '/dictionaries/';
            } else {
                $dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/') ?: 'dictionaries/';
                $fileName = $name;
            }

            $this->dictionaries[$name] = $this->readDictionaryFile($dictDir . $fileName);
        }

        return $this->dictionaries[$name];
    }

     * This method retrieves a tag as an array with language => string mappings.
     * @param string $tag The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve a tag
     * from the specific dictionary.
     * @return array|null An associative array with language => string mappings, or null if the tag wasn't found.
    public function getTag(string $tag): ?array
    {
        // first check translations loaded by the includeInlineTranslation and includeLanguageFile methods
        if (array_key_exists($tag, $this->langtext)) {
            return $this->langtext[$tag];
        }

        // check whether we should use the default dictionary or a dictionary specified in the tag
        if (substr($tag, 0, 1) === '{' && preg_match('/^{((?:\w+:)?\w+?):(.*)}$/D', $tag, $matches)) {
            $dictionary = $matches[1];
            $tag = $matches[2];
        } else {
            $dictionary = $this->defaultDictionary;
            if ($dictionary === null) {
                // we don't have any dictionary to load the tag from
                return null;
            }
        }

        $dictionary = $this->getDictionary($dictionary);
        if (!array_key_exists($tag, $dictionary)) {
            return null;
        }

        return $dictionary[$tag];
    }

    /**
     * Retrieve the preferred translation of a given text.
     *
     * @param array $translations The translations, as an associative array with language => text mappings.
     * @return string The preferred translation.
     * @throws \Exception If there's no suitable translation.
    public function getPreferredTranslation(array $translations): string
    {
        // look up translation of tag in the selected language
        $selected_language = $this->language->getLanguage();
        if (array_key_exists($selected_language, $translations)) {
            return $translations[$selected_language];
        }

        // look up translation of tag in the default language
        $default_language = $this->language->getDefaultLanguage();
        if (array_key_exists($default_language, $translations)) {
            return $translations[$default_language];
        }

        // check for english translation
        if (array_key_exists('en', $translations)) {
            return $translations['en'];
        }

        // pick the first translation available
        if (count($translations) > 0) {
            $languages = array_keys($translations);
            return $translations[$languages[0]];
        }

        // we don't have anything to return
        throw new \Exception('Nothing to return from translation.');
     * Translate the name of an attribute.
     * @param string $name The attribute name.
     * @return string The translated attribute name, or the original attribute name if no translation was found.
    public function getAttributeTranslation(string $name): string
    {
        // normalize attribute name
        $normName = strtolower($name);
        $normName = str_replace([":", "-"], "_", $normName);
        // check for an extra dictionary
        $extraDict = $this->configuration->getString('attributes.extradictionary', null);
        if ($extraDict !== null) {
            $dict = $this->getDictionary($extraDict);
            if (array_key_exists($normName, $dict)) {
                return $this->getPreferredTranslation($dict[$normName]);
        // search the default attribute dictionary
        $dict = $this->getDictionary('attributes');
        if (array_key_exists('attribute_' . $normName, $dict)) {
            return $this->getPreferredTranslation($dict['attribute_' . $normName]);
        // no translations found
    /**
     * Mark a string for translation without translating it.
     *
     * @param string $tag A tag name to mark for translation.
     *
     * @return string The tag, unchanged.
     */
    public static function noop(string $tag): string
     * Include a translation inline instead of putting translations in dictionaries. This function is recommended to be
Tim van Dijen's avatar
Tim van Dijen committed
     * used ONLY for variable data, or when the translation is already provided by an external source, as a database
Tim van Dijen's avatar
Tim van Dijen committed
     * @param string $tag The tag that has a translation
     * @param mixed $translation The translation array
     * @throws \Exception If $translation is neither a string nor an array.
    public function includeInlineTranslation(string $tag, $translation): void
        if (is_string($translation)) {
            $translation = ['en' => $translation];
        } elseif (!is_array($translation)) {
            throw new \Exception(
                "Inline translation should be string or array. Is " . gettype($translation) . " now!"
            );
        Logger::debug('Translate: Adding inline language translation for tag [' . $tag . ']');
        $this->langtext[$tag] = $translation;
    }

     * Include a language file from the dictionaries directory.
     * @param string $file File name of dictionary to include
     * @param \SimpleSAML\Configuration|null $otherConfig Optionally provide a different configuration object than the
     * one provided in the constructor to be used to find the directory of the dictionary. This allows to combine
     * dictionaries inside the SimpleSAMLphp main code distribution together with external dictionaries. Defaults to
     * null.
    public function includeLanguageFile(string $file, Configuration $otherConfig = null): void
        if (!empty($otherConfig)) {
            $filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
        } else {
            $filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
        }
        $filebase = $filebase ?: 'dictionaries/';
        $lang = $this->readDictionaryFile($filebase . $file);
        Logger::debug('Translate: Merging language array. Loading [' . $file . ']');
        $this->langtext = array_merge($this->langtext, $lang);
    }

     * Read a dictionary file in JSON format.
     * @param string $filename The absolute path to the dictionary file, minus the .definition.json ending.
     * @return array An array holding all the translations in the file.
    private function readDictionaryJSON(string $filename): array
        $definitionFile = $filename . '.definition.json';
        Assert::true(file_exists($definitionFile));

        $fileContent = file_get_contents($definitionFile);
        $lang = json_decode($fileContent, true);

        if (empty($lang)) {
            Logger::error('Invalid dictionary definition file [' . $definitionFile . ']');
        $translationFile = $filename . '.translation.json';
        if (file_exists($translationFile)) {
            $fileContent = file_get_contents($translationFile);
            $moreTrans = json_decode($fileContent, true);
            if (!empty($moreTrans)) {
                $lang = array_merge_recursive($lang, $moreTrans);
    /**
     * Read a dictionary file in PHP format.
     *
     * @param string $filename The absolute path to the dictionary file.
     * @return array An array holding all the translations in the file.
    private function readDictionaryPHP(string $filename): array
        $phpFile = $filename . '.php';
        Assert::true(file_exists($phpFile));
        include($phpFile);
        /** @psalm-var array|null $lang */
        if (isset($lang)) {
            return $lang;
        }

    /**
     * Read a dictionary file.
     *
     * @param string $filename The absolute path to the dictionary file.
     * @return array An array holding all the translations in the file.
    private function readDictionaryFile(string $filename): array
        Logger::debug('Translate: Reading dictionary [' . $filename . ']');
        $jsonFile = $filename . '.definition.json';
        if (file_exists($jsonFile)) {
            return $this->readDictionaryJSON($filename);
        }

        $phpFile = $filename . '.php';
        if (file_exists($phpFile)) {
            return $this->readDictionaryPHP($filename);
        }

            $_SERVER['PHP_SELF'] . ' - Translate: Could not find dictionary file at [' . $filename . ']'
Tim van Dijen's avatar
Tim van Dijen committed
    /**
     * Translate a singular text.
     *
     * @param string|null $original The string before translation.
Tim van Dijen's avatar
Tim van Dijen committed
     *
     * @return string The translated string.
     */
    public static function translateSingularGettext(?string $original): string
        // This may happen if you forget to set a variable and then run undefinedVar through the trans-filter
        $original = $original ?? 'undefined variable';

        $text = BaseTranslator::$current->gettext($original);
Tim van Dijen's avatar
Tim van Dijen committed
        if (func_num_args() === 1) {
            return $text;
        }

        $args = array_slice(func_get_args(), 1);

        return strtr($text, is_array($args[0]) ? $args[0] : $args);
    }

Tim van Dijen's avatar
Tim van Dijen committed
    /**
     * Translate a plural text.
     *
     * @param string|null $original The string before translation.
Tim van Dijen's avatar
Tim van Dijen committed
     * @param string $plural
     * @param string $value
     *
     * @return string The translated string.
     */
    public static function translatePluralGettext(?string $original, string $plural, string $value): string
        // This may happen if you forget to set a variable and then run undefinedVar through the trans-filter
        $original = $original ?? 'undefined variable';

        $text = BaseTranslator::$current->ngettext($original, $plural, $value);

        if (func_num_args() === 3) {
            return $text;
        }

        $args = array_slice(func_get_args(), 3);

        return strtr($text, is_array($args[0]) ? $args[0] : $args);
    }
    /**
     * Pick a translation from a given array of translations for the current language.
     *
     * @param array|null $context An array of options. The current language must be specified
     *     as an ISO 639 code accessible with the key "currentLanguage" in the array.
     * @param array|null $translations An array of translations. Each translation has an
     *     ISO 639 code as its key, identifying the language it corresponds to.
     *
     * @return null|string The translation appropriate for the current language, or null if none found. If the
     * $context or $translations arrays are null, or $context['currentLanguage'] is not defined, null is also returned.
     */
    public static function translateFromArray(?array $context, ?array $translations): ?string
        if (!is_array($translations)) {
        } elseif (!is_array($context) || !isset($context['currentLanguage'])) {
        } elseif (isset($translations[$context['currentLanguage']])) {
            return $translations[$context['currentLanguage']];
        }

        // we don't have a translation for the current language, load alternative priorities
        $sspcfg = Configuration::getInstance();
Tim van Dijen's avatar
Tim van Dijen committed
        /** @psalm-var \SimpleSAML\Configuration $langcfg */
        $langcfg = $sspcfg->getConfigItem('language');
        $priorities = $langcfg->getArray('priorities', []);
Tim van Dijen's avatar
Tim van Dijen committed
        if (!empty($priorities[$context['currentLanguage']])) {
            foreach ($priorities[$context['currentLanguage']] as $lang) {
                if (isset($translations[$lang])) {
                    return $translations[$lang];
                }
            }
        }

        // nothing we can use, return null so that we can set a default
        return null;
    }

    /**
     * Prefix tag
     *
     * @param string $tag Translation tag
     * @param string $prefix Prefix to be added
     *
     * @return string Prefixed tag
     */
    public static function addTagPrefix(string $tag, string $prefix): string
    {
        $tagPos = strrpos($tag, ':');
        // if tag contains ':' target actual tag
        $tagPos = ($tagPos === false) ? 0 : $tagPos + 1;
        // add prefix at $tagPos
        return substr_replace($tag, $prefix, $tagPos, 0);
    }