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