diff --git a/config-templates/config.php b/config-templates/config.php
index 8191be3d18759106d120187a1347ef6e41476454..aa11e1f4f37d0c9f68bc3b7273fdc7f73c36acb1 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..d7b830814093ea2dba58d9352aaf1b008d68a24f
--- /dev/null
+++ b/lib/SimpleSAML/Locale/Language.php
@@ -0,0 +1,230 @@
+<?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_Locale_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.
+     */
+    public $languageParameterName = 'language';
+
+
+    /**
+     * Constructor
+     *
+     * @param $configuration   Configuration object
+     * @param $defaultDictionary  The default dictionary where tags will come from.
+     */
+    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));
+        }
+    }
+
+
+    /**
+     * 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_Locale_Language::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_Locale_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();
+    }
+
+
+    /**
+     * This function gets the prefered language for the user based on the Accept-Language http header.
+     *
+     * @return The prefered language based on the Accept-Language http header, or NULL if none of the
+     *         languages in the header were available.
+     */
+    private function getHTTPLanguage() {
+        $languageScore = SimpleSAML_Utilities::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;
+    }
+
+
+    /**
+     * Returns the language default (from configuration)
+     */
+    public function getDefaultLanguage() {
+        return $this->configuration->getString('language.default', 'en');
+    }
+
+    /**
+     * Returns a list of all available languages.
+     */
+    public 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.
+     */
+    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 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;
+    }
+
+
+    /**
+     * Set the user-selected language in a cookie.
+     *
+     * @param string $language  The language.
+     */
+    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_Utilities::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..a6e57b43623e0ccd7b0f91ff356cd8ef7b83f188
--- /dev/null
+++ b/lib/SimpleSAML/Locale/Translate.php
@@ -0,0 +1,400 @@
+<?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_Locale_Translate {
+
+    private $configuration = null;
+
+    private $langtext = array();
+
+
+    /**
+     * Associative array of dictionaries.
+     */
+    private $dictionaries = array();
+
+
+    /**
+     * The default dictionary.
+     */
+    private $defaultDictionary = NULL;
+
+
+    /**
+     * Constructor
+     *
+     * @param $configuration   Configuration object
+     * @param $defaultDictionary  The default dictionary where tags will come from.
+     */
+    function __construct(SimpleSAML_Configuration $configuration, $defaultDictionary = NULL) {
+        $this->configuration = $configuration;
+        $this->language = new SimpleSAML_Locale_Language($configuration);
+
+        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_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;
+        }
+    }
+
+
+    /**
+     * Retrieve a dictionary.
+     *
+     * This function retrieves a dictionary with the given name.
+     *
+     * @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.
+     *
+     * This function retrieves a tag as an array with language => string mappings.
+     *
+     * @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.
+     */
+    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 $translations  The translations, as an associative array with language => text mappings.
+     * @return The preferred translation.
+     */
+    public function getTranslation($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 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.
+     */
+    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.
+     *
+     * 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.
+     */
+    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;
+            }
+
+            $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);
+            }
+        }
+
+        $translated = $this->getTranslation($tagData);
+
+#        if (!empty($replacements)){        echo('<pre> [' . $tag . ']'); print_r($replacements); exit; }
+        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 $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.
+     */
+    private function t_not_translated($tag, $fallbacktag) {
+        if ($fallbacktag) {
+            return 'not translated (' . $tag . ')';
+        } else {
+            return $tag;
+        }
+    }
+
+
+    /**
+     * 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
+     */
+    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 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.
+     */
+    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);
+    }
+
+
+    /**
+     * Read a dictionary file in json format.
+     *
+     * @param string $filename  The absolute path to the dictionary file, minus the .definition.json ending.
+     * @return array  The translation array from 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 = self::lang_merge($lang, $moreTrans);
+            }
+        }
+
+        return $lang;
+    }
+
+
+    /**
+     * Read a dictionary file in PHP format.
+     *
+     * @param string $filename  The absolute path to the dictionary file.
+     * @return array  The translation array from 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 $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);
+        }
+
+
+        $phpFile = $filename . '.php';
+        if (file_exists($phpFile)) {
+            return $this->readDictionaryPHP($filename);
+        }
+
+        SimpleSAML_Logger::error($_SERVER['PHP_SELF'].' - Template: Could not find template file [' . $this->template . '] at [' . $filename . ']');
+        return array();
+    }
+
+
+    // 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;
+    }
+
+
+}
diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php
index ab20e116982aa3d7746bffdfb3dc5b706e2b609b..afe8e8e9fb0cacbf173145b80d3381cbf95a21c6 100644
--- a/lib/SimpleSAML/XHTML/Template.php
+++ b/lib/SimpleSAML/XHTML/Template.php
@@ -8,41 +8,12 @@
  */
 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.
