Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
Localization.php 6.99 KiB
<?php

/**
 * Glue to connect one or more translation/locale systems to the rest
 *
 * @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
 * @package SimpleSAMLphp
 */

namespace SimpleSAML\Locale;

use Gettext\Translations;
use Gettext\Translator;

class Localization
{

    /**
     * The configuration to use.
     *
     * @var \SimpleSAML_Configuration
     */
    private $configuration;

    /**
     * The default gettext domain.
     */
    const DEFAULT_DOMAIN = 'messages';

    /**
     * Old internationalization backend included in SimpleSAMLphp.
     */
    const SSP_I18N_BACKEND = 'SimpleSAMLphp';

    /**
     * An internationalization backend implemented purely in PHP.
     */
    const GETTEXT_I18N_BACKEND = 'gettext/gettext';

    /*
     * The default locale directory
     */
    private $localeDir;

    /*
     * Where specific domains are stored
     */
    private $localeDomainMap = array();

    /*
     * Pointer to currently active translator
     */
    private $translator;


    /**
     * Constructor
     *
     * @param \SimpleSAML_Configuration $configuration Configuration object
     */
    public function __construct(\SimpleSAML_Configuration $configuration)
    {
        $this->configuration = $configuration;
        $this->localeDir = $this->configuration->resolvePath('locales');
        $this->language = new Language($configuration);
        $this->langcode = $this->language->getPosixLanguage($this->language->getLanguage());
        $this->i18nBackend = $this->configuration->getString('language.i18n.backend', self::SSP_I18N_BACKEND);
        $this->setupL10N();
    }

    /**
     * Dump the default locale directory
     */
    public function getLocaleDir()
    {
        return $this->localeDir;
    }


    /**
     * Get the default locale dir for a specific module aka. domain
     *
     * @param string $domain Name of module/domain
     */
    public function getDomainLocaleDir($domain)
    {
        $localeDir = $this->configuration->resolvePath('modules') . '/' . $domain . '/locales';
        return $localeDir;
    }


    /*
     * Add a new translation domain from a module
     * (We're assuming that each domain only exists in one place)
     *
     * @param string $module Module name
     * @param string $localeDir Absolute path if the module is housed elsewhere
     */
    public function addModuleDomain($module, $localeDir = null)
    {
        if (!$localeDir) {
            $localeDir = $this->getDomainLocaleDir($module);
        }
        $this->addDomain($localeDir, $module);
    }


    /*
     * Add a new translation domain
     * (We're assuming that each domain only exists in one place)
     *
     * @param string $localeDir Location of translations
     * @param string $domain Domain at location
     */
    public function addDomain($localeDir, $domain)
    {
        $this->localeDomainMap[$domain] = $localeDir;
        \SimpleSAML\Logger::debug("Localization: load domain '$domain' at '$localeDir'");
        $this->loadGettextGettextFromPO($domain);
    }

    /*
     * Get and check path of localization file
     *
     * @param string $domain Name of localization domain
     * @throws Exception If the path does not exist even for the default, fallback language
     */
    public function getLangPath($domain = self::DEFAULT_DOMAIN)
    {
        $langcode = explode('_', $this->langcode);
        $langcode = $langcode[0];
        $localeDir = $this->localeDomainMap[$domain];
        $langPath = $localeDir.'/'.$langcode.'/LC_MESSAGES/';
        \SimpleSAML\Logger::debug("Trying langpath for '$langcode' as '$langPath'");
        if (is_dir($langPath) && is_readable($langPath)) {
            return $langPath;
        }

        // Language not found, fall back to default
        $defLangcode = $this->language->getDefaultLanguage();
        $langPath = $localeDir.'/'.$defLangcode.'/LC_MESSAGES/';
        if (is_dir($langPath) && is_readable($langPath)) {
            // Report that the localization for the preferred language is missing
            $error = "Localization not found for langcode '$langcode' at '$langPath', falling back to langcode '".
                     $defLangcode."'";
            \SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
            return $langPath;
        }

        // Locale for default language missing even, error out
        $error = "Localization directory missing/broken for langcode '$langcode' and domain '$domain'";
        \SimpleSAML\Logger::critical($_SERVER['PHP_SELF'].' - '.$error);
        throw new \Exception($error);
    }


    /**
     * Setup the translator
     */
    private function setupTranslator()
    {
        $this->translator = new Translator();
        $this->translator->register();
    }


    /**
     * Load translation domain from Gettext/Gettext using .po
     *
     * Note: Since Twig I18N does not support domains, all loaded files are
     * merged. Use contexts if identical strings need to be disambiguated.
     *
     * @param string $domain Name of domain
     * @param boolean $catchException Whether to catch an exception on error or return early
     *
     * @throws \Exception If something is wrong with the locale file for the domain and activated language
     */
    private function loadGettextGettextFromPO($domain = self::DEFAULT_DOMAIN, $catchException = true)
    {
        try {
            $langPath = $this->getLangPath($domain);
        } catch (\Exception $e) {
            $error = "Something wrong with path '$langPath', cannot load domain '$domain'";
            \SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
            if ($catchException) {
                // bail out!
                return;
            } else {
                throw $e;
            }
        }
        $poFile = $domain.'.po';
        $poPath = $langPath.$poFile;
        if (file_exists($poPath) && is_readable($poPath)) {
            $translations = Translations::fromPoFile($poPath);
            $this->translator->loadTranslations($translations);
        } else {
            $error = "Localization file '$poFile' not found in '$langPath', falling back to default";
            \SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
        }
    }


    /**
     * Test to check if backend is set to default
     *
     * (if false: backend unset/there's an error)
     */
    public function isI18NBackendDefault()
    {
        if ($this->i18nBackend === $this::SSP_I18N_BACKEND) {
            return true;
        }
        return false;
    }


    /**
     * Set up L18N if configured or fallback to old system
     */
    private function setupL10N()
    {
        if ($this->i18nBackend === self::SSP_I18N_BACKEND) {
            \SimpleSAML\Logger::debug("Localization: using old system");
            return;
        }

        $this->setupTranslator();
        // setup default domain
        $this->addDomain($this->localeDir, self::DEFAULT_DOMAIN);
    }

    /**
     * Show which domains are registered
     */
    public function getRegisteredDomains()
    {
        return $this->localeDomainMap;
    }

}