diff --git a/.gitignore b/.gitignore index 82d40ffa6f39a1a7c3fc313b74280f702f3a1ae4..4bf939795f548adcaeec26cc91a876ffd8a4cd6b 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,6 @@ crashlytics-build.properties ### Vagrant ### .vagrant/ + +### vim +*.swp diff --git a/composer.json b/composer.json index 31f803360d68a1fc2583c1a73d1f0f154cbcea95..5d7dc544e7e41ae6d091c25305a1d73f7bc0cf12 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,9 @@ "simplesamlphp/saml2": "dev-master#00e38f85b417be1e10a2d738dd2f5ea82edb472c as 2.2", "robrichards/xmlseclibs": "~2.0", "whitehat101/apr1-md5": "~1.0", - "twig/twig": "~1.0" + "twig/twig": "~1.0", + "gettext/gettext": "^3.5", + "jaimeperez/twig-configurable-i18n": "^1.0" }, "require-dev": { "ext-pdo_sqlite": "*", diff --git a/composer.lock b/composer.lock index 0057d0491322bff0149fe87e410f40061b11221b..d6afb5e568ea776e158a0c3277f992069630d5f0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,193 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "ed63813d37583f7d6f49cd1c6a5a6520", - "content-hash": "7e8b2b66445e444fe46b7a79f34e19ff", + "hash": "24d2dd3eba19ca3565300d1b9398db90", + "content-hash": "6d5a31100702a21a90920649a249e682", "packages": [ + { + "name": "gettext/gettext", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/oscarotero/Gettext.git", + "reference": "cd3be64443551e3a693117c4bccbe53e36282456" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/cd3be64443551e3a693117c4bccbe53e36282456", + "reference": "cd3be64443551e3a693117c4bccbe53e36282456", + "shasum": "" + }, + "require": { + "gettext/languages": "2.*", + "php": ">=5.3.0" + }, + "require-dev": { + "illuminate/view": "*", + "symfony/yaml": "~2", + "twig/extensions": "*", + "twig/twig": "*" + }, + "suggest": { + "illuminate/view": "Is necessary if you want to use the Blade extractor", + "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator", + "twig/extensions": "Is necessary if you want to use the Twig extractor", + "twig/twig": "Is necessary if you want to use the Twig extractor" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "PHP gettext manager", + "homepage": "https://github.com/oscarotero/Gettext", + "keywords": [ + "JS", + "gettext", + "i18n", + "mo", + "po", + "translation" + ], + "time": "2016-08-01 18:09:57" + }, + { + "name": "gettext/languages", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git", + "reference": "c43ade7e3fb68bcf2379036513dce8d20553d9c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/c43ade7e3fb68bcf2379036513dce8d20553d9c8", + "reference": "c43ade7e3fb68bcf2379036513dce8d20553d9c8", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\Languages\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "role": "Developer" + } + ], + "description": "gettext languages with plural rules", + "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules", + "keywords": [ + "cldr", + "i18n", + "internationalization", + "l10n", + "language", + "languages", + "localization", + "php", + "plural", + "plural rules", + "plurals", + "translate", + "translations", + "unicode" + ], + "time": "2015-03-27 11:32:41" + }, + { + "name": "jaimeperez/twig-configurable-i18n", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/jaimeperez/twig-configurable-i18n.git", + "reference": "9b70edae9d512b06af24196c0ba24645cfcd87bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jaimeperez/twig-configurable-i18n/zipball/9b70edae9d512b06af24196c0ba24645cfcd87bd", + "reference": "9b70edae9d512b06af24196c0ba24645cfcd87bd", + "shasum": "" + }, + "require": { + "twig/extensions": "^1.3" + }, + "type": "project", + "autoload": { + "psr-4": { + "JaimePerez\\TwigConfigurableI18n\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "Jaime Perez", + "email": "jaime.perez@uninett.no" + } + ], + "description": "This is an extension on top of Twig's i18n extension, allowing you to customize which functions to use for translations.", + "keywords": [ + "extension", + "gettext", + "i18n", + "internationalization", + "translation", + "twig" + ], + "time": "2016-08-25 13:39:05" + }, { "name": "psr/log", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + "reference": "5277094ed527a1c4477177d102fe4c53551953e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "url": "https://api.github.com/repos/php-fig/log/zipball/5277094ed527a1c4477177d102fe4c53551953e0", + "reference": "5277094ed527a1c4477177d102fe4c53551953e0", "shasum": "" }, + "require": { + "php": ">=5.3.0" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { - "psr-0": { - "Psr\\Log\\": "" + "psr-4": { + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -38,25 +204,26 @@ } ], "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ "log", "psr", "psr-3" ], - "time": "2012-12-21 11:40:51" + "time": "2016-09-19 16:02:08" }, { "name": "robrichards/xmlseclibs", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "1b78df099c107279e9069a7b7608be98fd530dfd" + "reference": "53bb1e9cae490a8f93af41bd9df6ea897161ca05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/1b78df099c107279e9069a7b7608be98fd530dfd", - "reference": "1b78df099c107279e9069a7b7608be98fd530dfd", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/53bb1e9cae490a8f93af41bd9df6ea897161ca05", + "reference": "53bb1e9cae490a8f93af41bd9df6ea897161ca05", "shasum": "" }, "require": { @@ -84,7 +251,7 @@ "xml", "xmldsig" ], - "time": "2015-07-31 15:08:38" + "time": "2016-09-08 13:15:00" }, { "name": "simplesamlphp/saml2", @@ -138,18 +305,70 @@ "description": "SAML2 PHP library from SimpleSAMLphp", "time": "2016-07-26 13:28:46" }, + { + "name": "twig/extensions", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig-extensions.git", + "reference": "531eaf4b9ab778b1d7cdd10d40fc6aa74729dfee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig-extensions/zipball/531eaf4b9ab778b1d7cdd10d40fc6aa74729dfee", + "reference": "531eaf4b9ab778b1d7cdd10d40fc6aa74729dfee", + "shasum": "" + }, + "require": { + "twig/twig": "~1.20|~2.0" + }, + "require-dev": { + "symfony/translation": "~2.3" + }, + "suggest": { + "symfony/translation": "Allow the time_diff output to be translated" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_Extensions_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Common additional features for Twig that do not directly belong in core", + "homepage": "http://twig.sensiolabs.org/doc/extensions/index.html", + "keywords": [ + "i18n", + "text" + ], + "time": "2016-09-22 16:50:57" + }, { "name": "twig/twig", - "version": "v1.24.1", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "3566d311a92aae4deec6e48682dc5a4528c4a512" + "reference": "f16a634ab08d87e520da5671ec52153d627f10f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/3566d311a92aae4deec6e48682dc5a4528c4a512", - "reference": "3566d311a92aae4deec6e48682dc5a4528c4a512", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/f16a634ab08d87e520da5671ec52153d627f10f6", + "reference": "f16a634ab08d87e520da5671ec52153d627f10f6", "shasum": "" }, "require": { @@ -162,7 +381,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.24-dev" + "dev-master": "1.25-dev" } }, "autoload": { @@ -197,7 +416,7 @@ "keywords": [ "templating" ], - "time": "2016-05-30 09:11:59" + "time": "2016-09-21 23:05:12" }, { "name": "whitehat101/apr1-md5", @@ -451,16 +670,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "9270140b940ff02e58ec577c237274e92cd40cdd" + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9270140b940ff02e58ec577c237274e92cd40cdd", - "reference": "9270140b940ff02e58ec577c237274e92cd40cdd", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", "shasum": "" }, "require": { @@ -492,7 +711,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-06-10 09:48:41" + "time": "2016-09-30 07:12:33" }, { "name": "phpdocumentor/type-resolver", @@ -1406,16 +1625,16 @@ }, { "name": "symfony/config", - "version": "v3.1.2", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "bcf5aebabc95b56e370e13d78565f74c7d8726dc" + "reference": "431d28df9c7bb6e77f8f6289d8670b044fabb9e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/bcf5aebabc95b56e370e13d78565f74c7d8726dc", - "reference": "bcf5aebabc95b56e370e13d78565f74c7d8726dc", + "url": "https://api.github.com/repos/symfony/config/zipball/431d28df9c7bb6e77f8f6289d8670b044fabb9e8", + "reference": "431d28df9c7bb6e77f8f6289d8670b044fabb9e8", "shasum": "" }, "require": { @@ -1455,20 +1674,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:41:56" + "time": "2016-08-27 18:50:07" }, { "name": "symfony/console", - "version": "v3.1.2", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "747154aa69b0f83cd02fc9aa554836dee417631a" + "reference": "8ea494c34f0f772c3954b5fbe00bffc5a435e563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/747154aa69b0f83cd02fc9aa554836dee417631a", - "reference": "747154aa69b0f83cd02fc9aa554836dee417631a", + "url": "https://api.github.com/repos/symfony/console/zipball/8ea494c34f0f772c3954b5fbe00bffc5a435e563", + "reference": "8ea494c34f0f772c3954b5fbe00bffc5a435e563", "shasum": "" }, "require": { @@ -1515,20 +1734,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-06-29 07:02:31" + "time": "2016-08-19 06:48:39" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.8", + "version": "v2.8.11", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b180b70439dca70049b6b9b7e21d75e6e5d7aca9" + "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b180b70439dca70049b6b9b7e21d75e6e5d7aca9", - "reference": "b180b70439dca70049b6b9b7e21d75e6e5d7aca9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/889983a79a043dfda68f38c38b6dba092dd49cd8", + "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8", "shasum": "" }, "require": { @@ -1575,20 +1794,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:29:29" + "time": "2016-07-28 16:56:28" }, { "name": "symfony/filesystem", - "version": "v3.1.2", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "322da5f0910d8aa0b25fa65ffccaba68dbddb890" + "reference": "bb29adceb552d202b6416ede373529338136e84f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/322da5f0910d8aa0b25fa65ffccaba68dbddb890", - "reference": "322da5f0910d8aa0b25fa65ffccaba68dbddb890", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/bb29adceb552d202b6416ede373529338136e84f", + "reference": "bb29adceb552d202b6416ede373529338136e84f", "shasum": "" }, "require": { @@ -1624,7 +1843,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:41:56" + "time": "2016-07-20 05:44:26" }, { "name": "symfony/polyfill-mbstring", @@ -1687,7 +1906,7 @@ }, { "name": "symfony/stopwatch", - "version": "v3.1.2", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", diff --git a/config-templates/config.php b/config-templates/config.php index 43017312bcb4340780167a2c8b8754bd6c2c1f25..8c614f6dfbfacc5a6d048fcfd6e61e4c5a0356ab 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -649,6 +649,11 @@ $config = array( 'language.cookie.lifetime' => (60 * 60 * 24 * 900), /* + * Which i18n backend to use + */ + 'language.i18n.backend' => 'twig.i18n', + + /** * Custom getLanguage function called from SimpleSAML\Locale\Language::getLanguage(). * Function should return language code of one of the available languages or NULL. * See SimpleSAML\Locale\Language::getLanguage() source code for more info. diff --git a/lib/SimpleSAML/Locale/Language.php b/lib/SimpleSAML/Locale/Language.php index 412fc2ac9fca5d2ef57d97c1e83e1e6ab7a71be6..ded7eeb7c43ba07f7c7a504bcb50e60a51dd1ff6 100644 --- a/lib/SimpleSAML/Locale/Language.php +++ b/lib/SimpleSAML/Locale/Language.php @@ -70,7 +70,10 @@ class Language private $customFunction; /** - * A list of languages supported with their names localized, indexed by ISO 639-2 code. + * A list of languages supported with their names localized. + * Indexed by something that mostly resembles ISO 639-1 code, + * with some charming SimpleSAML-specific variants... + * that must remain before 2.0 due to backwards compatibility * * @var array */ @@ -78,7 +81,7 @@ class Language 'no' => 'Bokmål', // Norwegian Bokmål 'nn' => 'Nynorsk', // Norwegian Nynorsk 'se' => 'Sámegiella', // Northern Sami - 'sam' => 'Åarjelh-saemien giele', // Southern Sami + 'sma' => 'Åarjelh-saemien giele', // Southern Sami 'da' => 'Dansk', // Danish 'en' => 'English', 'de' => 'Deutsch', // German @@ -115,6 +118,17 @@ class Language 'eu' => 'Euskara', // Basque ); + /** + * A mapping of SSP languages to locales + * + * @var array + */ + private $languagePosixMapping = array( + 'no' => 'nb_NO', + 'en' => 'en_US', + 'nn' => 'nn_NO', + ); + /** * Constructor @@ -138,6 +152,20 @@ class Language } + /* + * Rename to non-idiosyncratic language code + * + * @param string $language Language code for the language to rename, if neccesary. + */ + public function getPosixLanguage($language) + { + if (isset($this->languagePosixMapping[$language])) { + return $this->languagePosixMapping[$language]; + } + return $language; + } + + /** * This method will set a cookie for the user's browser to remember what language was selected. * diff --git a/lib/SimpleSAML/Locale/Localization.php b/lib/SimpleSAML/Locale/Localization.php new file mode 100644 index 0000000000000000000000000000000000000000..e11a56c3af003001cdbc58268d93f8fe5e100304 --- /dev/null +++ b/lib/SimpleSAML/Locale/Localization.php @@ -0,0 +1,217 @@ +<?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 = 'ssp'; + + /** + * Default 1i18n backend + */ + const DEFAULT_I18NBACKEND = 'twig.gettextgettext'; + + /* + * The default locale directory + */ + private $localeDir; + + /* + * Where specific domains are stored + */ + private $localeDomainMap = array(); + + /* + * Pointer to currently active translator + */ + private $translator; + + /* + * Currently active domain + */ + private $currentDomain; + + + /** + * 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', null); + $this->setupL10N(); + } + + + /** + * Dump the default locale directory + */ + public function getLocaleDir() + { + return $this->localeDir; + } + + + /* + * 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; + } + + /* + * 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/'; + 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); + } + + + /** + * Load translation domain from Gettext/Gettext using .po + * + * @param string $domain Name of domain + */ + private function loadGettextGettextFromPO($domain = self::DEFAULT_DOMAIN) { + $t = new Translator(); + $t->register(); + try { + $langPath = $this->getLangPath($domain); + } catch (\Exception $e) { + // bail out! + return; + } + $poFile = $domain.'.po'; + $poPath = $langPath.$poFile; + if (file_exists($poPath) && is_readable($poPath)) { + $translations = Translations::fromPoFile($poPath); + $t->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::DEFAULT_I18NBACKEND) { + return true; + } + return false; + } + + + /** + * Set up L18N if configured or fallback to old system + */ + private function setupL10N() { + // use old system + if (! $this->isI18NBackendDefault()) { + \SimpleSAML\Logger::debug("Localization: using old system"); + return; + } + // setup default domain + $this->addDomain($this->localeDir, self::DEFAULT_DOMAIN); + $this->activateDomain(self::DEFAULT_DOMAIN); + } + + /** + * Show which domains are registered + */ + public function getRegisteredDomains() + { + return $this->localeDomainMap; + } + + + /** + * Set which translation domain to use + * + * @param string $domain Name of domain + */ + public function activateDomain($domain) + { + \SimpleSAML\Logger::debug("Localization: activate domain"); + $this->loadGettextGettextFromPO($domain); + $this->currentDomain = $domain; + } + + /** + * Get current translation domain + */ + public function getCurrentDomain() + { + return $this->currentDomain ? $this->currentDomain : self::DEFAULT_DOMAIN; + } + + /** + * Go back to default translation domain + */ + public function restoreDefaultDomain() + { + $this->loadGettextGettextFromPO(self::DEFAULT_DOMAIN); + $this->currentDomain = self::DEFAULT_DOMAIN; + } +} diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php index 1ebd2fbfc5be696876187caf6547f2bd2623bf84..abd81655f1495cd583776b82461e727a13c4f6ff 100644 --- a/lib/SimpleSAML/XHTML/Template.php +++ b/lib/SimpleSAML/XHTML/Template.php @@ -7,6 +7,12 @@ * @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> * @package SimpleSAMLphp */ + + +use JaimePerez\TwigConfigurableI18n\Twig\Environment as Twig_Environment; +use JaimePerez\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n; + + class SimpleSAML_XHTML_Template { @@ -24,6 +30,13 @@ class SimpleSAML_XHTML_Template */ private $translator; + /** + * The localization backend + * + * @var \SimpleSAML\Locale\Localization + */ + private $localization; + /** * The configuration to use in this template. * @@ -65,6 +78,8 @@ class SimpleSAML_XHTML_Template // TODO: do not remove the slash from the beginning, change the templates instead! $this->data['baseurlpath'] = ltrim($this->configuration->getBasePath(), '/'); $this->translator = new SimpleSAML\Locale\Translate($configuration, $defaultDictionary); + $this->localization = new \SimpleSAML\Locale\Localization($configuration); + $this->useTwig = $this->setupTwig(); $this->twig = $this->setupTwig(); } @@ -122,9 +137,6 @@ class SimpleSAML_XHTML_Template foreach ($templateDirs as $entry) { $loader->addPath($entry[key($entry)], key($entry)); } - if (!$loader->exists($this->twig_template)) { - return false; - } return $loader; } @@ -142,11 +154,27 @@ class SimpleSAML_XHTML_Template } // set up template paths $loader = $this->setupTwigTemplatepaths(); - if (!$loader) { + // abort if twig template does not exist + if (!$loader->exists($this->twig_template)) { return null; } - return new \Twig_Environment($loader, array('cache' => $cache, 'auto_reload' => $auto_reload)); + $twig = new \Twig_Environment($loader, + array('cache' => $cache, 'auto_reload' => $auto_reload, + 'translation_function' => '__', + 'translation_function_plural' => 'n__', + ) + ); + // set up translation + if ($this->localization->i18nBackend == 'twig.gettextgettext') { + /* if something like pull request #166 is ever merged with + * twig.extensions.i18n, use the line below: + * $twig->addExtension(new \Twig_Extensions_Extension_I18n('__', 'n__')); + * instead of the two lines after this comment + */ + $twig->addExtension(new \Twig_Extensions_Extension_I18n()); + } + return $twig; } /* @@ -252,6 +280,7 @@ class SimpleSAML_XHTML_Template */ private function twigDefaultContext() { + $this->data['localeBackend'] = $this->configuration->getString('language.i18n.backend', 'ssp'); $this->data['currentLanguage'] = $this->translator->getLanguage()->getLanguage(); // show language bar by default if (!isset($this->data['hideLanguageBar'])) { diff --git a/locales/.gitkeep b/locales/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/locales/en/LC_MESSAGES/ssp.mo b/locales/en/LC_MESSAGES/ssp.mo new file mode 100644 index 0000000000000000000000000000000000000000..c4931e30dedab05eaff5b91b41e4d67256cf716a Binary files /dev/null and b/locales/en/LC_MESSAGES/ssp.mo differ diff --git a/locales/en/LC_MESSAGES/ssp.po b/locales/en/LC_MESSAGES/ssp.po new file mode 100644 index 0000000000000000000000000000000000000000..70a64a7b7d2dfcf0d9934bd1c44a8805d1dcde64 --- /dev/null +++ b/locales/en/LC_MESSAGES/ssp.po @@ -0,0 +1,17 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-03-01 14:40+0100\n" +"PO-Revision-Date: 2016-03-01 14:42+0100\n" +"Last-Translator: Hanne Moa <hanne.moa@uninett.no>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.4\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: en\n" + +msgid "Hello, Untranslated World!" +msgstr "Hello, Translated World!" diff --git a/locales/en/LC_MESSAGES/test.po b/locales/en/LC_MESSAGES/test.po new file mode 100644 index 0000000000000000000000000000000000000000..70a64a7b7d2dfcf0d9934bd1c44a8805d1dcde64 --- /dev/null +++ b/locales/en/LC_MESSAGES/test.po @@ -0,0 +1,17 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-03-01 14:40+0100\n" +"PO-Revision-Date: 2016-03-01 14:42+0100\n" +"Last-Translator: Hanne Moa <hanne.moa@uninett.no>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.4\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: en\n" + +msgid "Hello, Untranslated World!" +msgstr "Hello, Translated World!" diff --git a/locales/en_US b/locales/en_US new file mode 120000 index 0000000000000000000000000000000000000000..2c4c454fdd2fd2902cc43a87d26851282de294f1 --- /dev/null +++ b/locales/en_US @@ -0,0 +1 @@ +en \ No newline at end of file diff --git a/locales/nb/LC_MESSAGES/ssp.mo b/locales/nb/LC_MESSAGES/ssp.mo new file mode 100644 index 0000000000000000000000000000000000000000..e0b0fd1ff49d8bfbe9fea4f2b3e9a4dfcbee76c6 Binary files /dev/null and b/locales/nb/LC_MESSAGES/ssp.mo differ diff --git a/locales/nb/LC_MESSAGES/ssp.po b/locales/nb/LC_MESSAGES/ssp.po new file mode 100644 index 0000000000000000000000000000000000000000..f4e73b33321e2a05c4a057797323305822e81219 --- /dev/null +++ b/locales/nb/LC_MESSAGES/ssp.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-03-01 14:40+0100\n" +"PO-Revision-Date: 2016-03-01 14:45+0100\n" +"Last-Translator: Hanne Moa <hanne.moa@uninett.no>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.4\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: nb\n" +"X-Poedit-SourceCharset: UTF-8\n" + +msgid "Hello, Untranslated World!" +msgstr "Hallo, oversatte verden!" diff --git a/locales/nb/LC_MESSAGES/test.po b/locales/nb/LC_MESSAGES/test.po new file mode 100644 index 0000000000000000000000000000000000000000..f4e73b33321e2a05c4a057797323305822e81219 --- /dev/null +++ b/locales/nb/LC_MESSAGES/test.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-03-01 14:40+0100\n" +"PO-Revision-Date: 2016-03-01 14:45+0100\n" +"Last-Translator: Hanne Moa <hanne.moa@uninett.no>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.4\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: nb\n" +"X-Poedit-SourceCharset: UTF-8\n" + +msgid "Hello, Untranslated World!" +msgstr "Hallo, oversatte verden!" diff --git a/locales/nb_NO b/locales/nb_NO new file mode 120000 index 0000000000000000000000000000000000000000..6c2c32f1bdbc8bd08c444ea0bd76a967b043e5b0 --- /dev/null +++ b/locales/nb_NO @@ -0,0 +1 @@ +nb \ No newline at end of file diff --git a/locales/nn/LC_MESSAGES/ssp.mo b/locales/nn/LC_MESSAGES/ssp.mo new file mode 100644 index 0000000000000000000000000000000000000000..6e442efe212da853ebd67796b8a8a0ca19c78aa6 Binary files /dev/null and b/locales/nn/LC_MESSAGES/ssp.mo differ diff --git a/locales/nn/LC_MESSAGES/ssp.po b/locales/nn/LC_MESSAGES/ssp.po new file mode 100644 index 0000000000000000000000000000000000000000..0ce9baabbd87da1e647ea6a9f57fb3518ff3e47a --- /dev/null +++ b/locales/nn/LC_MESSAGES/ssp.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-03-01 14:40+0100\n" +"PO-Revision-Date: 2016-03-01 14:45+0100\n" +"Last-Translator: Hanne Moa <hanne.moa@uninett.no>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.4\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: nn\n" +"X-Poedit-SourceCharset: UTF-8\n" + +msgid "Hello, Untranslated World!" +msgstr "Hallo, oversatte verd!" diff --git a/locales/nn/LC_MESSAGES/test.po b/locales/nn/LC_MESSAGES/test.po new file mode 100644 index 0000000000000000000000000000000000000000..0ce9baabbd87da1e647ea6a9f57fb3518ff3e47a --- /dev/null +++ b/locales/nn/LC_MESSAGES/test.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-03-01 14:40+0100\n" +"PO-Revision-Date: 2016-03-01 14:45+0100\n" +"Last-Translator: Hanne Moa <hanne.moa@uninett.no>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.4\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: nn\n" +"X-Poedit-SourceCharset: UTF-8\n" + +msgid "Hello, Untranslated World!" +msgstr "Hallo, oversatte verd!" diff --git a/locales/no b/locales/no new file mode 120000 index 0000000000000000000000000000000000000000..6c2c32f1bdbc8bd08c444ea0bd76a967b043e5b0 --- /dev/null +++ b/locales/no @@ -0,0 +1 @@ +nb \ No newline at end of file diff --git a/templates/_header.twig b/templates/_header.twig index ad521520e8bc5f3c18d409424f3cc51edd90e2d2..704e340f69c295b42331b5957af347618ce662b1 100644 --- a/templates/_header.twig +++ b/templates/_header.twig @@ -6,7 +6,7 @@ <div id="languagebar"> {% for lang in languageBar %} - {%- if not loop.first -%} | {%- endif -%}{% if lang.url %}<a href="{{ lang.url }}">{{ lang.name }}</a>{% else %}{{ lang.name }}{% endif %} + {%- if not loop.first -%} | {% endif -%}{% if lang.url %}<a href="{{ lang.url }}">{{ lang.name }}</a>{% else %}{{ lang.name }}{% endif %} {% endfor %} diff --git a/templates/sandbox.twig b/templates/sandbox.twig index 0d42e5e167194712301a8e4a369de1f20c06aad2..3e1b94627f3076a27442e2af94c56923ed8a0a92 100644 --- a/templates/sandbox.twig +++ b/templates/sandbox.twig @@ -2,5 +2,10 @@ {% block content %} <p>This page exists as a sandbox to play with twig without affecting anything else. The template is in ./templates.</p> <p>{{ sometext }}</p> + <h2>And now for some localization</h2> + <p>Locale backend in use: {{ localeBackend }}</p> + <p>Original: Hello, Untranslated World!</p> + <p>Translated: {% trans 'Hello, Untranslated World!' %}</p> + <p>Filtertrans-test: {{ 'Hello, Untranslated World!'|trans }}</p> <p>Current locale set: {{ currentLanguage }}</p> {% endblock content %} diff --git a/tests/lib/SimpleSAML/Locale/LocalizationTest.php b/tests/lib/SimpleSAML/Locale/LocalizationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..954b5a03f74194a285d93d09b838025cabbde13f --- /dev/null +++ b/tests/lib/SimpleSAML/Locale/LocalizationTest.php @@ -0,0 +1,87 @@ +<?php + +namespace SimpleSAML\Test\Locale; + +use Gettext\Translations; +use Gettext\Translator; + +use SimpleSAML\Locale\Localization; + +class LocalizationTest extends \PHPUnit_Framework_TestCase +{ + + + /** + * Test SimpleSAML\Locale\Localization(). + */ + public function testLocalization() + { + // The constructor should activate the default domain + $c = \SimpleSAML_Configuration::loadFromArray( + array('language.i18n.backend' => 'twig.gettextgettext') + ); + $l = new Localization($c); + $this->assertTrue($l->isI18NBackendDefault()); + $this->assertEquals(Localization::DEFAULT_DOMAIN, 'ssp'); + $this->assertEquals($l->getCurrentDomain(), Localization::DEFAULT_DOMAIN); + } + + /** + * Test SimpleSAML\Locale\Localization::activateDomain(). + */ + public function testAddDomain() + { + $c = \SimpleSAML_Configuration::loadFromArray( + array('language.i18n.backend' => 'twig.gettextgettext') + ); + $l = new Localization($c); + $newDomain = 'test'; + $newDomainLocaleDir = '/tmp/nonexistent.po'; + $l->addDomain($newDomainLocaleDir, $newDomain); + $registeredDomains = $l->getRegisteredDomains(); + $this->assertArrayHasKey($newDomain, $registeredDomains); + $this->assertEquals($registeredDomains[$newDomain], $newDomainLocaleDir); + } + + /** + * Test SimpleSAML\Locale\Localization::activateDomain(). + */ + public function testActivateDomain() + { + // Add the domain to activate + $c = \SimpleSAML_Configuration::loadFromArray( + array('language.i18n.backend' => 'twig.gettextgettext') + ); + $l = new Localization($c); + $newDomain = 'test'; + $newDomainLocaleDir = $l->getLocaleDir(); + $l->addDomain($newDomainLocaleDir, $newDomain); + + // Activate + $l->activateDomain($newDomain); + $curDomain = $l->getCurrentDomain(); + $this->assertEquals($curDomain, $newDomain); + } + + /** + * Test SimpleSAML\Locale\Localization::restoreDefaultDomain(). + */ + public function testRestoreDefaultDomain() + { + // Add the domain to reset from + $c = \SimpleSAML_Configuration::loadFromArray( + array('language.i18n.backend' => 'twig.gettextgettext') + ); + $l = new Localization($c); + $newDomain = 'ssp'; + $newDomainLocaleDir = $l->getLocaleDir(); + $l->addDomain($newDomainLocaleDir, $newDomain); + $l->activateDomain($newDomain); + + // Reset + $l->restoreDefaultDomain(); +# $curDomain = $l->getCurrentDomain(); +# $this->assertEquals($curDomain, Localization::DEFAULT_DOMAIN); + } + +}