diff --git a/config-templates/config.php b/config-templates/config.php
index 224b85670af9ec5253e365b22f02537a7fdea7ab..49a981d204b05ad3a7785e8b91b8d948ddab1c63 100644
--- a/config-templates/config.php
+++ b/config-templates/config.php
@@ -423,9 +423,9 @@ $config = array(
     'language.cookie.lifetime' => (60 * 60 * 24 * 900),
 
     /**
-     * Custom getLanguage function called from SimpleSAML_XHTML_Template::getLanguage().
+     * Custom getLanguage function called from SimpleSAML\Locale\Language::getLanguage().
      * Function should return language code of one of the available languages or NULL.
-     * See SimpleSAML_XHTML_Template::getLanguage() source code for more info.
+     * See SimpleSAML\Locale\Language::getLanguage() source code for more info.
      *
      * This option can be used to implement a custom function for determining
      * the default language for the user.
diff --git a/lib/SimpleSAML/Locale/Language.php b/lib/SimpleSAML/Locale/Language.php
new file mode 100644
index 0000000000000000000000000000000000000000..7d22791916a15eade2a2a6f70402d86c69422477
--- /dev/null
+++ b/lib/SimpleSAML/Locale/Language.php
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * Choosing the language to localize to for our minimalistic XHTML PHP based template system.
+ *
+ * @author Andreas Ă…kre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
+ * @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
+ * @package SimpleSAMLphp
+ */
+
+namespace SimpleSAML\Locale;
+
+use SimpleSAML\Utils\HTTP;
+
+class Language
+{
+
+    /**
+     * This is the default language map. It is used to map languages codes from the user agent to other language codes.
+     */
+    private static $defaultLanguageMap = array('nb' => 'no');
+
+    private $configuration = null;
+    private $availableLanguages = array('en');
+    private $language = null;
+
+
+    /**
+     * HTTP GET language parameter name.
+     */
+    private $languageParameterName = 'language';
+
+
+    /**
+     * Constructor
+     *
+     * @param \SimpleSAML_Configuration $configuration Configuration object
+     */
+    public function __construct(\SimpleSAML_Configuration $configuration)
+    {
+        $this->configuration = $configuration;
+
+        $this->availableLanguages = $this->configuration->getArray('language.available', array('en'));
+
+        $this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language');
+        if (isset($_GET[$this->languageParameterName])) {
+            $this->setLanguage(
+                $_GET[$this->languageParameterName],
+                $this->configuration->getBoolean('language.parameter.setcookie', true)
+            );
+        }
+    }
+
+
+    /**
+     * This method will set a cookie for the user's browser to remember what language was selected.
+     *
+     * @param string  $language Language code for the language to set.
+     * @param boolean $setLanguageCookie Whether to set the language cookie or not. Defaults to true.
+     */
+    public function setLanguage($language, $setLanguageCookie = true)
+    {
+        $language = strtolower($language);
+        if (in_array($language, $this->availableLanguages, true)) {
+            $this->language = $language;
+            if ($setLanguageCookie === true) {
+                Language::setLanguageCookie($language);
+            }
+        }
+    }
+
+
+    /**
+     * This method will return the language selected by the user, or the default language. It looks first for a cached
+     * language code, then checks for a language cookie, then it tries to calculate the preferred language from HTTP
+     * headers.
+     *
+     * @return string The language selected by the user according to the processing rules specified, or the default
+     * language in any other case.
+     */
+    public function getLanguage()
+    {
+        // language is set in object
+        if (isset($this->language)) {
+            return $this->language;
+        }
+
+        // run custom getLanguage function if defined
+        $customFunction = $this->configuration->getArray('language.get_language_function', null);
+        if (isset($customFunction)) {
+            assert('is_callable($customFunction)');
+            $customLanguage = call_user_func($customFunction, $this);
+            if ($customLanguage !== null && $customLanguage !== false) {
+                return $customLanguage;
+            }
+        }
+
+        // language is provided in a stored cookie
+        $languageCookie = Language::getLanguageCookie();
+        if ($languageCookie !== null) {
+            $this->language = $languageCookie;
+            return $languageCookie;
+        }
+
+        // check if we can find a good language from the Accept-Language HTTP header
+        $httpLanguage = $this->getHTTPLanguage();
+        if ($httpLanguage !== null) {
+            return $httpLanguage;
+        }
+
+        // language is not set, and we get the default language from the configuration
+        return $this->getDefaultLanguage();
+    }
+
+
+    /**
+     * Get the language parameter name.
+     *
+     * @return string The language parameter name.
+     */
+    public function getLanguageParameterName()
+    {
+        return $this->languageParameterName;
+    }
+
+
+    /**
+     * This method returns the preferred language for the user based on the Accept-Language HTTP header.
+     *
+     * @return string The preferred language based on the Accept-Language HTTP header, or null if none of the languages
+     * in the header is available.
+     */
+    private function getHTTPLanguage()
+    {
+        $languageScore = HTTP::getAcceptLanguage();
+
+        // for now we only use the default language map. We may use a configurable language map in the future
+        $languageMap = self::$defaultLanguageMap;
+
+        // find the available language with the best score
+        $bestLanguage = null;
+        $bestScore = -1.0;
+
+        foreach ($languageScore as $language => $score) {
+
+            // apply the language map to the language code
+            if (array_key_exists($language, $languageMap)) {
+                $language = $languageMap[$language];
+            }
+
+            if (!in_array($language, $this->availableLanguages, true)) {
+                // skip this language - we don't have it
+                continue;
+            }
+
+            /* Some user agents use very limited precicion of the quality value, but order the elements in descending
+             * order. Therefore we rely on the order of the output from getAcceptLanguage() matching the order of the
+             * languages in the header when two languages have the same quality.
+             */
+            if ($score > $bestScore) {
+                $bestLanguage = $language;
+                $bestScore = $score;
+            }
+        }
+
+        return $bestLanguage;
+    }
+
+
+    /**
+     * Return the default language according to configuration.
+     *
+     * @return string The default language that has been configured. Defaults to english if not configured.
+     */
+    public function getDefaultLanguage()
+    {
+        return $this->configuration->getString('language.default', 'en');
+    }
+
+
+    /**
+     * Return a list of all languages available.
+     *
+     * @return array An array holding all the languages available.
+     */
+    public function getLanguageList()
+    {
+        $thisLang = $this->getLanguage();
+        $lang = array();
+        foreach ($this->availableLanguages as $nl) {
+            $lang[$nl] = ($nl == $thisLang);
+        }
+        return $lang;
+    }
+
+
+    /**
+     * Check whether a language is right-to-left or not.
+     *
+     * @return boolean True if the language is right-to-left, false otherwise.
+     */
+    public function isLanguageRTL()
+    {
+        $rtlLanguages = $this->configuration->getArray('language.rtl', array());
+        $thisLang = $this->getLanguage();
+        if (in_array($thisLang, $rtlLanguages)) {
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * Retrieve the user-selected language from a cookie.
+     *
+     * @return string|null The selected language or null if unset.
+     */
+    public static function getLanguageCookie()
+    {
+        $config = \SimpleSAML_Configuration::getInstance();
+        $availableLanguages = $config->getArray('language.available', array('en'));
+        $name = $config->getString('language.cookie.name', 'language');
+
+        if (isset($_COOKIE[$name])) {
+            $language = strtolower((string) $_COOKIE[$name]);
+            if (in_array($language, $availableLanguages, true)) {
+                return $language;
+            }
+        }
+
+        return null;
+    }
+
+
+    /**
+     * This method will attempt to set the user-selected language in a cookie. It will do nothing if the language
+     * specified is not in the list of available languages, or the headers have already been sent to the browser.
+     *
+     * @param string $language The language set by the user.
+     */
+    public static function setLanguageCookie($language)
+    {
+        assert('is_string($language)');
+
+        $language = strtolower($language);
+        $config = \SimpleSAML_Configuration::getInstance();
+        $availableLanguages = $config->getArray('language.available', array('en'));
+
+        if (!in_array($language, $availableLanguages, true) || headers_sent()) {
+            return;
+        }
+
+        $name = $config->getString('language.cookie.name', 'language');
+        $params = array(
+            'lifetime' => ($config->getInteger('language.cookie.lifetime', 60 * 60 * 24 * 900)),
+            'domain'   => ($config->getString('language.cookie.domain', null)),
+            'path'     => ($config->getString('language.cookie.path', '/')),
+            'httponly' => false,
+        );
+
+        HTTP::setCookie($name, $language, $params, false);
+    }
+}
diff --git a/lib/SimpleSAML/Locale/Translate.php b/lib/SimpleSAML/Locale/Translate.php
new file mode 100644
index 0000000000000000000000000000000000000000..ebbac327575e74acf27c6274234ebf8ce02bf190
--- /dev/null
+++ b/lib/SimpleSAML/Locale/Translate.php
@@ -0,0 +1,446 @@
+<?php
+
+/**
+ * The translation-relevant bits from our original minimalistic XHTML PHP based template system.
+ *
+ * @author Andreas Ă…kre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
+ * @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
+ * @package SimpleSAMLphp
+ */
+
+namespace SimpleSAML\Locale;
+
+class Translate
+{
+
+    /**
+     * The configuration to be used for this translator.
+     *
+     * @var \SimpleSAML_Configuration
+     */
+    private $configuration;
+
+    private $langtext = array();
+
+    /**
+     * Associative array of dictionaries.
+     */
+    private $dictionaries = array();
+
+    /**
+     * The default dictionary.
+     */
+    private $defaultDictionary = null;
+
+    /**
+     * The language object we'll use internally.
+     *
+     * @var \SimpleSAML\Locale\Language
+     */
+    private $language;
+
+
+    /**
+     * Constructor
+     *
+     * @param \SimpleSAML_Configuration $configuration Configuration object
+     * @param string|null               $defaultDictionary The default dictionary where tags will come from.
+     */
+    public function __construct(\SimpleSAML_Configuration $configuration, $defaultDictionary = null)
+    {
+        $this->configuration = $configuration;
+        $this->language = new Language($configuration);
+
+        if ($defaultDictionary !== null && substr($defaultDictionary, -4) === '.php') {
+            // TODO: drop this entire if clause for 2.0
+            // for backwards compatibility - print warning
+            $backtrace = debug_backtrace();
+            $where = $backtrace[0]['file'].':'.$backtrace[0]['line'];
+            \SimpleSAML_Logger::warning(
+                'Deprecated use of new SimpleSAML\Locale\Translate(...) at '.$where.
+                '. The last parameter is now a dictionary name, which should not end in ".php".'
+            );
+
+            $this->defaultDictionary = substr($defaultDictionary, 0, -4);
+        } else {
+            $this->defaultDictionary = $defaultDictionary;
+        }
+    }
+
+
+    /**
+     * Return the internal language object used by this translator.
+     *
+     * @return \SimpleSAML\Locale\Language
+     */
+    public function getLanguage()
+    {
+        return $this->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($name)
+    {
+        assert('is_string($name)');
+
+        if (!array_key_exists($name, $this->dictionaries)) {
+            $sepPos = strpos($name, ':');
+            if ($sepPos !== false) {
+                $module = substr($name, 0, $sepPos);
+                $fileName = substr($name, $sepPos + 1);
+                $dictDir = \SimpleSAML_Module::getModuleDir($module).'/dictionaries/';
+            } else {
+                $dictDir = $this->configuration->getPathValue('dictionarydir', '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 An associative array with language => string mappings, or null if the tag wasn't found.
+     */
+    public function getTag($tag)
+    {
+        assert('is_string($tag)');
+
+        // 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($translations)
+    {
+        assert('is_array($translations)');
+
+        // 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($name)
+    {
+        // 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
+        return $name;
+    }
+
+
+    /**
+     * Translate a tag into the current language, with a fallback to english.
+     *
+     * This function is used to look up a translation tag in dictionaries, and return the translation into the current
+     * language. If no translation into the current language can be found, english will be tried, and if that fails,
+     * placeholder text will be returned.
+     *
+     * An array can be passed as the tag. In that case, the array will be assumed to be on the form (language => text),
+     * and will be used as the source of translations.
+     *
+     * This function can also do replacements into the translated tag. It will search the translated tag for the keys
+     * provided in $replacements, and replace any found occurrences with the value of the key.
+     *
+     * @param string|array $tag A tag name for the translation which should be looked up, or an array with
+     * (language => text) mappings.
+     * @param array        $replacements An associative array of keys that should be replaced with values in the
+     *     translated string.
+     * @param boolean      $fallbackdefault Default translation to use as a fallback if no valid translation was found.
+     *
+     * @return string  The translated tag, or a placeholder value if the tag wasn't found.
+     */
+    public function t(
+        $tag,
+        $replacements = array(),
+        $fallbackdefault = true,
+        $oldreplacements = array(), // TODO: remove this for 2.0
+        $striptags = false // TODO: remove this for 2.0
+    ) {
+        if (!is_array($replacements)) {
+            // TODO: remove this entire if for 2.0
+
+            // old style call to t(...). Print warning to log
+            $backtrace = debug_backtrace();
+            $where = $backtrace[0]['file'].':'.$backtrace[0]['line'];
+            \SimpleSAML_Logger::warning(
+                'Deprecated use of SimpleSAML_Template::t(...) at '.$where.
+                '. Please update the code to use the new style of parameters.'
+            );
+
+            // for backwards compatibility
+            if (!$replacements && $this->getTag($tag) === null) {
+                \SimpleSAML_Logger::warning(
+                    'Code which uses $fallbackdefault === FALSE should be updated to use the getTag() method instead.'
+                );
+                return null;
+            }
+
+            $replacements = $oldreplacements;
+        }
+
+        if (is_array($tag)) {
+            $tagData = $tag;
+        } else {
+            $tagData = $this->getTag($tag);
+            if ($tagData === null) {
+                // tag not found
+                \SimpleSAML_Logger::info('Template: Looking up ['.$tag.']: not translated at all.');
+                return $this->getStringNotTranslated($tag, $fallbackdefault);
+            }
+        }
+
+        $translated = $this->getPreferredTranslation($tagData);
+
+        foreach ($replacements as $k => $v) {
+            // try to translate if no replacement is given
+            if ($v == null) {
+                $v = $this->t($k);
+            }
+            $translated = str_replace($k, $v, $translated);
+        }
+        return $translated;
+    }
+
+
+    /**
+     * Return the string that should be used when no translation was found.
+     *
+     * @param string  $tag A name tag of the string that should be returned.
+     * @param boolean $fallbacktag If set to true and string was not found in any languages, return the tag itself. If
+     * false return null.
+     *
+     * @return string The string that should be used, or the tag name if $fallbacktag is set to false.
+     */
+    private function getStringNotTranslated($tag, $fallbacktag)
+    {
+        if ($fallbacktag) {
+            return 'not translated ('.$tag.')';
+        } else {
+            return $tag;
+        }
+    }
+
+
+    /**
+     * Include a translation inline instead of putting translations in dictionaries. This function is recommended to be
+     * used ONLU from variable data, or when the translation is already provided by an external source, as a database
+     * or in metadata.
+     *
+     * @param string       $tag The tag that has a translation
+     * @param array|string $translation The translation array
+     *
+     * @throws \Exception If $translation is neither a string nor an array.
+     */
+    public function includeInlineTranslation($tag, $translation)
+    {
+        if (is_string($translation)) {
+            $translation = array('en' => $translation);
+        } elseif (!is_array($translation)) {
+            throw new \Exception("Inline translation should be string or array. Is ".gettype($translation)." now!");
+        }
+
+        \SimpleSAML_Logger::debug('Template: 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($file, $otherConfig = null)
+    {
+        if (!empty($otherConfig)) {
+            $filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
+        } else {
+            $filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
+        }
+
+        $lang = $this->readDictionaryFile($filebase.$file);
+        \SimpleSAML_Logger::debug('Template: 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($filename)
+    {
+        $definitionFile = $filename.'.definition.json';
+        assert('file_exists($definitionFile)');
+
+        $fileContent = file_get_contents($definitionFile);
+        $lang = json_decode($fileContent, true);
+
+        if (empty($lang)) {
+            \SimpleSAML_Logger::error('Invalid dictionary definition file ['.$definitionFile.']');
+            return array();
+        }
+
+        $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);
+            }
+        }
+
+        return $lang;
+    }
+
+
+    /**
+     * 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($filename)
+    {
+        $phpFile = $filename.'.php';
+        assert('file_exists($phpFile)');
+
+        $lang = null;
+        include($phpFile);
+        if (isset($lang)) {
+            return $lang;
+        }
+
+        return array();
+    }
+
+
+    /**
+     * 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($filename)
+    {
+        assert('is_string($filename)');
+
+        \SimpleSAML_Logger::debug('Template: Reading ['.$filename.']');
+
+        $jsonFile = $filename.'.definition.json';
+        if (file_exists($jsonFile)) {
+            return $this->readDictionaryJSON($filename);
+        }
+
+        $phpFile = $filename.'.php';
+        if (file_exists($phpFile)) {
+            return $this->readDictionaryPHP($filename);
+        }
+
+        \SimpleSAML_Logger::error(
+            $_SERVER['PHP_SELF'].' - Template: Could not find dictionary file at ['.$filename.']'
+        );
+        return array();
+    }
+}
diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php
index ab20e116982aa3d7746bffdfb3dc5b706e2b609b..1f365346bdf7080a47c057d25841bf2ea7e38dc0 100644
--- a/lib/SimpleSAML/XHTML/Template.php
+++ b/lib/SimpleSAML/XHTML/Template.php
@@ -1,711 +1,336 @@
 <?php
 
+
 /**
  * A minimalistic XHTML PHP based template system implemented for SimpleSAMLphp.
  *
  * @author Andreas Ă…kre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
  * @package SimpleSAMLphp
  */
-class SimpleSAML_XHTML_Template {
+class SimpleSAML_XHTML_Template
+{
 
     /**
-     * This is the default language map. It is used to map languages codes from the user agent to
-     * other language codes.
+     * The data associated with this template, accessible within the template itself.
+     *
+     * @var array
      */
-    private static $defaultLanguageMap = array('nb' => 'no');
-
-
-    private $configuration = null;
-    private $template = 'default.php';
-    private $availableLanguages = array('en');
-    private $language = null;
-
-    private $langtext = array();
-
-    public $data = null;
-
+    public $data = array();
 
     /**
-     * Associative array of dictionaries.
+     * A translator instance configured to work with this template.
+     *
+     * @var \SimpleSAML\Locale\Translate
      */
-    private $dictionaries = array();
-
+    private $translator;
 
     /**
-     * The default dictionary.
+     * The configuration to use in this template.
+     *
+     * @var SimpleSAML_Configuration
      */
-    private $defaultDictionary = NULL;
-
+    private $configuration;
 
     /**
-     * HTTP GET language parameter name.
+     * The file to load in this template.
+     *
+     * @var string
      */
-    private $languageParameterName = 'language';
+    private $template = 'default.php';
 
 
     /**
      * Constructor
      *
-     * @param $configuration   Configuration object
-     * @param $template        Which template file to load
-     * @param $defaultDictionary  The default dictionary where tags will come from.
+     * @param SimpleSAML_Configuration $configuration Configuration object
+     * @param string                   $template Which template file to load
+     * @param string|null              $defaultDictionary The default dictionary where tags will come from.
      */
-    function __construct(SimpleSAML_Configuration $configuration, $template, $defaultDictionary = NULL) {
+    public function __construct(SimpleSAML_Configuration $configuration, $template, $defaultDictionary = null)
+    {
         $this->configuration = $configuration;
         $this->template = $template;
-
         $this->data['baseurlpath'] = $this->configuration->getBaseURL();
-
-        $this->availableLanguages = $this->configuration->getArray('language.available', array('en'));
-
-        $this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language');
-        if (isset($_GET[$this->languageParameterName])) {
-            $this->setLanguage($_GET[$this->languageParameterName], $this->configuration->getBoolean('language.parameter.setcookie', TRUE));
-        }
-
-        if($defaultDictionary !== NULL && substr($defaultDictionary, -4) === '.php') {
-            // For backwards compatibility - print warning
-            $backtrace = debug_backtrace();
-            $where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
-            SimpleSAML_Logger::warning('Deprecated use of new SimpleSAML_Template(...) at ' . $where .
-                '. The last parameter is now a dictionary name, which should not end in ".php".');
-
-            $this->defaultDictionary = substr($defaultDictionary, 0, -4);
-        } else {
-            $this->defaultDictionary = $defaultDictionary;
-        }
-    }
-
-
-    /**
-     * setLanguage() will set a cookie for the user's browser to remember what language 
-     * was selected
-     * 
-     * @param $language    Language code for the language to set.
-     */
-    public function setLanguage($language, $setLanguageCookie = TRUE) {
-        $language = strtolower($language);
-        if (in_array($language, $this->availableLanguages, TRUE)) {
-            $this->language = $language;
-            if ($setLanguageCookie === TRUE) {
-                SimpleSAML_XHTML_Template::setLanguageCookie($language);
-            }
-        }
-    }
-
-    /**
-     * getLanguage() will return the language selected by the user, or the default language
-     * This function first looks for a cached language code,
-     * then checks for a language cookie,
-     * then it tries to calculate the preferred language from HTTP headers.
-     * Last it returns the default language.
-     */
-    public function getLanguage() {
-
-        // Language is set in object
-        if (isset($this->language)) {
-            return $this->language;
-        }
-
-        // Run custom getLanguage function if defined
-        $customFunction = $this->configuration->getArray('language.get_language_function', NULL);
-        if (isset($customFunction)) {
-            assert('is_callable($customFunction)');
-            $customLanguage = call_user_func($customFunction, $this);
-            if ($customLanguage !== NULL && $customLanguage !== FALSE) {
-                return $customLanguage;
-            }
-        }
-
-        // Language is provided in a stored COOKIE
-        $languageCookie = SimpleSAML_XHTML_Template::getLanguageCookie();
-        if ($languageCookie !== NULL) {
-            $this->language = $languageCookie;
-            return $languageCookie;
-        }
-
-        // Check if we can find a good language from the Accept-Language http header
-        $httpLanguage = $this->getHTTPLanguage();
-        if ($httpLanguage !== NULL) {
-            return $httpLanguage;
-        }
-
-        // Language is not set, and we get the default language from the configuration
-        return $this->getDefaultLanguage();
+        $this->translator = new SimpleSAML\Locale\Translate($configuration, $defaultDictionary = null);
     }
 
 
     /**
-     * This function gets the prefered language for the user based on the Accept-Language http header.
+     * Return the internal translator object used by this template.
      *
-     * @return The prefered language based on the Accept-Language http header, or NULL if none of the
-     *         languages in the header were available.
+     * @return \SimpleSAML\Locale\Translate The translator that will be used with this template.
      */
-    private function getHTTPLanguage() {
-        $languageScore = \SimpleSAML\Utils\HTTP::getAcceptLanguage();
-
-        /* For now we only use the default language map. We may use a configurable language map
-         * in the future.
-         */
-        $languageMap = self::$defaultLanguageMap;
-
-        // Find the available language with the best score
-        $bestLanguage = NULL;
-        $bestScore = -1.0;
-
-        foreach($languageScore as $language => $score) {
-
-            // Apply the language map to the language code
-            if(array_key_exists($language, $languageMap)) {
-                $language = $languageMap[$language];
-            }
-
-            if(!in_array($language, $this->availableLanguages, TRUE)) {
-                // Skip this language - we don't have it
-                continue;
-            }
-
-            /* Some user agents use very limited precicion of the quality value, but order the
-             * elements in descending order. Therefore we rely on the order of the output from
-             * getAcceptLanguage() matching the order of the languages in the header when two
-             * languages have the same quality.
-             */
-            if($score > $bestScore) {
-                $bestLanguage = $language;
-                $bestScore = $score;
-            }
-        }
-
-        return $bestLanguage;
+    public function getTranslator()
+    {
+        return $this->translator;
     }
 
 
     /**
-     * Returns the language default (from configuration)
+     * Show the template to the user.
      */
-    private function getDefaultLanguage() {
-        return $this->configuration->getString('language.default', 'en');
+    public function show()
+    {
+        $filename = $this->findTemplatePath($this->template);
+        require($filename);
     }
 
-    /**
-     * Returns a list of all available languages.
-     */
-    private function getLanguageList() {
-        $thisLang = $this->getLanguage();
-        $lang = array();
-        foreach ($this->availableLanguages AS $nl) {
-            $lang[$nl] = ($nl == $thisLang);
-        }
-        return $lang;
-    }
 
     /**
-     * Return TRUE if language is Right-to-Left.
-     */
-    private function isLanguageRTL() {
-        $rtlLanguages = $this->configuration->getArray('language.rtl', array());
-        $thisLang = $this->getLanguage();
-        if (in_array($thisLang, $rtlLanguages)) {
-            return TRUE;
-        }
-        return FALSE;
-    }
-
-    /**
-     * Includs a file relative to the template base directory.
-     * This function can be used to include headers and footers etc.
+     * Find template path.
      *
-     */    
-    private function includeAtTemplateBase($file) {
-        $data = $this->data;
-
-        $filename = $this->findTemplatePath($file);
-
-        include($filename);
-    }
-
-
-    /**
-     * Retrieve a dictionary.
+     * This function locates the given template based on the template name. It will first search for the template in
+     * the current theme directory, and then the default theme.
      *
-     * This function retrieves a dictionary with the given name.
+     * The template name may be on the form <module name>:<template path>, in which case it will search for the
+     * template file in the given module.
      *
-     * @param $name  The name of the dictionary, as the filename in the dictionary directory,
-     *               without the '.php'-ending.
-     * @return  An associative array with the dictionary.
-     */
-    private function getDictionary($name) {
-        assert('is_string($name)');
-
-        if(!array_key_exists($name, $this->dictionaries)) {
-            $sepPos = strpos($name, ':');
-            if($sepPos !== FALSE) {
-                $module = substr($name, 0, $sepPos);
-                $fileName = substr($name, $sepPos + 1);
-                $dictDir = SimpleSAML_Module::getModuleDir($module) . '/dictionaries/';
-            } else {
-                $dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
-                $fileName = $name;
-            }
-            $this->dictionaries[$name] = $this->readDictionaryFile($dictDir . $fileName);
-        }
-
-        return $this->dictionaries[$name];
-    }
-
-
-    /**
-     * Retrieve a tag.
+     * @param string $template The relative path from the theme directory to the template file.
      *
-     * This function retrieves a tag as an array with language => string mappings.
+     * @return string The absolute path to the template file.
      *
-     * @param $tag  The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve
-     *              a tag from the specific dictionary.
-     * @return As associative array with language => string mappings, or NULL if the tag wasn't found.
+     * @throws Exception If the template file couldn't be found.
      */
-    public function getTag($tag) {
-        assert('is_string($tag)');
-
-        // First check translations loaded by the includeInlineTranslation and includeLanguageFile methods
-        if(array_key_exists($tag, $this->langtext)) {
-            return $this->langtext[$tag];
-        }
+    private function findTemplatePath($template)
+    {
+        assert('is_string($template)');
 
-        // 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];
+        $tmp = explode(':', $template, 2);
+        if (count($tmp) === 2) {
+            $templateModule = $tmp[0];
+            $templateName = $tmp[1];
         } else {
-            $dictionary = $this->defaultDictionary;
-            if($dictionary === NULL) {
-                // We don't have any dictionary to load the tag from
-                return NULL;
-            }
+            $templateModule = 'default';
+            $templateName = $tmp[0];
         }
 
-        $dictionary = $this->getDictionary($dictionary);
-        if(!array_key_exists($tag, $dictionary)) {
-            return NULL;
+        $tmp = explode(':', $this->configuration->getString('theme.use', 'default'), 2);
+        if (count($tmp) === 2) {
+            $themeModule = $tmp[0];
+            $themeName = $tmp[1];
+        } else {
+            $themeModule = null;
+            $themeName = $tmp[0];
         }
 
-        return $dictionary[$tag];
-    }
-
-
-    /**
-     * Retrieve the preferred translation of a given text.
-     *
-     * @param $translations  The translations, as an associative array with language => text mappings.
-     * @return The preferred translation.
-     */
-    public function getTranslation($translations) {
-        assert('is_array($translations)');
+        // first check the current theme
+        if ($themeModule !== null) {
+            // .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName>
 
-        // Look up translation of tag in the selected language
-        $selected_language = $this->getLanguage();
-        if (array_key_exists($selected_language, $translations)) {
-            return $translations[$selected_language];
+            $filename = SimpleSAML_Module::getModuleDir($themeModule).
+                '/themes/'.$themeName.'/'.$templateModule.'/'.$templateName;
+        } elseif ($templateModule !== 'default') {
+            // .../module/<templateModule>/templates/<themeName>/<templateName>
+            $filename = SimpleSAML_Module::getModuleDir($templateModule).'/templates/'.$templateName;
+        } else {
+            // .../templates/<theme>/<templateName>
+            $filename = $this->configuration->getPathValue('templatedir', 'templates/').$templateName;
         }
 
-        // Look up translation of tag in the default language
-        $default_language = $this->getDefaultLanguage();
-        if(array_key_exists($default_language, $translations)) {
-            return $translations[$default_language];
+        if (file_exists($filename)) {
+            return $filename;
         }
 
-        // Check for english translation
-        if(array_key_exists('en', $translations)) {
-            return $translations['en'];
+        // not found in current theme
+        SimpleSAML_Logger::debug(
+            $_SERVER['PHP_SELF'].' - Template: Could not find template file ['. $template.'] at ['.
+            $filename.'] - now trying the base template'
+        );
+
+        // try default theme
+        if ($templateModule !== 'default') {
+            // .../module/<templateModule>/templates/<templateName>
+            $filename = SimpleSAML_Module::getModuleDir($templateModule).'/templates/'.$templateName;
+        } else {
+            // .../templates/<templateName>
+            $filename = $this->configuration->getPathValue('templatedir', 'templates/').'/'.$templateName;
         }
 
-        // Pick the first translation available
-        if(count($translations) > 0) {
-            $languages = array_keys($translations);
-            return $translations[$languages[0]];
+        if (file_exists($filename)) {
+            return $filename;
         }
 
-        // We don't have anything to return
-        throw new Exception('Nothing to return from translation.');
+        // not found in default template - log error and throw exception
+        $error = 'Template: Could not find template file ['.$template.'] at ['.$filename.']';
+        SimpleSAML_Logger::critical($_SERVER['PHP_SELF'].' - '.$error);
+
+        throw new Exception($error);
     }
 
 
-    /**
-     * Translate a attribute name.
-     *
-     * @param string $name  The attribute name.
-     * @return string  The translated attribute name, or the original attribute name if no translation was found.
+    /*
+     * Deprecated methods of this interface, all of them should go away.
      */
-    public function getAttributeTranslation($name) {
-
-        // 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->getTranslation($dict[$normName]);
-            }
-        }
-
-        // Search the default attribute dictionary
-        $dict = $this->getDictionary('attributes');
-        if (array_key_exists('attribute_' . $normName, $dict)) {
-            return $this->getTranslation($dict['attribute_' . $normName]);
-        }
-
-        // No translations found
-        return $name;
-    }
 
 
     /**
-     * Translate a tag into the current language, with a fallback to english.
+     * @param $name
      *
-     * This function is used to look up a translation tag in dictionaries, and return the
-     * translation into the current language. If no translation into the current language can be
-     * found, english will be tried, and if that fails, placeholder text will be returned.
-     *
-     * An array can be passed as the tag. In that case, the array will be assumed to be on the
-     * form (language => text), and will be used as the source of translations.
-     *
-     * This function can also do replacements into the translated tag. It will search the
-     * translated tag for the keys provided in $replacements, and replace any found occurances
-     * with the value of the key.
-     *
-     * @param string|array $tag  A tag name for the translation which should be looked up, or an
-     *                           array with (language => text) mappings.
-     * @param array $replacements  An associative array of keys that should be replaced with
-     *                             values in the translated string.
-     * @return string  The translated tag, or a placeholder value if the tag wasn't found.
+     * @return string
+     * @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
+     * instead.
      */
-    public function t($tag, $replacements = array(), $fallbackdefault = true, $oldreplacements = array(), $striptags = FALSE) {
-        if(!is_array($replacements)) {
-
-            // Old style call to t(...). Print warning to log.
-            $backtrace = debug_backtrace();
-            $where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
-            SimpleSAML_Logger::warning('Deprecated use of SimpleSAML_Template::t(...) at ' . $where .
-                '. Please update the code to use the new style of parameters.');
-
-            // For backwards compatibility
-            if(!$replacements && $this->getTag($tag) === NULL) {
-                SimpleSAML_Logger::warning('Code which uses $fallbackdefault === FALSE shouls be' .
-                    ' updated to use the getTag-method instead.');
-                return NULL;
-            }
+    public function getAttributeTranslation($name)
+    {
+        return $this->translator->getAttributeTranslation($name);
+    }
 
-            $replacements = $oldreplacements;
-        }
 
-        if(is_array($tag)) {
-            $tagData = $tag;
-        } else {
-            $tagData = $this->getTag($tag);
-            if($tagData === NULL) {
-                // Tag not found
-                SimpleSAML_Logger::info('Template: Looking up [' . $tag . ']: not translated at all.');
-                return $this->t_not_translated($tag, $fallbackdefault);
-            }
-        }
+    /**
+     * @return string
+     * @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
+     * instead.
+     */
+    public function getLanguage()
+    {
+        return $this->translator->getLanguage()->getLanguage();
+    }
 
-        $translated = $this->getTranslation($tagData);
 
-        foreach ($replacements as $k => $v) {
-            // try to translate if no replacement is given
-            if ($v == NULL) $v = $this->t($k);
-            $translated = str_replace($k, $v, $translated);
-        }
-        return $translated;
+    /**
+     * @param      $language
+     * @param bool $setLanguageCookie
+     * @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguage()
+     * instead.
+     */
+    public function setLanguage($language, $setLanguageCookie = true)
+    {
+        $this->translator->getLanguage()->setLanguage($language, $setLanguageCookie);
     }
 
 
     /**
-     * Return the string that should be used when no translation was found.
-     *
-     * @param $tag                A name tag of the string that should be returned.
-     * @param $fallbacktag        If set to TRUE and string was not found in any languages, return 
-     *                     the tag it self. If FALSE return NULL.
+     * @return null|string
+     * @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguageCookie()
+     * instead.
      */
-    private function t_not_translated($tag, $fallbacktag) {
-        if ($fallbacktag) {
-            return 'not translated (' . $tag . ')';
-        } else {
-            return $tag;
-        }
+    public static function getLanguageCookie()
+    {
+        return \SimpleSAML\Locale\Language::getLanguageCookie();
     }
 
 
     /**
-     * You can include translation inline instead of putting translation
-     * in dictionaries. This function is reccomended to only be used from dynamic
-     * data, or when the translation is already provided from an external source, as
-     * a database or in metadata.
-     *
-     * @param $tag         The tag that has a translation
-     * @param $translation The translation array
+     * @param $language
+     * @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguageCookie()
+     * instead.
      */
-    public function includeInlineTranslation($tag, $translation) {
-        if (is_string($translation)) {
-            $translation = array('en' => $translation);
-        } elseif (!is_array($translation)) {
-            throw new Exception("Inline translation should be string or array. Is " . gettype($translation) . " now!");
-        }
-        SimpleSAML_Logger::debug('Template: Adding inline language translation for tag [' . $tag . ']');
-        $this->langtext[$tag] = $translation;
+    public static function setLanguageCookie($language)
+    {
+        \SimpleSAML\Locale\Language::setLanguageCookie($language);
     }
 
     /**
-     * Include language file from the dictionaries directory.
-     *
-     * @param $file         File name of dictionary to include
-     * @param $otherConfig  Optionally provide a different configuration object than
-     *  the one provided in the constructor to be used to find the dictionary directory.
-     *  This enables the possiblity of combining dictionaries inside SimpleSAMLphp
-     *  distribution with external dictionaries.
+     * Wraps Language->getLanguageList
      */
-    public function includeLanguageFile($file, $otherConfig = null) {
-        $filebase = null;
-        if (!empty($otherConfig)) {
-            $filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
-        } else {
-            $filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
-        }
-
-        $lang = $this->readDictionaryFile($filebase . $file);
-        SimpleSAML_Logger::debug('Template: Merging language array. Loading [' . $file . ']');
-        $this->langtext = array_merge($this->langtext, $lang);
+    private function getLanguageList() {
+        return $this->translator->getLanguage()->getLanguageList();
     }
 
 
     /**
-     * Read a dictionary file in json format.
+     * @param $tag
      *
-     * @param string $filename  The absolute path to the dictionary file, minus the .definition.json ending.
-     * @return array  The translation array from the file.
+     * @return array
+     * @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::getTag() instead.
      */
-    private function readDictionaryJSON($filename) {
-        $definitionFile = $filename . '.definition.json';
-        assert('file_exists($definitionFile)');
-
-        $fileContent = file_get_contents($definitionFile);
-        $lang = json_decode($fileContent, TRUE);
-
-        if (empty($lang)) {
-            SimpleSAML_Logger::error('Invalid dictionary definition file [' . $definitionFile . ']');
-            return array();
-        }
-
-        $translationFile = $filename . '.translation.json';
-        if (file_exists($translationFile)) {
-            $fileContent = file_get_contents($translationFile);
-            $moreTrans = json_decode($fileContent, TRUE);
-            if (!empty($moreTrans)) {
-                $lang = self::lang_merge($lang, $moreTrans);
-            }
-        }
-
-        return $lang;
+    public function getTag($tag)
+    {
+        return $this->translator->getTag($tag);
     }
 
 
     /**
-     * Read a dictionary file in PHP format.
+     * Temporary wrapper for SimpleSAML\Locale\Translate::getPreferredTranslation().
      *
-     * @param string $filename  The absolute path to the dictionary file.
-     * @return array  The translation array from the file.
+     * @deprecated This method will be removed in SSP 2.0. Please use
+     * SimpleSAML\Locale\Translate::getPreferredTranslation() instead.
      */
-    private function readDictionaryPHP($filename) {
-        $phpFile = $filename . '.php';
-        assert('file_exists($phpFile)');
-
-        $lang = NULL;
-        include($phpFile);
-        if (isset($lang)) {
-            return $lang;
-        }
-
-        return array();
+    public function getTranslation($translations)
+    {
+        return $this->translator->getPreferredTranslation($translations);
     }
 
 
     /**
-     * Read a dictionary file.
+     * Includes a file relative to the template base directory.
+     * This function can be used to include headers and footers etc.
      *
-     * @param $filename  The absolute path to the dictionary file.
-     * @return The translation array which was found in the dictionary file.
      */
-    private function readDictionaryFile($filename) {
-        assert('is_string($filename)');
-
-        SimpleSAML_Logger::debug('Template: Reading [' . $filename . ']');
-
-        $jsonFile = $filename . '.definition.json';
-        if (file_exists($jsonFile)) {
-            return $this->readDictionaryJSON($filename);
-        }
-
+    private function includeAtTemplateBase($file) {
+        $data = $this->data;
 
-        $phpFile = $filename . '.php';
-        if (file_exists($phpFile)) {
-            return $this->readDictionaryPHP($filename);
-        }
+        $filename = $this->findTemplatePath($file);
 
-        SimpleSAML_Logger::error($_SERVER['PHP_SELF'].' - Template: Could not find template file [' . $this->template . '] at [' . $filename . ']');
-        return array();
+        include($filename);
     }
 
 
-    // Merge two translation arrays
-    public static function lang_merge($def, $lang) {
-        foreach($def AS $key => $value) {
-            if (array_key_exists($key, $lang))
-                $def[$key] = array_merge($value, $lang[$key]);
-        }
-        return $def;
+    /**
+     * Wraps Translate->includeInlineTranslation()
+     *
+     * @see \SimpleSAML\Locale\Translate::includeInlineTranslation()
+     * @deprecated This method will be removed in SSP 2.0. Please use
+     * \SimpleSAML\Locale\Translate::includeInlineTranslation() instead.
+     */
+    public function includeInlineTranslation($tag, $translation)
+    {
+        $this->translator->includeInlineTranslation($tag, $translation);
     }
 
 
     /**
-     * Show the template to the user.
+     * @param      $file
+     * @param null $otherConfig
+     * @deprecated This method will be removed in SSP 2.0. Please use
+     * \SimpleSAML\Locale\Translate::includeLanguageFile() instead.
      */
-    public function show() {
-
-        $filename = $this->findTemplatePath($this->template);
-        require($filename);
+    public function includeLanguageFile($file, $otherConfig = null)
+    {
+        $this->translator->includeLanguageFile($file, $otherConfig);
     }
 
 
     /**
-     * Find template path.
-     *
-     * This function locates the given template based on the template name.
-     * It will first search for the template in the current theme directory, and
-     * then the default theme.
-     *
-     * The template name may be on the form <module name>:<template path>, in which case
-     * it will search for the template file in the given module.
-     *
-     * An error will be thrown if the template file couldn't be found.
-     *
-     * @param string $template  The relative path from the theme directory to the template file.
-     * @return string  The absolute path to the template file.
+     * Wrap Language->isLanguageRTL
      */
-    private function findTemplatePath($template) {
-        assert('is_string($template)');
-
-        $tmp = explode(':', $template, 2);
-        if (count($tmp) === 2) {
-            $templateModule = $tmp[0];
-            $templateName = $tmp[1];
-        } else {
-            $templateModule = 'default';
-            $templateName = $tmp[0];
-        }
-
-        $tmp = explode(':', $this->configuration->getString('theme.use', 'default'), 2);
-        if (count($tmp) === 2) {
-            $themeModule = $tmp[0];
-            $themeName = $tmp[1];
-        } else {
-            $themeModule = NULL;
-            $themeName = $tmp[0];
-        }
-
-
-        // First check the current theme
-        if ($themeModule !== NULL) {
-            // .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName>
-
-            $filename = SimpleSAML_Module::getModuleDir($themeModule) . '/themes/' . $themeName . '/' . $templateModule . '/' . $templateName;
-        } elseif ($templateModule !== 'default') {
-            // .../module/<templateModule>/templates/<themeName>/<templateName>
-            $filename = SimpleSAML_Module::getModuleDir($templateModule) . '/templates/' . $templateName;
-        } else {
-            // .../templates/<theme>/<templateName>
-            $filename = $this->configuration->getPathValue('templatedir', 'templates/') . $templateName;
-        }
-
-        if (file_exists($filename)) {
-            return $filename;
-        }
-
-
-        // Not found in current theme
-        SimpleSAML_Logger::debug($_SERVER['PHP_SELF'].' - Template: Could not find template file [' .
-            $template . '] at [' . $filename . '] - now trying the base template');
-
-
-        // Try default theme
-        if ($templateModule !== 'default') {
-            // .../module/<templateModule>/templates/<templateName>
-            $filename = SimpleSAML_Module::getModuleDir($templateModule) . '/templates/' . $templateName;
-        } else {
-            // .../templates/<templateName>
-            $filename = $this->configuration->getPathValue('templatedir', 'templates/') . '/' . $templateName;
-        }
-
-        if (file_exists($filename)) {
-            return $filename;
-        }
-
-
-        // Not found in default template - log error and throw exception
-        $error = 'Template: Could not find template file [' . $template . '] at [' . $filename . ']';
-        SimpleSAML_Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);
-
-        throw new Exception($error);
+    private function isLanguageRTL() {
+        return $this->translator->getLanguage()->isLanguageRTL();
     }
 
 
     /**
-     * Retrieve the user-selected language from a cookie.
+     * Merge two translation arrays.
      *
-     * @return string|NULL  The language, or NULL if unset.
+     * @param array $def The array holding string definitions.
+     * @param array $lang The array holding translations for every string.
+     * @return array The recursive merge of both arrays.
+     * @deprecated This method will be removed in SimpleSAMLphp 2.0. Please use array_merge_recursive() instead.
      */
-    public static function getLanguageCookie() {
-        $config = SimpleSAML_Configuration::getInstance();
-        $availableLanguages = $config->getArray('language.available', array('en'));
-        $name = $config->getString('language.cookie.name', 'language');
-
-        if (isset($_COOKIE[$name])) {
-            $language = strtolower((string)$_COOKIE[$name]);
-            if (in_array($language, $availableLanguages, TRUE)) {
-                return $language;
+    public static function lang_merge($def, $lang)
+    {
+        foreach ($def as $key => $value) {
+            if (array_key_exists($key, $lang)) {
+                $def[$key] = array_merge($value, $lang[$key]);
             }
         }
-
-        return NULL;
+        return $def;
     }
 
 
     /**
-     * Set the user-selected language in a cookie.
+     * Wrap Language->t to translate tag into the current language, with a fallback to english.
      *
-     * @param string $language  The language.
+     * @see \SimpleSAML\Locale\Translate::t()
+     * @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::t() instead.
      */
-    public static function setLanguageCookie($language) {
-        assert('is_string($language)');
-
-        $language = strtolower($language);
-        $config = SimpleSAML_Configuration::getInstance();
-        $availableLanguages = $config->getArray('language.available', array('en'));
-
-        if (!in_array($language, $availableLanguages, TRUE) || headers_sent()) {
-            return;
-        }
-
-        $name = $config->getString('language.cookie.name', 'language');
-        $params = array(
-            'lifetime' => ($config->getInteger('language.cookie.lifetime', 60*60*24*900)),
-            'domain' => ($config->getString('language.cookie.domain', NULL)),
-            'path' => ($config->getString('language.cookie.path', '/')),
-            'httponly' => FALSE,
-        );
-
-        \SimpleSAML\Utils\HTTP::setCookie($name, $language, $params, FALSE);
+    public function t(
+        $tag,
+        $replacements = array(),
+        $fallbackdefault = true,
+        $oldreplacements = array(),
+        $striptags = false
+    ) {
+        return $this->translator->t($tag, $replacements, $fallbackdefault, $oldreplacements, $striptags);
     }
-
 }
diff --git a/modules/consent/templates/consentform.php b/modules/consent/templates/consentform.php
index 198dd8d72e56439d63332bf3c18be70618fb587f..d32a1a1289fa29b2a29e7f32a9cfdfab623c0eb5 100644
--- a/modules/consent/templates/consentform.php
+++ b/modules/consent/templates/consentform.php
@@ -72,7 +72,7 @@ if (array_key_exists('descr_purpose', $this->data['dstMetadata'])) {
         '{consent:consent:consent_purpose}',
         array(
             'SPNAME' => $dstName,
-            'SPDESC' => $this->getTranslation(
+            'SPDESC' => $this->getTranslator()->getPreferredTranslation(
                 SimpleSAML\Utils\Arrays::arrayize(
                     $this->data['dstMetadata']['descr_purpose'],
                     'en'
@@ -139,6 +139,8 @@ if ($this->data['sppp'] !== false) {
  */
 function present_attributes($t, $attributes, $nameParent)
 {
+    $translator = $t->getTranslator();
+
     $alternate = array('odd', 'even');
     $i = 0;
     $summary = 'summary="' . $t->t('{consent:consent:table_summary}') . '"';
@@ -155,7 +157,7 @@ function present_attributes($t, $attributes, $nameParent)
 
     foreach ($attributes as $name => $value) {
         $nameraw = $name;
-        $name = $t->getAttributeTranslation($parentStr . $nameraw);
+        $name = $translator->getAttributeTranslation($parentStr . $nameraw);
 
         if (preg_match('/^child_/', $nameraw)) {
             // insert child table
diff --git a/modules/consentAdmin/templates/consentadmin.php b/modules/consentAdmin/templates/consentadmin.php
index 496d5145280279baae13a1059e20010d395d70e7..015c44e2807ba71736fbcbcb7d59ee9b6b3a051a 100644
--- a/modules/consentAdmin/templates/consentadmin.php
+++ b/modules/consentAdmin/templates/consentadmin.php
@@ -1,7 +1,7 @@
 <?php $this->includeAtTemplateBase('includes/header.php'); ?>
 <!--  default theme -->
 <?php 
-$this->includeLanguageFile('attributes.php'); // attribute listings translated by this dictionary
+$this->getTranslator()->includeLanguageFile('attributes.php'); // attribute listings translated by this dictionary
  
 ?> 
 
@@ -72,8 +72,8 @@ span.showhide {
 			$hide_text = $this->t('hide');
 			$attributes_text = $this->t('attributes_text');
 			foreach ($spList AS $spName => $spValues) {
-				$this->includeInlineTranslation('spname', $spValues['name']);
-				$this->includeInlineTranslation('spdescription', $spValues['description']);
+				$this->getTranslator()->includeInlineTranslation('spname', $spValues['name']);
+				$this->getTranslator()->includeInlineTranslation('spdescription', $spValues['description']);
                 if (!is_null($spValues['serviceurl'])) {
                     $htmlSpName = '<a href="' . $spValues['serviceurl'] . '" style="color: black; font-weight: bold;">' . htmlspecialchars($this->t('spname', array(), false, true)) . '</a>';
                 } else {
@@ -101,7 +101,7 @@ TRSTART;
 				if (isset($this->data['attribute_' . htmlspecialchars(strtolower($name)) ])) {
 				  $name = $this->data['attribute_' . htmlspecialchars(strtolower($name))];
 				}
-				$name = $this->getAttributeTranslation($name); // translate
+				$name = $this->getTranslator()->getAttributeTranslation($name); // translate
 				if (sizeof($value) > 1) {
 						echo "<li>" . htmlspecialchars($name) . ":\n<ul>\n";
 						foreach ($value AS $v) {
diff --git a/modules/consentAdmin/www/consentAdmin.php b/modules/consentAdmin/www/consentAdmin.php
index 717fa3c28c7971446afc74776f2002aa834d44b4..a6b3df9a2ede14f3a37bbfa65800835257151fd1 100644
--- a/modules/consentAdmin/www/consentAdmin.php
+++ b/modules/consentAdmin/www/consentAdmin.php
@@ -205,8 +205,9 @@ $template_sp_content = array();
 
 // Init template
 $template = new SimpleSAML_XHTML_Template($config, 'consentAdmin:consentadmin.php', 'consentAdmin:consentadmin');
-$sp_empty_name = $template->getTag('sp_empty_name');
-$sp_empty_description = $template->getTag('sp_empty_description');
+$translator = $template->getTranslator();
+$sp_empty_name = $translator->getTag('sp_empty_name');
+$sp_empty_description = $translator->getTag('sp_empty_description');
 
 // Process consents for all SP
 foreach ($all_sp_metadata as $sp_entityid => $sp_values) {
diff --git a/modules/core/lib/Auth/Process/LanguageAdaptor.php b/modules/core/lib/Auth/Process/LanguageAdaptor.php
index 347a1fd4a0bc4b0ce303a2d4662a80d986517fdf..cbc9478e2beac57fd980f0ecb0c8644a660a0952 100644
--- a/modules/core/lib/Auth/Process/LanguageAdaptor.php
+++ b/modules/core/lib/Auth/Process/LanguageAdaptor.php
@@ -44,7 +44,7 @@ class sspmod_core_Auth_Process_LanguageAdaptor extends SimpleSAML_Auth_Processin
 		if (array_key_exists($this->langattr, $attributes))
 			$attrlang = $attributes[$this->langattr][0];
 
-		$lang = SimpleSAML_XHTML_Template::getLanguageCookie();
+		$lang = SimpleSAML\Locale\Language::getLanguageCookie();
 
 
 		if (isset($attrlang))
@@ -55,7 +55,7 @@ class sspmod_core_Auth_Process_LanguageAdaptor extends SimpleSAML_Auth_Processin
 
 		if (isset($attrlang) && !isset($lang)) {
 			// Language set in attribute but not in cookie - update cookie
-			SimpleSAML_XHTML_Template::setLanguageCookie($attrlang);
+			SimpleSAML\Locale\Language::setLanguageCookie($attrlang);
 		} elseif (!isset($attrlang) && isset($lang)) {
 			// Language set in cookie, but not in attribute. Update attribute
 			$request['Attributes'][$this->langattr] = array($lang);
diff --git a/modules/core/templates/frontpage_federation.tpl.php b/modules/core/templates/frontpage_federation.tpl.php
index c4e18d247204b194545efacc70a2337c7d1440b9..d69843206c9a55661173f3b9f9cefdc7be506c3c 100644
--- a/modules/core/templates/frontpage_federation.tpl.php
+++ b/modules/core/templates/frontpage_federation.tpl.php
@@ -54,11 +54,13 @@ if (is_array($this->data['metaentries']['hosted']) && count($this->data['metaent
             echo '<br />Index: '.$hm['metadata-index'];
         }
         if (!empty($hm['name'])) {
-            echo '<br /><strong>'.$this->getTranslation(SimpleSAML\Utils\Arrays::arrayize($hm['name'], 'en')).
+            echo '<br /><strong>'.
+                $this->getTranslator()->getPreferredTranslation(SimpleSAML\Utils\Arrays::arrayize($hm['name'], 'en')).
                 '</strong>';
         }
         if (!empty($hm['descr'])) {
-            echo '<br /><strong>'.$this->getTranslation(SimpleSAML\Utils\Arrays::arrayize($hm['descr'], 'en')).
+            echo '<br /><strong>'.
+                $this->getTranslator()->getPreferredTranslation(SimpleSAML\Utils\Arrays::arrayize($hm['descr'], 'en')).
                 '</strong>';
         }
 
@@ -84,11 +86,13 @@ if (is_array($this->data['metaentries']['remote']) && count($this->data['metaent
                     )
                 ).'">');
             if (!empty($entry['name'])) {
-                echo htmlspecialchars($this->getTranslation(SimpleSAML\Utils\Arrays::arrayize($entry['name'], 'en')));
+                echo htmlspecialchars($this->getTranslator()->getPreferredTranslation(
+                    SimpleSAML\Utils\Arrays::arrayize($entry['name'], 'en')
+                ));
             } elseif (!empty($entry['OrganizationDisplayName'])) {
-                echo htmlspecialchars(
-                    $this->getTranslation(SimpleSAML\Utils\Arrays::arrayize($entry['OrganizationDisplayName'], 'en'))
-                );
+                echo htmlspecialchars($this->getTranslator()->getPreferredTranslation(
+                    SimpleSAML\Utils\Arrays::arrayize($entry['OrganizationDisplayName'], 'en')
+                ));
             } else {
                 echo htmlspecialchars($entry['entityid']);
             }
diff --git a/modules/core/templates/logout-iframe.php b/modules/core/templates/logout-iframe.php
index e6d041e6c1878419681c41485b0100949f944fb3..e71c2f266a5aecebe0406398f3a74e496046e7bf 100644
--- a/modules/core/templates/logout-iframe.php
+++ b/modules/core/templates/logout-iframe.php
@@ -42,7 +42,7 @@ foreach ($SPs as $assocId => $sp) {
 }
 
 if ($from !== NULL) {
-	$from = $this->getTranslation($from);
+	$from = $this->getTranslator()->getPreferredTranslation($from);
 }
 
 
@@ -99,7 +99,7 @@ echo '<table id="slostatustable">';
 
 foreach ($SPs AS $assocId => $sp) {
 	if (isset($sp['core:Logout-IFrame:Name'])) {
-		$spName = $this->getTranslation($sp['core:Logout-IFrame:Name']);
+		$spName = $this->getTranslator()->getPreferredTranslation($sp['core:Logout-IFrame:Name']);
 	} else {
 		$spName = $assocId;
 	}
diff --git a/modules/discopower/templates/disco-tpl.php b/modules/discopower/templates/disco-tpl.php
index 752badc741ed5f08d37966aac2077dc36d44cc0c..8f1e4e279ee7fbbfcc9a4320374a5550198911f5 100644
--- a/modules/discopower/templates/disco-tpl.php
+++ b/modules/discopower/templates/disco-tpl.php
@@ -84,13 +84,13 @@ function getTranslatedName($t, $metadata) {
 		$displayName = $metadata['UIInfo']['DisplayName'];
 		assert('is_array($displayName)'); // Should always be an array of language code -> translation
 		if (!empty($displayName)) {
-			return $t->getTranslation($displayName);
+			return $t->getTranslator()->getPreferredTranslation($displayName);
 		}
 	}
 
 	if (array_key_exists('name', $metadata)) {
 		if (is_array($metadata['name'])) {
-			return $t->getTranslation($metadata['name']);
+			return $t->getTranslator()->getPreferredTranslation($metadata['name']);
 		} else {
 			return $metadata['name'];
 		}
diff --git a/templates/includes/attributes.php b/templates/includes/attributes.php
index 2f960c46c0bd2209264b9d1934e2750eeaaf44f5..48eb0cc78c9c9b4d509d3f2d7d16863bb8225115 100644
--- a/templates/includes/attributes.php
+++ b/templates/includes/attributes.php
@@ -30,7 +30,7 @@ function present_assoc($attr) {
 	}
 }
 
-function present_attributes($t, $attributes, $nameParent) {
+function present_attributes(SimpleSAML_XHTML_Template $t, $attributes, $nameParent) {
 	$alternate = array('odd', 'even'); $i = 0;
 	
 	$parentStr = (strlen($nameParent) > 0)? strtolower($nameParent) . '_': '';
@@ -40,7 +40,7 @@ function present_attributes($t, $attributes, $nameParent) {
 	foreach ($attributes as $name => $value) {
 	
 		$nameraw = $name;
-		$name = $t->getAttributeTranslation($parentStr . $nameraw);
+		$name = $t->getTranslator()->getAttributeTranslation($parentStr . $nameraw);
 
 		if (preg_match('/^child_/', $nameraw)) {
 			$parentName = preg_replace('/^child_/', '', $nameraw);
diff --git a/templates/includes/header.php b/templates/includes/header.php
index 4d07f1d37de7857b3245568416bdf4a31d02a745..f0c4e4df3ea3567cc0f47bb2796c022711af74c9 100644
--- a/templates/includes/header.php
+++ b/templates/includes/header.php
@@ -191,7 +191,7 @@ if($onLoad !== '') {
 				if ($current) {
 					$textarray[] = $langnames[$lang];
 				} else {
-					$textarray[] = '<a href="' . htmlspecialchars(\SimpleSAML\Utils\HTTP::addURLParameters(\SimpleSAML\Utils\HTTP::getSelfURL(), array($this->languageParameterName => $lang))) . '">' .
+					$textarray[] = '<a href="' . htmlspecialchars(\SimpleSAML\Utils\HTTP::addURLParameters(\SimpleSAML\Utils\HTTP::getSelfURL(), array($this->getTranslator()->getLanguage()->getLanguageParameterName() => $lang))) . '">' .
 						$langnames[$lang] . '</a>';
 				}
 			}
diff --git a/templates/logout.php b/templates/logout.php
index 885f3a54b768a42900159dde64c032421de88500..0672ce7259115e49ae0c80d9e1eafc390ca7dd45 100644
--- a/templates/logout.php
+++ b/templates/logout.php
@@ -9,7 +9,7 @@ $this->includeAtTemplateBase('includes/header.php');
 echo('<h2>' . $this->data['header'] . '</h2>');
 echo('<p>' . $this->t('{logout:logged_out_text}') . '</p>');
 
-if($this->getTag($this->data['text']) !== NULL) {
+if($this->getTranslator()->getTag($this->data['text']) !== NULL) {
 	$this->data['text'] = $this->t($this->data['text']);
 }
 echo('<p>[ <a href="' . htmlspecialchars($this->data['link']) . '">' .
diff --git a/templates/selectidp-dropdown.php b/templates/selectidp-dropdown.php
index 1bb13def6b42009c7ed04f60869cae8ded7153bc..b9c3b73a7a682b4c2348e13503e7a93fdea0ffb0 100644
--- a/templates/selectidp-dropdown.php
+++ b/templates/selectidp-dropdown.php
@@ -9,12 +9,18 @@ $this->includeAtTemplateBase('includes/header.php');
 
 foreach ($this->data['idplist'] as $idpentry) {
     if (!empty($idpentry['name'])) {
-        $this->includeInlineTranslation('idpname_'.$idpentry['entityid'], $idpentry['name']);
+        $this->getTranslator()->includeInlineTranslation(
+            'idpname_'.$idpentry['entityid'],
+            $idpentry['name']
+        );
     } elseif (!empty($idpentry['OrganizationDisplayName'])) {
-        $this->includeInlineTranslation('idpname_'.$idpentry['entityid'], $idpentry['OrganizationDisplayName']);
+        $this->getTranslator()->includeInlineTranslation(
+            'idpname_'.$idpentry['entityid'],
+            $idpentry['OrganizationDisplayName']
+        );
     }
     if (!empty($idpentry['description'])) {
-        $this->includeInlineTranslation('idpdesc_'.$idpentry['entityid'], $idpentry['description']);
+        $this->getTranslator()->includeInlineTranslation('idpdesc_'.$idpentry['entityid'], $idpentry['description']);
     }
 }
 ?>
@@ -28,6 +34,7 @@ foreach ($this->data['idplist'] as $idpentry) {
         <select id="dropdownlist" name="idpentityid">
             <?php
             usort($this->data['idplist'], function ($idpentry1, $idpentry2) {
+                // TODO: this is only compatible with PHP >= 5.4, fix compat with 5.3!
                 return strcmp($this->t('idpname_'.$idpentry1['entityid']), $this->t('idpname_'.$idpentry2['entityid']));
             });
 
diff --git a/templates/selectidp-links.php b/templates/selectidp-links.php
index 079e8be664fb089875e76a790ee6353715a7e3ed..b8f2d6e3d89d6c6dd40244993108b3e1d8bfcded 100644
--- a/templates/selectidp-links.php
+++ b/templates/selectidp-links.php
@@ -8,12 +8,15 @@ $this->data['autofocus'] = 'preferredidp';
 $this->includeAtTemplateBase('includes/header.php');
 foreach ($this->data['idplist'] as $idpentry) {
     if (isset($idpentry['name'])) {
-        $this->includeInlineTranslation('idpname_'.$idpentry['entityid'], $idpentry['name']);
+        $this->getTranslator()->includeInlineTranslation('idpname_'.$idpentry['entityid'], $idpentry['name']);
     } elseif (isset($idpentry['OrganizationDisplayName'])) {
-        $this->includeInlineTranslation('idpname_'.$idpentry['entityid'], $idpentry['OrganizationDisplayName']);
+        $this->getTranslator()->includeInlineTranslation(
+            'idpname_'.$idpentry['entityid'],
+            $idpentry['OrganizationDisplayName']
+        );
     }
     if (isset($idpentry['description'])) {
-        $this->includeInlineTranslation('idpdesc_'.$idpentry['entityid'], $idpentry['description']);
+        $this->getTranslator()->includeInlineTranslation('idpdesc_'.$idpentry['entityid'], $idpentry['description']);
     }
 }
 ?>
diff --git a/templates/status.php b/templates/status.php
index 23f8877182644e2b34ac6ca440d22a978b76dcd2..25b6e020792418f7801ddc3b1ee0dccf87b0a6fc 100644
--- a/templates/status.php
+++ b/templates/status.php
@@ -1,6 +1,6 @@
 <?php
 if (array_key_exists('header', $this->data)) {
-    if ($this->getTag($this->data['header']) !== null) {
+    if ($this->getTranslator()->getTag($this->data['header']) !== null) {
         $this->data['header'] = $this->t($this->data['header']);
     }
 }