-     */
-    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;
 
 
-    /**
-     * Associative array of dictionaries.
-     */
-    private $dictionaries = array();
-
-
-    /**
-     * The default dictionary.
-     */
-    private $defaultDictionary = NULL;
-
-
-    /**
-     * HTTP GET language parameter name.
-     */
-    private $languageParameterName = 'language';
-
-
     /**
      * Constructor
      *
@@ -53,169 +24,15 @@ class SimpleSAML_XHTML_Template {
     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 function gets the prefered language for the user based on the Accept-Language http header.
-     *
-     * @return The prefered language based on the Accept-Language http header, or NULL if none of the
-     *         languages in the header were available.
-     */
-    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;
-    }
-
-
-    /**
-     * Returns the language default (from configuration)
-     */
-    private function getDefaultLanguage() {
-        return $this->configuration->getString('language.default', 'en');
-    }
-
-    /**
-     * 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;
+        $this->translator = new SimpleSAML_Locale_Translate($configuration, $defaultDictionary = NULL);
     }
 
     /**
      * Includs a file relative to the template base directory.
      * This function can be used to include headers and footers etc.
      *
-     */    
+     */
     private function includeAtTemplateBase($file) {
         $data = $this->data;
 
@@ -224,347 +41,34 @@ class SimpleSAML_XHTML_Template {
         include($filename);
     }
 
-
-    /**
-     * Retrieve a dictionary.
-     *
-     * This function retrieves a dictionary with the given name.
-     *
-     * @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.
-     *
-     * This function retrieves a tag as an array with language => string mappings.
-     *
-     * @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.
-     */
-    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 $translations  The translations, as an associative array with language => text mappings.
-     * @return The preferred translation.
+     * Wrap Translate->getTranslation
      */
     public function getTranslation($translations) {
-        assert('is_array($translations)');
-
-        // Look up translation of tag in the selected language
-        $selected_language = $this->getLanguage();
-        if (array_key_exists($selected_language, $translations)) {
-            return $translations[$selected_language];
-        }
-
-        // Look up translation of tag in the default language
-        $default_language = $this->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.');
+        return $this->translator->getTranslation($translations);
     }
 
-
     /**
-     * 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.
-     */
-    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.
-     *
-     * 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.
+     * Wrap Language->t to translate tag into the current language, with a fallback to english.
      */
     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;
-            }
-
-            $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);
-            }
-        }
-
-        $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;
+        return $this->translator->t($tag, $replacements, $fallbackdefault, $oldreplacements, $striptags);
     }
 
 
     /**
-     * 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.
+     * Wrap Language->isLanguageRTL
      */
-    private function t_not_translated($tag, $fallbacktag) {
-        if ($fallbacktag) {
-            return 'not translated (' . $tag . ')';
-        } else {
-            return $tag;
-        }
-    }
-
-
-    /**
-     * 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
-     */
-    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 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.
-     */
-    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);
-    }
-
-
-    /**
-     * Read a dictionary file in json format.
-     *
-     * @param string $filename  The absolute path to the dictionary file, minus the .definition.json ending.
-     * @return array  The translation array from 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 = self::lang_merge($lang, $moreTrans);
-            }
-        }
-
-        return $lang;
-    }
-
-
-    /**
-     * Read a dictionary file in PHP format.
-     *
-     * @param string $filename  The absolute path to the dictionary file.
-     * @return array  The translation array from the file.
-     */
-    private function readDictionaryPHP($filename) {
-        $phpFile = $filename . '.php';
-        assert('file_exists($phpFile)');
-
-        $lang = NULL;
-        include($phpFile);
-        if (isset($lang)) {
-            return $lang;
-        }
-
-        return array();
+    private function isLanguageRTL() {
+        return $this->translator->language->isLanguageRTL();
     }
 
 
     /**
-     * Read a dictionary file.
-     *
-     * @param $filename  The absolute path to the dictionary file.
-     * @return The translation array which was found in the dictionary file.
+     * Wraps Language->getLanguageList
      */
-    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 template file [' . $this->template . '] at [' . $filename . ']');
-        return array();
-    }
-
-
-    // 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;
+    private function getLanguageList() {
+        return $this->translator->language->getLanguageList();
     }
 
 
@@ -658,54 +162,4 @@ class SimpleSAML_XHTML_Template {
 
         throw new Exception($error);
     }
-
-
-    /**
-     * Retrieve the user-selected language from a cookie.
-     *
-     * @return string|NULL  The 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;
-    }
-
-
-    /**
-     * Set the user-selected language in a cookie.
-     *
-     * @param string $language  The language.
-     */
-    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);
-    }
-
 }
diff --git a/modules/core/lib/Auth/Process/LanguageAdaptor.php b/modules/core/lib/Auth/Process/LanguageAdaptor.php
index 347a1fd4a0bc4b0ce303a2d4662a80d986517fdf..2084a229465eb06cd5d46f2f97272d3b4b79512a 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/templates/includes/attributes.php b/templates/includes/attributes.php
index 2f960c46c0bd2209264b9d1934e2750eeaaf44f5..b5177638006c0b55766bbb6f3a6d3b438fa44cd1 100644
--- a/templates/includes/attributes.php
+++ b/templates/includes/attributes.php
@@ -40,7 +40,7 @@ function present_attributes($t, $attributes, $nameParent) {
 	foreach ($attributes as $name => $value) {
 	
 		$nameraw = $name;
-		$name = $t->getAttributeTranslation($parentStr . $nameraw);
+		$name = $t->translator->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..c787f8783b09e43130ff85ac3277dfdd9e776b96 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->translator->language->languageParameterName => $lang))) . '">' .
 						$langnames[$lang] . '</a>';
 				}
 			}