diff --git a/.gitmodules b/.gitmodules
index 6e3e46ab64a54bd017b25bcbc3bbeeb4041e33d9..a0e8238efe28bd0c246a196ae2548b952b812b37 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -21,7 +21,7 @@
 [submodule "plugins/CustomAlerts"]
     path = plugins/CustomAlerts
     url = https://github.com/piwik/plugin-CustomAlerts.git
-    branch = master
+    branch = translation
 [submodule "plugins/TasksTimetable"]
     path = plugins/TasksTimetable
     url = https://github.com/piwik/plugin-TasksTimetable.git
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4317add11b1c762932e715837522f0e5222b7da9..79d6020320fa2e982b265b166efe2c1ee1c53661 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
 
 This is a changelog for Piwik platform developers. All changes for our HTTP API's, Plugins, Themes, etc will be listed here. 
 
+## Piwik 2.11.0
+
+### Breaking Changes
+* The event `User.getLanguage` has been removed.
+
+### Deprecations
+* The `Piwik\Translate` class has been deprecated in favor of `Piwik\Translation\Translator`.
+
 ## Piwik 2.10.0
 
 ### Breaking Changes
diff --git a/config/environment/dev.php b/config/environment/dev.php
new file mode 100644
index 0000000000000000000000000000000000000000..c695c23890cd504f77c288ab0e6cbc988c4b355a
--- /dev/null
+++ b/config/environment/dev.php
@@ -0,0 +1,8 @@
+<?php
+
+return array(
+
+    // Disable translation cache
+    'Piwik\Translation\Loader\LoaderInterface' => DI\object('Piwik\Translation\Loader\JsonFileLoader'),
+
+);
diff --git a/config/environment/test.php b/config/environment/test.php
index 95857fd134df8ebde7b3ca9e201c0756bccc790e..0dfd6d97703817952345820fc0d477c604a08a13 100644
--- a/config/environment/test.php
+++ b/config/environment/test.php
@@ -5,4 +5,10 @@ return array(
     // Disable logging
     'Psr\Log\LoggerInterface' => DI\object('Psr\Log\NullLogger'),
 
+    // Disable translation cache
+    'Piwik\Translation\Loader\LoaderInterface' => DI\object('Piwik\Translation\Loader\JsonFileLoader'),
+    // Disable loading core translations
+    'Piwik\Translation\Translator' => DI\object()
+        ->constructorParameter('directories', array()),
+
 );
diff --git a/config/global.php b/config/global.php
index 4289a9c9731ba76be6b5c37ee8b03af6dcccc987..33d8dc33c51a002efc2751bd7ed16135494e4938 100644
--- a/config/global.php
+++ b/config/global.php
@@ -158,4 +158,7 @@ return array(
         return '%level% %tag%[%datetime%] %message%';
     }),
 
+    'Piwik\Translation\Loader\LoaderInterface' => DI\object('Piwik\Translation\Loader\LoaderCache')
+        ->constructor(DI\link('Piwik\Translation\Loader\JsonFileLoader')),
+
 );
diff --git a/console b/console
index ae091c57505d6a0d36b2cf6b79b2a6ab26ebbdeb..47b8ba59f989c356220e9af27e5c976512d8b662 100755
--- a/console
+++ b/console
@@ -14,8 +14,6 @@ if (!defined('PIWIK_INCLUDE_PATH')) {
 
 require_once PIWIK_INCLUDE_PATH . '/core/bootstrap.php';
 
-Piwik\Translate::loadEnglishTranslation();
-
 if (!Piwik\Common::isPhpCliMode()) {
     exit;
 }
diff --git a/core/Console.php b/core/Console.php
index 80fa29b758e1a77f5426f3fefe6282456a82516c..e248ed4770c3c23bfce90cf74497312b0766788f 100644
--- a/core/Console.php
+++ b/core/Console.php
@@ -47,8 +47,6 @@ class Console extends Application
             // Piwik not installed yet, no config file?
         }
 
-        Translate::reloadLanguage('en');
-
         $commands = $this->getAvailableCommands();
 
         foreach ($commands as $command) {
diff --git a/core/Container/ContainerFactory.php b/core/Container/ContainerFactory.php
index f0414452b7e9e01b056fa7629b1d6ec043c202d1..bf3b157ba8cc46cc2e8316936541287503df0b7a 100644
--- a/core/Container/ContainerFactory.php
+++ b/core/Container/ContainerFactory.php
@@ -12,6 +12,7 @@ use DI\Container;
 use DI\ContainerBuilder;
 use Doctrine\Common\Cache\ArrayCache;
 use Piwik\Config;
+use Piwik\Development;
 
 /**
  * Creates a configured DI container.
@@ -55,6 +56,11 @@ class ContainerFactory
         // Global config
         $builder->addDefinitions(PIWIK_USER_PATH . '/config/global.php');
 
+        // Development config
+        if (Development::isEnabled()) {
+            $builder->addDefinitions(PIWIK_USER_PATH . '/config/environment/dev.php');
+        }
+
         // User config
         if (file_exists(PIWIK_USER_PATH . '/config/config.php')) {
             $builder->addDefinitions(PIWIK_USER_PATH . '/config/config.php');
diff --git a/core/FrontController.php b/core/FrontController.php
index 70ac9c587c779dd5f38c4efe4f1fddbb6451d9f5..601e11e6775ea39853cc9b4b155812f23b390690 100644
--- a/core/FrontController.php
+++ b/core/FrontController.php
@@ -325,15 +325,13 @@ class FrontController extends Singleton
             $tmpPath . '/templates_c/',
         );
 
-        Translate::loadEnglishTranslation();
-
         Filechecks::dieIfDirectoriesNotWritable($directoriesToCheck);
 
         $this->handleMaintenanceMode();
         $this->handleProfiler();
         $this->handleSSLRedirection();
 
-        Plugin\Manager::getInstance()->loadPluginTranslations('en');
+        Plugin\Manager::getInstance()->loadPluginTranslations();
         Plugin\Manager::getInstance()->loadActivatedPlugins();
 
         if ($exceptionToThrow) {
@@ -445,7 +443,6 @@ class FrontController extends Singleton
         }
         SettingsServer::raiseMemoryLimitIfNecessary();
 
-        Translate::reloadLanguage();
         \Piwik\Plugin\Manager::getInstance()->postLoadPlugins();
 
         /**
diff --git a/core/Intl/Locale.php b/core/Intl/Locale.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c18b727c5cad810680d604024ddc043a6faaa9d
--- /dev/null
+++ b/core/Intl/Locale.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Piwik\Intl;
+
+class Locale
+{
+    public static function setLocale($locale)
+    {
+        $localeVariant = str_replace('UTF-8', 'UTF8', $locale);
+
+        setlocale(LC_ALL, $locale, $localeVariant);
+        setlocale(LC_CTYPE, '');
+    }
+
+    public static function setDefaultLocale()
+    {
+        self::setLocale('en_US.UTF-8');
+    }
+}
diff --git a/core/Piwik.php b/core/Piwik.php
index b4ce8e06946a99247f3d95616b6d1f31a655f45d..2227eca2e43ece42f1dcb109e1e9d70efefbbce1 100644
--- a/core/Piwik.php
+++ b/core/Piwik.php
@@ -9,6 +9,7 @@
 namespace Piwik;
 
 use Exception;
+use Piwik\Container\StaticContainer;
 use Piwik\Db\Adapter;
 use Piwik\Db\Schema;
 use Piwik\Db;
@@ -16,6 +17,7 @@ use Piwik\Plugin;
 use Piwik\Plugins\UsersManager\API as APIUsersManager;
 use Piwik\Session;
 use Piwik\Tracker;
+use Piwik\Translation\Translator;
 use Piwik\View;
 
 /**
@@ -733,25 +735,16 @@ class Piwik
      * @param string $translationId Translation ID, eg, `'General_Date'`.
      * @param array|string|int $args `sprintf` arguments to be applied to the internationalized
      *                               string.
+     * @param string|null $language Optionally force the language.
      * @return string The translated string or `$translationId`.
      * @api
      */
-    public static function translate($translationId, $args = array())
+    public static function translate($translationId, $args = array(), $language = null)
     {
-        if (!is_array($args)) {
-            $args = array($args);
-        }
+        /** @var Translator $translator */
+        $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
 
-        if (strpos($translationId, "_") !== false) {
-            list($plugin, $key) = explode("_", $translationId, 2);
-            if (isset($GLOBALS['Piwik_translations'][$plugin]) && isset($GLOBALS['Piwik_translations'][$plugin][$key])) {
-                $translationId = $GLOBALS['Piwik_translations'][$plugin][$key];
-            }
-        }
-        if (count($args) == 0) {
-            return $translationId;
-        }
-        return vsprintf($translationId, $args);
+        return $translator->translate($translationId, $args, $language);
     }
 
     /**
diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php
index 2224586ee484e2b098bf614763652454d0ee2ecb..675bc63f0fc400f9c7b13ee1a1d0a8e39c4f2068 100644
--- a/core/Plugin/Manager.php
+++ b/core/Plugin/Manager.php
@@ -11,11 +11,10 @@ namespace Piwik\Plugin;
 
 use Piwik\Cache;
 use Piwik\Columns\Dimension;
-use Piwik\Common;
 use Piwik\Config as PiwikConfig;
 use Piwik\Config;
+use Piwik\Container\StaticContainer;
 use Piwik\Db;
-use Piwik\Development;
 use Piwik\EventDispatcher;
 use Piwik\Filesystem;
 use Piwik\Log;
@@ -26,8 +25,8 @@ use Piwik\Singleton;
 use Piwik\Theme;
 use Piwik\Tracker;
 use Piwik\Translate;
+use Piwik\Translation\Translator;
 use Piwik\Updater;
-use Piwik\SettingsServer;
 use Piwik\Plugin\Dimension\ActionDimension;
 use Piwik\Plugin\Dimension\ConversionDimension;
 use Piwik\Plugin\Dimension\VisitDimension;
@@ -570,7 +569,8 @@ class Manager extends Singleton
      */
     public function loadAllPluginsAndGetTheirInfo()
     {
-        $language = Translate::getLanguageToLoad();
+        /** @var Translator $translator */
+        $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
 
         $plugins = array();
 
@@ -594,7 +594,7 @@ class Manager extends Singleton
                     'uninstallable'   => true,
                 );
             } else {
-                $this->loadTranslation($pluginName, $language);
+                $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang');
                 $this->loadPlugin($pluginName);
                 $info = array(
                     'activated'       => $this->isPluginActivated($pluginName),
@@ -605,7 +605,6 @@ class Manager extends Singleton
 
             $plugins[$pluginName] = $info;
         }
-        $this->loadPluginTranslations();
 
         $loadedPlugins = $this->getLoadedPlugins();
         foreach ($loadedPlugins as $oPlugin) {
@@ -686,57 +685,6 @@ class Manager extends Singleton
         $this->doLoadAlwaysActivatedPlugins = false;
     }
 
-    /**
-     * Load translations for loaded plugins
-     *
-     * @param bool|string $language Optional language code
-     */
-    public function loadPluginTranslations($language = false)
-    {
-        if (empty($language)) {
-            $language = Translate::getLanguageToLoad();
-        }
-
-        $cacheKey = 'PluginTranslations';
-
-        if (!empty($language)) {
-            $cacheKey .= '-' . trim($language);
-        }
-
-        if (!empty($this->loadedPlugins)) {
-            // makes sure to create a translation in case loaded plugins change (ie Tests vs Tracker vs UI etc)
-            $cacheKey .= '-' . md5(implode('', $this->getLoadedPluginsName()));
-        }
-
-        $cache = Cache::getLazyCache();
-        $translations = $cache->fetch($cacheKey);
-
-        if (!empty($translations) &&
-            is_array($translations) &&
-            !Development::isEnabled()) { // TODO remove this one here once we have environments in DI
-
-            Translate::mergeTranslationArray($translations);
-            return;
-        }
-
-        $translations = array();
-        $pluginNames  = self::getAllPluginsNames();
-
-        foreach ($pluginNames as $pluginName) {
-            if ($this->isPluginLoaded($pluginName) ||
-                $this->isPluginBundledWithCore($pluginName)) {
-
-                $this->loadTranslation($pluginName, $language);
-
-                if (isset($GLOBALS['Piwik_translations'][$pluginName])) {
-                    $translations[$pluginName] = $GLOBALS['Piwik_translations'][$pluginName];
-                }
-            }
-        }
-
-        $cache->save($cacheKey, $translations, 43200); // ttl=12hours
-    }
-
     /**
      * Execute postLoad() hook for loaded plugins
      */
@@ -1027,60 +975,6 @@ class Manager extends Singleton
         $this->loadedPlugins[$pluginName] = $newPlugin;
     }
 
-    /**
-     * Load translation
-     *
-     * @param Plugin $plugin
-     * @param string $langCode
-     * @throws \Exception
-     * @return bool whether the translation was found and loaded
-     */
-    private function loadTranslation($plugin, $langCode)
-    {
-        // we are in Tracker mode if Loader is not (yet) loaded
-        if (SettingsServer::isTrackerApiRequest()) {
-            return false;
-        }
-
-        if (is_string($plugin)) {
-            $pluginName = $plugin;
-        } else {
-            $pluginName = $plugin->getPluginName();
-        }
-
-        $path = self::getPluginsDirectory() . $pluginName . '/lang/%s.json';
-
-        $defaultLangPath = sprintf($path, $langCode);
-        $defaultEnglishLangPath = sprintf($path, 'en');
-
-        $translationsLoaded = false;
-
-        // merge in english translations as default first
-        if (file_exists($defaultEnglishLangPath)) {
-            $translations = $this->getTranslationsFromFile($defaultEnglishLangPath);
-            $translationsLoaded = true;
-            if (isset($translations[$pluginName])) {
-                // only merge translations of plugin - prevents overwritten strings
-                Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
-            }
-        }
-
-        // merge in specific language translations (to overwrite english defaults)
-        if (!empty($langCode) &&
-            $defaultEnglishLangPath != $defaultLangPath &&
-            file_exists($defaultLangPath)) {
-
-            $translations = $this->getTranslationsFromFile($defaultLangPath);
-            $translationsLoaded = true;
-            if (isset($translations[$pluginName])) {
-                // only merge translations of plugin - prevents overwritten strings
-                Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
-            }
-        }
-
-        return $translationsLoaded;
-    }
-
     /**
      * Return names of all installed plugins.
      *
@@ -1208,27 +1102,6 @@ class Manager extends Singleton
         $this->updatePluginsConfig($pluginsEnabled);
     }
 
-    /**
-     * @param  string $pathToTranslationFile
-     * @throws \Exception
-     * @return mixed
-     */
-    private function getTranslationsFromFile($pathToTranslationFile)
-    {
-        $data         = file_get_contents($pathToTranslationFile);
-        $translations = json_decode($data, true);
-
-        if (is_null($translations) && Common::hasJsonErrorOccurred()) {
-            $jsonError = Common::getLastJsonError();
-
-            $message = sprintf('Not able to load translation file %s: %s', $pathToTranslationFile, $jsonError);
-
-            throw new \Exception($message);
-        }
-
-        return $translations;
-    }
-
     /**
      * @param $pluginName
      * @return bool
@@ -1422,6 +1295,15 @@ class Manager extends Singleton
 
         return $sorted;
     }
+
+    public function loadPluginTranslations()
+    {
+        /** @var Translator $translator */
+        $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
+        foreach ($this->getAllPluginsNames() as $pluginName) {
+            $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang');
+        }
+    }
 }
 
 /**
diff --git a/core/Tracker/ScheduledTasksRunner.php b/core/Tracker/ScheduledTasksRunner.php
index 3f4602ef2b298c9c0c6e8d23928c04878c9ea088..2a4683a4cb3e904c5581141d944387b7c741ef1f 100644
--- a/core/Tracker/ScheduledTasksRunner.php
+++ b/core/Tracker/ScheduledTasksRunner.php
@@ -78,10 +78,6 @@ class ScheduledTasksRunner
             // Scheduled tasks assume Super User is running
             Piwik::setUserHasSuperUserAccess();
 
-            // While each plugins should ensure that necessary languages are loaded,
-            // we ensure English translations at least are loaded
-            Translate::loadEnglishTranslation();
-
             ob_start();
             CronArchive::$url = SettingsPiwik::getPiwikUrl();
             $cronArchive = new CronArchive();
diff --git a/core/Translate.php b/core/Translate.php
index c29d0f0339f317ebd9c20299e90dd332131b094a..eb7f19f21d0d5773a8899e73a776fbad6959d12d 100644
--- a/core/Translate.php
+++ b/core/Translate.php
@@ -9,14 +9,16 @@
 namespace Piwik;
 
 use Exception;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugin\Manager;
+use Piwik\Translation\Translator;
 
 /**
+ * @deprecated Use Piwik\Translation\Translator instead.
+ * @see \Piwik\Translation\Translator
  */
 class Translate
 {
-    private static $languageToLoad = null;
-    private static $loadedLanguage = false;
-
     /**
      * Clean a string that may contain HTML special chars, single/double quotes, HTML entities, leading/trailing whitespace
      *
@@ -28,25 +30,27 @@ class Translate
         return html_entity_decode(trim($s), ENT_QUOTES, 'UTF-8');
     }
 
+    /**
+     * @deprecated
+     */
     public static function loadEnglishTranslation()
     {
-        self::loadCoreTranslationFile('en');
+        self::loadAllTranslations();
     }
 
+    /**
+     * @deprecated
+     */
     public static function unloadEnglishTranslation()
     {
-        $GLOBALS['Piwik_translations'] = array();
+        self::reset();
     }
 
+    /**
+     * @deprecated
+     */
     public static function reloadLanguage($language = false)
     {
-        if (empty($language)) {
-            $language = self::getLanguageToLoad();
-        }
-        self::unloadEnglishTranslation();
-        self::loadEnglishTranslation();
-        self::loadCoreTranslation($language);
-        \Piwik\Plugin\Manager::getInstance()->loadPluginTranslations($language);
     }
 
     /**
@@ -57,41 +61,14 @@ class Translate
      */
     public static function loadCoreTranslation($language = false)
     {
-        if (empty($language)) {
-            $language = self::getLanguageToLoad();
-        }
-        if (self::$loadedLanguage == $language) {
-            return;
-        }
-        self::loadCoreTranslationFile($language);
-    }
-
-    private static function loadCoreTranslationFile($language)
-    {
-        if (empty($language)) {
-            return;
-        }
-        $path = PIWIK_INCLUDE_PATH . '/lang/' . $language . '.json';
-        if (!Filesystem::isValidFilename($language) || !is_readable($path)) {
-            throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($language)));
-        }
-        $data = file_get_contents($path);
-        $translations = json_decode($data, true);
-        self::mergeTranslationArray($translations);
-        self::setLocale();
-        self::$loadedLanguage = $language;
+        self::getTranslator()->addDirectory(PIWIK_INCLUDE_PATH . '/lang');
     }
 
+    /**
+     * @deprecated
+     */
     public static function mergeTranslationArray($translation)
     {
-        if (!isset($GLOBALS['Piwik_translations'])) {
-            $GLOBALS['Piwik_translations'] = array();
-        }
-        if (empty($translation)) {
-            return;
-        }
-        // we could check that no string overlap here
-        $GLOBALS['Piwik_translations'] = array_replace_recursive($GLOBALS['Piwik_translations'], $translation);
     }
 
     /**
@@ -100,46 +77,13 @@ class Translate
      */
     public static function getLanguageToLoad()
     {
-        if (is_null(self::$languageToLoad)) {
-            $lang = Common::getRequestVar('language', '', 'string');
-
-            /**
-             * Triggered when the current user's language is requested.
-             *
-             * By default the current language is determined by the **language** query
-             * parameter. Plugins can override this logic by subscribing to this event.
-             *
-             * **Example**
-             *
-             *     public function getLanguage(&$lang)
-             *     {
-             *         $client = new My3rdPartyAPIClient();
-             *         $thirdPartyLang = $client->getLanguageForUser(Piwik::getCurrentUserLogin());
-             *
-             *         if (!empty($thirdPartyLang)) {
-             *             $lang = $thirdPartyLang;
-             *         }
-             *     }
-             *
-             * @param string &$lang The language that should be used for the current user. Will be
-             *                      initialized to the value of the **language** query parameter.
-             */
-            Piwik::postEvent('User.getLanguage', array(&$lang));
-
-            self::$languageToLoad = $lang;
-        }
-
-        return self::$languageToLoad;
+        return self::getTranslator()->getCurrentLanguage();
     }
 
     /** Reset the cached language to load. Used in tests. */
     public static function reset()
     {
-        self::$languageToLoad = null;
-    }
-
-    private static function isALanguageLoaded() {
-        return !empty($GLOBALS['Piwik_translations']);
+        self::getTranslator()->reset();
     }
 
     /**
@@ -148,16 +92,12 @@ class Translate
      */
     public static function getLanguageLoaded()
     {
-        if (!self::isALanguageLoaded()) {
-            return null;
-        }
-
-        return self::$loadedLanguage;
+        return self::getTranslator()->getCurrentLanguage();
     }
 
     public static function getLanguageDefault()
     {
-        return Config::getInstance()->General['default_language'];
+        return self::getTranslator()->getDefaultLanguage();
     }
 
     /**
@@ -165,77 +105,25 @@ class Translate
      */
     public static function getJavascriptTranslations()
     {
-        $translations = & $GLOBALS['Piwik_translations'];
-
-        $clientSideTranslations = array();
-        foreach (self::getClientSideTranslationKeys() as $key) {
-            list($plugin, $stringName) = explode("_", $key, 2);
-            $clientSideTranslations[$key] = $translations[$plugin][$stringName];
-        }
-
-        $js = 'var translations = ' . json_encode($clientSideTranslations) . ';';
-        $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' .
-            'for(var i in translations) { piwik_translations[i] = translations[i];} ';
-        return $js;
+        return self::getTranslator()->getJavascriptTranslations();
     }
 
-    /**
-     * Returns the list of client side translations by key. These translations will be outputted
-     * to the translation JavaScript.
-     */
-    private static function getClientSideTranslationKeys()
+    public static function findTranslationKeyForTranslation($translation)
     {
-        $result = array();
-
-        /**
-         * Triggered before generating the JavaScript code that allows i18n strings to be used
-         * in the browser.
-         *
-         * Plugins should subscribe to this event to specify which translations
-         * should be available to JavaScript.
-         *
-         * Event handlers should add whole translation keys, ie, keys that include the plugin name.
-         *
-         * **Example**
-         *
-         *     public function getClientSideTranslationKeys(&$result)
-         *     {
-         *         $result[] = "MyPlugin_MyTranslation";
-         *     }
-         *
-         * @param array &$result The whole list of client side translation keys.
-         */
-        Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));
-
-        $result = array_unique($result);
-
-        return $result;
+        return self::getTranslator()->findTranslationKeyForTranslation($translation);
     }
 
     /**
-     * Set locale
-     *
-     * @see http://php.net/setlocale
+     * @return Translator
      */
-    private static function setLocale()
+    private static function getTranslator()
     {
-        $locale = $GLOBALS['Piwik_translations']['General']['Locale'];
-        $locale_variant = str_replace('UTF-8', 'UTF8', $locale);
-        setlocale(LC_ALL, $locale, $locale_variant);
-        setlocale(LC_CTYPE, '');
+        return StaticContainer::getContainer()->get('Piwik\Translation\Translator');
     }
 
-    public static function findTranslationKeyForTranslation($translation)
+    public static function loadAllTranslations()
     {
-        if (empty($GLOBALS['Piwik_translations'])) {
-            return;
-        }
-
-        foreach ($GLOBALS['Piwik_translations'] as $key => $translations) {
-            $possibleKey = array_search($translation, $translations);
-            if (!empty($possibleKey)) {
-                return $key . '_' . $possibleKey;
-            }
-        }
+        self::loadCoreTranslation();
+        Manager::getInstance()->loadPluginTranslations();
     }
 }
diff --git a/core/Translation/Loader/JsonFileLoader.php b/core/Translation/Loader/JsonFileLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..f80b9405289020c6e6c33b8c8b3e8a1534a1bb3c
--- /dev/null
+++ b/core/Translation/Loader/JsonFileLoader.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Translation\Loader;
+
+use Exception;
+use Piwik\Common;
+
+/**
+ * Loads translations from JSON files.
+ */
+class JsonFileLoader implements LoaderInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function load($language, array $directories)
+    {
+        if (empty($language)) {
+            return array();
+        }
+
+        $translations = array();
+
+        foreach ($directories as $directory) {
+            $filename = $directory . '/' . $language . '.json';
+
+            if (! file_exists($filename)) {
+                continue;
+            }
+
+            $translations = array_replace_recursive(
+                $translations,
+                $this->loadFile($filename)
+            );
+        }
+
+        return $translations;
+    }
+
+    private function loadFile($filename)
+    {
+        $data = file_get_contents($filename);
+        $translations = json_decode($data, true);
+
+        if (is_null($translations) && Common::hasJsonErrorOccurred()) {
+            throw new \Exception(sprintf(
+                'Not able to load translation file %s: %s',
+                $filename,
+                Common::getLastJsonError()
+            ));
+        }
+
+        if (!is_array($translations)) {
+            return array();
+        }
+
+        return $translations;
+    }
+}
diff --git a/core/Translation/Loader/LoaderCache.php b/core/Translation/Loader/LoaderCache.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ad7dc1ad92d232e8123e222fafc72d8bf26fc40
--- /dev/null
+++ b/core/Translation/Loader/LoaderCache.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Translation\Loader;
+
+use Piwik\Cache;
+
+/**
+ * Caches the translations loaded by another loader.
+ */
+class LoaderCache implements LoaderInterface
+{
+    /**
+     * @var LoaderInterface
+     */
+    private $loader;
+
+    /**
+     * @var Cache\Lazy
+     */
+    private $cache;
+
+    public function __construct(LoaderInterface $loader, Cache\Lazy $cache = null)
+    {
+        $this->loader = $loader;
+        // TODO DI
+        $this->cache = $cache ?: Cache::getLazyCache();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function load($language, array $directories)
+    {
+        if (empty($language)) {
+            return array();
+        }
+
+        $cacheKey = $this->getCacheKey($language, $directories);
+
+        $translations = $this->cache->fetch($cacheKey);
+
+        if (empty($translations) || !is_array($translations)) {
+            $translations = $this->loader->load($language, $directories);
+
+            $this->cache->save($cacheKey, $translations, 43200); // ttl=12hours
+        }
+
+        return $translations;
+    }
+
+    private function getCacheKey($language, array $directories)
+    {
+        $cacheKey = 'Translations-' . $language . '-';
+
+        // in case loaded plugins change (ie Tests vs Tracker vs UI etc)
+        $cacheKey .= sha1(implode('', $directories));
+
+        return $cacheKey;
+    }
+}
diff --git a/core/Translation/Loader/LoaderInterface.php b/core/Translation/Loader/LoaderInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..9aae71d45b4c759945aec1b9de08c53c411d5c95
--- /dev/null
+++ b/core/Translation/Loader/LoaderInterface.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Translation\Loader;
+
+/**
+ * Loads translations.
+ */
+interface LoaderInterface
+{
+    /**
+     * @param string $language
+     * @param mixed[] $directories Directories containing translation files.
+     * @throws \Exception The translation file was not found
+     * @return string[] Translations.
+     */
+    public function load($language, array $directories);
+}
diff --git a/core/Translation/Translator.php b/core/Translation/Translator.php
new file mode 100644
index 0000000000000000000000000000000000000000..dda946ccd9bc0c0b511235a6c35cc610ee76109a
--- /dev/null
+++ b/core/Translation/Translator.php
@@ -0,0 +1,255 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Translation;
+
+use Piwik\Config;
+use Piwik\Piwik;
+use Piwik\Plugin;
+use Piwik\Translation\Loader\LoaderInterface;
+
+/**
+ * Translates messages.
+ *
+ * @api
+ */
+class Translator
+{
+    /**
+     * Contains the translated messages, indexed by the language name.
+     *
+     * @var array
+     */
+    private $translations = array();
+
+    /**
+     * @var string
+     */
+    private $currentLanguage;
+
+    /**
+     * @var string
+     */
+    private $fallback = 'en';
+
+    /**
+     * Directories containing the translations to load.
+     *
+     * @var string[]
+     */
+    private $directories = array();
+
+    /**
+     * @var LoaderInterface
+     */
+    private $loader;
+
+    public function __construct(LoaderInterface $loader, array $directories = null)
+    {
+        $this->loader = $loader;
+        $this->currentLanguage = $this->getDefaultLanguage();
+
+        if ($directories === null) {
+            // TODO should be moved out of this class
+            $directories = array(PIWIK_INCLUDE_PATH . '/lang');
+        }
+        $this->directories = $directories;
+    }
+
+    /**
+     * Returns an internationalized string using a translation ID. If a translation
+     * cannot be found for the ID, the ID is returned.
+     *
+     * @param string $translationId Translation ID, eg, `General_Date`.
+     * @param array|string|int $args `sprintf` arguments to be applied to the internationalized
+     *                               string.
+     * @param string|null $language Optionally force the language.
+     * @return string The translated string or `$translationId`.
+     * @api
+     */
+    public function translate($translationId, $args = array(), $language = null)
+    {
+        $args = is_array($args) ? $args : array($args);
+
+        if (strpos($translationId, "_") !== false) {
+            list($plugin, $key) = explode("_", $translationId, 2);
+            $language = is_string($language) ? $language : $this->currentLanguage;
+
+            $translationId = $this->getTranslation($translationId, $language, $plugin, $key);
+        }
+
+        if (count($args) == 0) {
+            return $translationId;
+        }
+        return vsprintf($translationId, $args);
+    }
+
+    /**
+     * @return string
+     */
+    public function getCurrentLanguage()
+    {
+        return $this->currentLanguage;
+    }
+
+    /**
+     * @param string $language
+     */
+    public function setCurrentLanguage($language)
+    {
+        if (!$language) {
+            $language = $this->getDefaultLanguage();
+        }
+
+        $this->currentLanguage = $language;
+    }
+
+    /**
+     * @return string The default configured language.
+     */
+    public function getDefaultLanguage()
+    {
+        return Config::getInstance()->General['default_language'];
+    }
+
+    /**
+     * Generate javascript translations array
+     */
+    public function getJavascriptTranslations()
+    {
+        $clientSideTranslations = array();
+        foreach ($this->getClientSideTranslationKeys() as $id) {
+            list($plugin, $key) = explode('_', $id, 2);
+            $clientSideTranslations[$id] = $this->getTranslation($id, $this->currentLanguage, $plugin, $key);
+        }
+
+        $js = 'var translations = ' . json_encode($clientSideTranslations) . ';';
+        $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' .
+            'for(var i in translations) { piwik_translations[i] = translations[i];} ';
+        return $js;
+    }
+
+    /**
+     * Returns the list of client side translations by key. These translations will be outputted
+     * to the translation JavaScript.
+     */
+    private function getClientSideTranslationKeys()
+    {
+        $result = array();
+
+        /**
+         * Triggered before generating the JavaScript code that allows i18n strings to be used
+         * in the browser.
+         *
+         * Plugins should subscribe to this event to specify which translations
+         * should be available to JavaScript.
+         *
+         * Event handlers should add whole translation keys, ie, keys that include the plugin name.
+         *
+         * **Example**
+         *
+         *     public function getClientSideTranslationKeys(&$result)
+         *     {
+         *         $result[] = "MyPlugin_MyTranslation";
+         *     }
+         *
+         * @param array &$result The whole list of client side translation keys.
+         */
+        Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));
+
+        $result = array_unique($result);
+
+        return $result;
+    }
+
+    /**
+     * Add a directory containing translations.
+     *
+     * @param string $directory
+     */
+    public function addDirectory($directory)
+    {
+        if (isset($this->directories[$directory])) {
+            return;
+        }
+        // index by name to avoid duplicates
+        $this->directories[$directory] = $directory;
+
+        // clear currently loaded translations to force reloading them
+        $this->translations = array();
+    }
+
+    /**
+     * Should be used by tests only, and this method should eventually be removed.
+     */
+    public function reset()
+    {
+        $this->currentLanguage = $this->getDefaultLanguage();
+        $this->directories = array();
+        $this->translations = array();
+    }
+
+    /**
+     * @param string $translation
+     * @return null|string
+     */
+    public function findTranslationKeyForTranslation($translation)
+    {
+        foreach ($this->getAllTranslations() as $key => $translations) {
+            $possibleKey = array_search($translation, $translations);
+            if (!empty($possibleKey)) {
+                return $key . '_' . $possibleKey;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns all the translation messages loaded.
+     *
+     * @return array
+     */
+    public function getAllTranslations()
+    {
+        $this->loadTranslations($this->currentLanguage);
+
+        if (!isset($this->translations[$this->currentLanguage])) {
+            return array();
+        }
+
+        return $this->translations[$this->currentLanguage];
+    }
+
+    private function getTranslation($id, $lang, $plugin, $key)
+    {
+        $this->loadTranslations($lang);
+
+        if (isset($this->translations[$lang][$plugin])
+            && isset($this->translations[$lang][$plugin][$key])
+        ) {
+            return $this->translations[$lang][$plugin][$key];
+        }
+
+        // fallback
+        if ($lang !== $this->fallback) {
+            return $this->getTranslation($id, $this->fallback, $plugin, $key);
+        }
+
+        return $id;
+    }
+
+    private function loadTranslations($language)
+    {
+        if (empty($language) || isset($this->translations[$language])) {
+            return;
+        }
+
+        $this->translations[$language] = $this->loader->load($language, $this->directories);
+    }
+}
diff --git a/plugins/API/API.php b/plugins/API/API.php
index 8e875323672ebe4b613357af1ae3c58fff3834b2..8c19b0a9b885dbfb04d59dfe6b22de251c7ed125 100644
--- a/plugins/API/API.php
+++ b/plugins/API/API.php
@@ -12,6 +12,7 @@ use Piwik\API\Proxy;
 use Piwik\API\Request;
 use Piwik\Columns\Dimension;
 use Piwik\Config;
+use Piwik\Container\StaticContainer;
 use Piwik\DataTable;
 use Piwik\DataTable\Filter\ColumnDelete;
 use Piwik\DataTable\Row;
@@ -25,6 +26,7 @@ use Piwik\Plugin\Dimension\VisitDimension;
 use Piwik\Plugins\CoreAdminHome\CustomLogo;
 use Piwik\Segment\SegmentExpression;
 use Piwik\Translate;
+use Piwik\Translation\Translator;
 use Piwik\Version;
 
 require_once PIWIK_INCLUDE_PATH . '/core/Config.php';
@@ -321,7 +323,12 @@ class API extends \Piwik\Plugin\API
     public function getMetadata($idSite, $apiModule, $apiAction, $apiParameters = array(), $language = false,
                                 $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false)
     {
-        Translate::reloadLanguage($language);
+        if ($language) {
+            /** @var Translator $translator */
+            $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
+            $translator->setCurrentLanguage($language);
+        }
+
         $reporter = new ProcessedReport();
         $metadata = $reporter->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, $period, $date, $hideMetricsDoc, $showSubtableReports);
         return $metadata;
diff --git a/plugins/Actions/tests/Unit/ArchiverTest.php b/plugins/Actions/tests/Unit/ArchiverTest.php
index a3ba57511ab76e088de5741714d283c6f1e51d7c..ee1fe940eed4ecdadf7ff368d3c2903dfc0fd331 100644
--- a/plugins/Actions/tests/Unit/ArchiverTest.php
+++ b/plugins/Actions/tests/Unit/ArchiverTest.php
@@ -23,12 +23,12 @@ class ArchiverTests extends \PHPUnit_Framework_TestCase
 {
     public function setUp()
     {
-        Translate::reloadLanguage('en');
+        Translate::loadAllTranslations();
     }
 
     public function tearDown()
     {
-        Translate::unloadEnglishTranslation();
+        Translate::reset();
     }
 
     public function getActionNameTestData()
diff --git a/plugins/Contents/tests/System/ContentsTest.php b/plugins/Contents/tests/System/ContentsTest.php
index b555cf796253b34a333ea2e726ee65dcd47abefa..ccb7a690864aed9fc5297c5417a5796e8912cf36 100644
--- a/plugins/Contents/tests/System/ContentsTest.php
+++ b/plugins/Contents/tests/System/ContentsTest.php
@@ -47,12 +47,13 @@ class ContentsTest extends SystemTestCase
     protected function setup()
     {
         parent::setup();
-        Translate::reloadLanguage('en');
+        Translate::loadAllTranslations();
     }
 
     protected function tearDown()
     {
         parent::tearDown();
+        Translate::reset();
     }
 
     public function getApiForTesting()
diff --git a/plugins/CustomAlerts b/plugins/CustomAlerts
index 2fb9b4d44e7a1249c14eda3ada867a671476c17a..f23988fd34e52238546671b57d5df6456ddde4be 160000
--- a/plugins/CustomAlerts
+++ b/plugins/CustomAlerts
@@ -1 +1 @@
-Subproject commit 2fb9b4d44e7a1249c14eda3ada867a671476c17a
+Subproject commit f23988fd34e52238546671b57d5df6456ddde4be
diff --git a/plugins/Feedback/API.php b/plugins/Feedback/API.php
index ce6acdce200149be17b21b03761892779a345c6d..70ba272d4d36236851026834e85265a27f6e78d1 100644
--- a/plugins/Feedback/API.php
+++ b/plugins/Feedback/API.php
@@ -80,22 +80,12 @@ class API extends \Piwik\Plugin\API
 
     private function getEnglishTranslationForFeatureName($featureName)
     {
-        $loadedLanguage = Translate::getLanguageLoaded();
-
-        if ($loadedLanguage == 'en') {
+        if (Translate::getLanguageLoaded() == 'en') {
             return $featureName;
         }
 
         $translationKeyForFeature = Translate::findTranslationKeyForTranslation($featureName);
 
-        if (!empty($translationKeyForFeature)) {
-            Translate::reloadLanguage('en');
-
-            $featureName = Piwik::translate($translationKeyForFeature);
-            Translate::reloadLanguage($loadedLanguage);
-            return $featureName;
-        }
-
-        return $featureName;
+        return Piwik::translate($translationKeyForFeature, array(), 'en');
     }
 }
diff --git a/plugins/Insights/tests/Integration/ApiTest.php b/plugins/Insights/tests/Integration/ApiTest.php
index 6eeac84700809ee2fabf189284b88cbf47c3035a..56d51add2ddbe7b5d02ff62f717d11ee4dfa644d 100644
--- a/plugins/Insights/tests/Integration/ApiTest.php
+++ b/plugins/Insights/tests/Integration/ApiTest.php
@@ -41,10 +41,17 @@ class ApiTest extends SystemTestCase
 
         PiwikCache::flushAll();
 
-        Translate::reloadLanguage('en');
+        Translate::loadAllTranslations();
         $this->api = API::getInstance();
     }
 
+    public function tearDown()
+    {
+        parent::tearDown();
+
+        Translate::reset();
+    }
+
     /**
      * '/Mover1' => 2,    +8  // 400%
      * '/Old1' => 9,      -9  // -100%
diff --git a/plugins/Installation/Installation.php b/plugins/Installation/Installation.php
index 427d3d502e40b4015edd34590823bdfff06ec972..3d501c26270f09a9bd8c43092ee472433a0de6d3 100644
--- a/plugins/Installation/Installation.php
+++ b/plugins/Installation/Installation.php
@@ -91,8 +91,6 @@ class Installation extends \Piwik\Plugin
             $message = '';
         }
 
-        Translate::reloadLanguage();
-
         $action = Common::getRequestVar('action', 'welcome', 'string');
 
         if ($this->isAllowedAction($action)) {
diff --git a/plugins/LanguagesManager/Commands/CompareKeys.php b/plugins/LanguagesManager/Commands/CompareKeys.php
index 26c9a737f385f96a691a5a94b5b05af3c5d4935f..60364ac39d1d023343628f9b01845618504a71fe 100644
--- a/plugins/LanguagesManager/Commands/CompareKeys.php
+++ b/plugins/LanguagesManager/Commands/CompareKeys.php
@@ -9,7 +9,8 @@
 
 namespace Piwik\Plugins\LanguagesManager\Commands;
 
-use Piwik\Translate;
+use Piwik\Container\StaticContainer;
+use Piwik\Translation\Translator;
 use Symfony\Component\Console\Input\ArrayInput;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -49,8 +50,10 @@ class CompareKeys extends TranslationBase
 
         $englishFromOTrance = json_decode(file_get_contents($englishFromOTrance), true);
 
-        Translate::reloadLanguage('en');
-        $availableTranslations = $GLOBALS['Piwik_translations'];
+        /** @var Translator $translator */
+        $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
+        $translator->setCurrentLanguage('en');
+        $availableTranslations = $translator->getAllTranslations();
 
         $categories = array_unique(array_merge(array_keys($englishFromOTrance), array_keys($availableTranslations)));
         sort($categories);
diff --git a/plugins/LanguagesManager/Commands/FetchFromOTrance.php b/plugins/LanguagesManager/Commands/FetchFromOTrance.php
index a28f62df232b16c4bb652f9048d0c400e56f3c3e..ae130b0bc631e15749c96e57e5bd2c9d9078ec0e 100644
--- a/plugins/LanguagesManager/Commands/FetchFromOTrance.php
+++ b/plugins/LanguagesManager/Commands/FetchFromOTrance.php
@@ -11,6 +11,8 @@ namespace Piwik\Plugins\LanguagesManager\Commands;
 
 use Piwik\Container\StaticContainer;
 use Piwik\Unzip;
+use Symfony\Component\Console\Helper\DialogHelper;
+use Symfony\Component\Console\Helper\ProgressHelper;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -36,6 +38,7 @@ class FetchFromOTrance extends TranslationBase
     {
         $output->writeln("Starting to fetch latest language pack");
 
+        /** @var DialogHelper $dialog */
         $dialog = $this->getHelperSet()->get('dialog');
 
         $cookieFile = self::getDownloadPath() . DIRECTORY_SEPARATOR . 'cookie.txt';
@@ -139,6 +142,7 @@ class FetchFromOTrance extends TranslationBase
 
         $output->writeln("Converting downloaded php files to json");
 
+        /** @var ProgressHelper $progress */
         $progress = $this->getHelperSet()->get('progress');
 
         $progress->start($output, count($filesToConvert));
diff --git a/plugins/LanguagesManager/Commands/SetTranslations.php b/plugins/LanguagesManager/Commands/SetTranslations.php
index 89a4994670bc86f5e145225ddb9822056c075878..ef4ebdc22a24d20301c1285569b2be506c525433 100644
--- a/plugins/LanguagesManager/Commands/SetTranslations.php
+++ b/plugins/LanguagesManager/Commands/SetTranslations.php
@@ -10,20 +10,19 @@
 namespace Piwik\Plugins\LanguagesManager\Commands;
 
 use Piwik\Plugins\LanguagesManager\API;
-use Piwik\Translate\Filter\ByBaseTranslations;
-use Piwik\Translate\Filter\ByParameterCount;
-use Piwik\Translate\Filter\EmptyTranslations;
-use Piwik\Translate\Filter\EncodedEntities;
-use Piwik\Translate\Filter\UnnecassaryWhitespaces;
-use Piwik\Translate\Validate\CoreTranslations;
-use Piwik\Translate\Validate\NoScripts;
-use Piwik\Translate\Writer;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByBaseTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByParameterCount;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EmptyTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EncodedEntities;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\UnnecassaryWhitespaces;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\CoreTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\NoScripts;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Writer;
+use Symfony\Component\Console\Helper\DialogHelper;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 
-/**
- */
 class SetTranslations extends TranslationBase
 {
     protected function configure()
@@ -37,6 +36,7 @@ class SetTranslations extends TranslationBase
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
+        /** @var DialogHelper $dialog */
         $dialog = $this->getHelperSet()->get('dialog');
 
         $languageCode = $input->getOption('code');
diff --git a/plugins/LanguagesManager/Commands/Update.php b/plugins/LanguagesManager/Commands/Update.php
index 7014ab724132c7753a1382606a3247e21a807cfe..4d0c28bd7657b8b9c7256ccd381379ae51a715bf 100644
--- a/plugins/LanguagesManager/Commands/Update.php
+++ b/plugins/LanguagesManager/Commands/Update.php
@@ -10,6 +10,8 @@
 namespace Piwik\Plugins\LanguagesManager\Commands;
 
 use Piwik\Plugins\LanguagesManager\API;
+use Symfony\Component\Console\Helper\DialogHelper;
+use Symfony\Component\Console\Helper\ProgressHelper;
 use Symfony\Component\Console\Input\ArrayInput;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -31,6 +33,7 @@ class Update extends TranslationBase
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
+        /** @var DialogHelper $dialog */
         $dialog = $this->getHelperSet()->get('dialog');
 
         $command = $this->getApplication()->find('translations:fetch');
@@ -60,6 +63,7 @@ class Update extends TranslationBase
             $output->writeln("(!) Non interactive mode: New languages will be skipped");
         }
 
+        /** @var ProgressHelper $progress */
         $progress = $this->getHelperSet()->get('progress');
 
         $progress->start($output, count($files));
diff --git a/plugins/LanguagesManager/LanguagesManager.php b/plugins/LanguagesManager/LanguagesManager.php
index 6a0fcf10e14d8b23a6308813b4efc08ef2299a57..afd60dc53d4144b0ec594b89841d71b953b96792 100644
--- a/plugins/LanguagesManager/LanguagesManager.php
+++ b/plugins/LanguagesManager/LanguagesManager.php
@@ -12,10 +12,13 @@ namespace Piwik\Plugins\LanguagesManager;
 use Exception;
 use Piwik\Common;
 use Piwik\Config;
+use Piwik\Container\StaticContainer;
 use Piwik\Cookie;
 use Piwik\Db;
+use Piwik\Intl\Locale;
 use Piwik\Piwik;
 use Piwik\Translate;
+use Piwik\Translation\Translator;
 use Piwik\View;
 
 /**
@@ -29,12 +32,12 @@ class LanguagesManager extends \Piwik\Plugin
     public function getListHooksRegistered()
     {
         return array(
-            'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
-            'AssetManager.getJavaScriptFiles' => 'getJsFiles',
-            'User.getLanguage'                => 'getLanguageToLoad',
-            'UsersManager.deleteUser'         => 'deleteUserLanguage',
-            'Template.topBar'                 => 'addLanguagesManagerToOtherTopBar',
-            'Template.jsGlobalVariables'      => 'jsGlobalVariables'
+            'AssetManager.getStylesheetFiles'            => 'getStylesheetFiles',
+            'AssetManager.getJavaScriptFiles'            => 'getJsFiles',
+            'Request.dispatchCoreAndPluginUpdatesScreen' => 'initLanguage',
+            'UsersManager.deleteUser'                    => 'deleteUserLanguage',
+            'Template.topBar'                            => 'addLanguagesManagerToOtherTopBar',
+            'Template.jsGlobalVariables'                 => 'jsGlobalVariables'
         );
     }
 
@@ -89,14 +92,24 @@ class LanguagesManager extends \Piwik\Plugin
         return $view->render();
     }
 
-    function getLanguageToLoad(&$language)
+    public function initLanguage()
     {
+        /** @var Translator $translator */
+        $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
+
+        $language = Common::getRequestVar('language', '', 'string');
         if (empty($language)) {
-            $language = self::getLanguageCodeForCurrentUser();
+            $userLanguage = self::getLanguageCodeForCurrentUser();
+            if (API::getInstance()->isLanguageAvailable($userLanguage)) {
+                $language = $userLanguage;
+            }
         }
-        if (!API::getInstance()->isLanguageAvailable($language)) {
-            $language = Translate::getLanguageDefault();
+        if (!empty($language) && API::getInstance()->isLanguageAvailable($language)) {
+            $translator->setCurrentLanguage($language);
         }
+
+        $locale = $translator->translate('General_Locale');
+        Locale::setLocale($locale);
     }
 
     public function deleteUserLanguage($userLogin)
diff --git a/plugins/LanguagesManager/tests/Integration/LanguagesManagerTest.php b/plugins/LanguagesManager/Test/Integration/LanguagesManagerTest.php
similarity index 90%
rename from plugins/LanguagesManager/tests/Integration/LanguagesManagerTest.php
rename to plugins/LanguagesManager/Test/Integration/LanguagesManagerTest.php
index 7b41c3f55e93e0d98d9338a8664c718eed3e2b44..4ea84a03621e4c1d06d7ea81c40960fafff4c022 100755
--- a/plugins/LanguagesManager/tests/Integration/LanguagesManagerTest.php
+++ b/plugins/LanguagesManager/Test/Integration/LanguagesManagerTest.php
@@ -6,21 +6,24 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Plugins\LanguagesManager\tests;
+namespace Piwik\Plugins\LanguagesManager\Test\Integration;
 
 use Piwik\Common;
 use Piwik\Plugins\LanguagesManager\API;
-use Piwik\Translate\Filter\ByParameterCount;
-use Piwik\Translate\Filter\EmptyTranslations;
-use Piwik\Translate\Filter\EncodedEntities;
-use Piwik\Translate\Filter\UnnecassaryWhitespaces;
-use Piwik\Translate\Validate\CoreTranslations;
-use Piwik\Translate\Validate\NoScripts;
-use Piwik\Translate\Writer;
 use \Exception;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByParameterCount;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EmptyTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EncodedEntities;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\UnnecassaryWhitespaces;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\CoreTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\NoScripts;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Writer;
 
 require_once PIWIK_INCLUDE_PATH . '/plugins/LanguagesManager/API.php';
 
+/**
+ * @group LanguagesManager
+ */
 class LanguagesManagerTest extends \PHPUnit_Framework_TestCase
 {
     public function setUp()
diff --git a/tests/PHPUnit/Unit/Translate/Filter/ByBaseTranslationsTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByBaseTranslationsTest.php
similarity index 95%
rename from tests/PHPUnit/Unit/Translate/Filter/ByBaseTranslationsTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByBaseTranslationsTest.php
index 5cec2c7ca7b6c37027b4ef593d1fd94d7d11a7ad..aba41991b6267551e2d398a2f6bb7d6966eddcf9 100644
--- a/tests/PHPUnit/Unit/Translate/Filter/ByBaseTranslationsTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByBaseTranslationsTest.php
@@ -6,10 +6,13 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter\Filter;
 
-use Piwik\Translate\Filter\ByBaseTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByBaseTranslations;
 
+/**
+ * @group LanguagesManager
+ */
 class ByBaseTranslationsTest extends \PHPUnit_Framework_TestCase
 {
     public function getFilterTestData()
diff --git a/tests/PHPUnit/Unit/Translate/Filter/ByParameterCountTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByParameterCountTest.php
similarity index 94%
rename from tests/PHPUnit/Unit/Translate/Filter/ByParameterCountTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByParameterCountTest.php
index b014b0a9f15e075810b7473051930dadb32763a1..f8b57e91066f68f5e6423b6c745c91f8b8800482 100644
--- a/tests/PHPUnit/Unit/Translate/Filter/ByParameterCountTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByParameterCountTest.php
@@ -6,12 +6,12 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter\Filter;
 
-use Piwik\Translate\Filter\ByParameterCount;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByParameterCount;
 
 /**
- * @group ByParameterCountTest
+ * @group LanguagesManager
  */
 class ByParameterCountTest extends \PHPUnit_Framework_TestCase
 {
diff --git a/tests/PHPUnit/Unit/Translate/Filter/EmptyTranslationsTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EmptyTranslationsTest.php
similarity index 92%
rename from tests/PHPUnit/Unit/Translate/Filter/EmptyTranslationsTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EmptyTranslationsTest.php
index 47ec5348db3583384f8eb9718bbfa4b539d6edb8..da250c0d798565f9b3dd94a3e396657832d5fdf4 100644
--- a/tests/PHPUnit/Unit/Translate/Filter/EmptyTranslationsTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EmptyTranslationsTest.php
@@ -6,10 +6,13 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter\Filter;
 
-use Piwik\Translate\Filter\EmptyTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EmptyTranslations;
 
+/**
+ * @group LanguagesManager
+ */
 class EmptyTranslationsTest extends \PHPUnit_Framework_TestCase
 {
     public function getFilterTestData()
diff --git a/tests/PHPUnit/Unit/Translate/Filter/EncodedEntitiesTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EncodedEntitiesTest.php
similarity index 94%
rename from tests/PHPUnit/Unit/Translate/Filter/EncodedEntitiesTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EncodedEntitiesTest.php
index 19ceb5f7843d33c55a413a32a41af86547eaffe0..9411a65124e48b80c2e906873458f54140c1fab7 100644
--- a/tests/PHPUnit/Unit/Translate/Filter/EncodedEntitiesTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EncodedEntitiesTest.php
@@ -6,10 +6,13 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter\Filter;
 
-use Piwik\Translate\Filter\EncodedEntities;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EncodedEntities;
 
+/**
+ * @group LanguagesManager
+ */
 class EncodedEntitiesTest extends \PHPUnit_Framework_TestCase
 {
     public function getFilterTestData()
diff --git a/tests/PHPUnit/Unit/Translate/Filter/UnnecassaryWhitespacesTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/UnnecassaryWhitespacesTest.php
similarity index 95%
rename from tests/PHPUnit/Unit/Translate/Filter/UnnecassaryWhitespacesTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/UnnecassaryWhitespacesTest.php
index 77cecf7e77be97ad529de343f9e9f952aa34ec92..701df50144b1b0bb491deedf40129e99275c7e9f 100644
--- a/tests/PHPUnit/Unit/Translate/Filter/UnnecassaryWhitespacesTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/UnnecassaryWhitespacesTest.php
@@ -6,10 +6,13 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter\Filter;
 
-use Piwik\Translate\Filter\UnnecassaryWhitespaces;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\UnnecassaryWhitespaces;
 
+/**
+ * @group LanguagesManager
+ */
 class UnnecassaryWhitepsacesTest extends \PHPUnit_Framework_TestCase
 {
     public function getFilterTestData()
diff --git a/tests/PHPUnit/Unit/Translate/Validate/CoreTranslationsTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/CoreTranslationsTest.php
similarity index 95%
rename from tests/PHPUnit/Unit/Translate/Validate/CoreTranslationsTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/CoreTranslationsTest.php
index 258267fd3e858373acd75619b8f3eee1fd7b224c..2a82991fcdbfcb537e40e4c3dc0f6845450d25b6 100644
--- a/tests/PHPUnit/Unit/Translate/Validate/CoreTranslationsTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/CoreTranslationsTest.php
@@ -6,10 +6,13 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate\Validate;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter\Validate;
 
-use Piwik\Translate\Validate\CoreTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\CoreTranslations;
 
+/**
+ * @group LanguagesManager
+ */
 class CoreTranslationsTest extends \PHPUnit_Framework_TestCase
 {
     public function setUp()
diff --git a/tests/PHPUnit/Unit/Translate/Validate/NoScriptsTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/NoScriptsTest.php
similarity index 93%
rename from tests/PHPUnit/Unit/Translate/Validate/NoScriptsTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/NoScriptsTest.php
index 519126708d30f6dfed125e378e700a8cfe358917..fcf5201d76827f3d0ffa16bb9e7906d6c82ffd11 100644
--- a/tests/PHPUnit/Unit/Translate/Validate/NoScriptsTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/NoScriptsTest.php
@@ -6,10 +6,13 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate\Validate;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter\Validate;
 
-use Piwik\Translate\Validate\NoScripts;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\NoScripts;
 
+/**
+ * @group LanguagesManager
+ */
 class NoScriptsTest extends \PHPUnit_Framework_TestCase
 {
     public function getFilterTestDataValid()
diff --git a/tests/PHPUnit/Unit/Translate/WriterTest.php b/plugins/LanguagesManager/Test/Unit/TranslationWriter/WriterTest.php
similarity index 93%
rename from tests/PHPUnit/Unit/Translate/WriterTest.php
rename to plugins/LanguagesManager/Test/Unit/TranslationWriter/WriterTest.php
index e33ce6e74e45e8d08f7ce85a8c8eee1dcff82137..41b754f55b9d904f0d793cf9d183cc16ea293f6c 100644
--- a/tests/PHPUnit/Unit/Translate/WriterTest.php
+++ b/plugins/LanguagesManager/Test/Unit/TranslationWriter/WriterTest.php
@@ -6,16 +6,19 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
-namespace Piwik\Tests\Unit\Translate;
+namespace Piwik\Plugins\LanguagesManager\Test\Unit\TranslationWriter;
 
 use Piwik\Container\StaticContainer;
-use Piwik\Translate\Filter\ByBaseTranslations;
-use Piwik\Translate\Filter\ByParameterCount;
-use Piwik\Translate\Filter\UnnecassaryWhitespaces;
-use Piwik\Translate\Validate\CoreTranslations;
-use Piwik\Translate\Validate\NoScripts;
-use Piwik\Translate\Writer;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByBaseTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\ByParameterCount;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\UnnecassaryWhitespaces;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\CoreTranslations;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\NoScripts;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Writer;
 
+/**
+ * @group LanguagesManager
+ */
 class WriterTest extends \PHPUnit_Framework_TestCase
 {
     public function setUp()
diff --git a/core/Translate/Filter/ByBaseTranslations.php b/plugins/LanguagesManager/TranslationWriter/Filter/ByBaseTranslations.php
similarity index 96%
rename from core/Translate/Filter/ByBaseTranslations.php
rename to plugins/LanguagesManager/TranslationWriter/Filter/ByBaseTranslations.php
index 8a2e095d9574f68cab87a883b40e0c2ece820d82..1504f49e0d52c8e825038dd03c6c495d0604a570 100644
--- a/core/Translate/Filter/ByBaseTranslations.php
+++ b/plugins/LanguagesManager/TranslationWriter/Filter/ByBaseTranslations.php
@@ -7,10 +7,8 @@
  *
  */
 
-namespace Piwik\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
 
-/**
- */
 class ByBaseTranslations extends FilterAbstract
 {
     protected $baseTranslations = array();
diff --git a/core/Translate/Filter/ByParameterCount.php b/plugins/LanguagesManager/TranslationWriter/Filter/ByParameterCount.php
similarity index 97%
rename from core/Translate/Filter/ByParameterCount.php
rename to plugins/LanguagesManager/TranslationWriter/Filter/ByParameterCount.php
index 357ab5ba33e6b4b3904b5226edaa6b61aca42202..0d8a3cd482e46ce7711a83526cf13e94bf9e5588 100644
--- a/core/Translate/Filter/ByParameterCount.php
+++ b/plugins/LanguagesManager/TranslationWriter/Filter/ByParameterCount.php
@@ -7,10 +7,8 @@
  *
  */
 
-namespace Piwik\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
 
-/**
- */
 class ByParameterCount extends FilterAbstract
 {
     protected $baseTranslations = array();
diff --git a/core/Translate/Filter/EmptyTranslations.php b/plugins/LanguagesManager/TranslationWriter/Filter/EmptyTranslations.php
similarity index 94%
rename from core/Translate/Filter/EmptyTranslations.php
rename to plugins/LanguagesManager/TranslationWriter/Filter/EmptyTranslations.php
index 75e3e6536f681823384c202fa6cf47571cf0571b..15e17b2cc90cbc0d35daec4aaae715355461f8a8 100644
--- a/core/Translate/Filter/EmptyTranslations.php
+++ b/plugins/LanguagesManager/TranslationWriter/Filter/EmptyTranslations.php
@@ -7,10 +7,8 @@
  *
  */
 
-namespace Piwik\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
 
-/**
- */
 class EmptyTranslations extends FilterAbstract
 {
     /**
diff --git a/core/Translate/Filter/EncodedEntities.php b/plugins/LanguagesManager/TranslationWriter/Filter/EncodedEntities.php
similarity index 93%
rename from core/Translate/Filter/EncodedEntities.php
rename to plugins/LanguagesManager/TranslationWriter/Filter/EncodedEntities.php
index b7e3d6a54e2e4050917f134c4859aad31d35302d..492ad6953a199b70ef8cb685c0343b8613d29a3d 100644
--- a/core/Translate/Filter/EncodedEntities.php
+++ b/plugins/LanguagesManager/TranslationWriter/Filter/EncodedEntities.php
@@ -7,12 +7,10 @@
  *
  */
 
-namespace Piwik\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
 
 use Piwik\Translate;
 
-/**
- */
 class EncodedEntities extends FilterAbstract
 {
     /**
diff --git a/core/Translate/Filter/FilterAbstract.php b/plugins/LanguagesManager/TranslationWriter/Filter/FilterAbstract.php
similarity index 90%
rename from core/Translate/Filter/FilterAbstract.php
rename to plugins/LanguagesManager/TranslationWriter/Filter/FilterAbstract.php
index 4e7ecc064d6f3943b1da9dfc0491eb9c4fb86dd6..0f157fa5cc52659838b4148ae750e5d98aa96819 100644
--- a/core/Translate/Filter/FilterAbstract.php
+++ b/plugins/LanguagesManager/TranslationWriter/Filter/FilterAbstract.php
@@ -7,10 +7,8 @@
  *
  */
 
-namespace Piwik\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
 
-/**
- */
 abstract class FilterAbstract
 {
     protected $filteredData = array();
diff --git a/core/Translate/Filter/UnnecassaryWhitespaces.php b/plugins/LanguagesManager/TranslationWriter/Filter/UnnecassaryWhitespaces.php
similarity index 97%
rename from core/Translate/Filter/UnnecassaryWhitespaces.php
rename to plugins/LanguagesManager/TranslationWriter/Filter/UnnecassaryWhitespaces.php
index 61211fc8abd75a61f26655ff3ae86e1d3de95fca..ce665b165a9753977c7e6159050fdbc3d1c9501c 100644
--- a/core/Translate/Filter/UnnecassaryWhitespaces.php
+++ b/plugins/LanguagesManager/TranslationWriter/Filter/UnnecassaryWhitespaces.php
@@ -7,10 +7,8 @@
  *
  */
 
-namespace Piwik\Translate\Filter;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Filter;
 
-/**
- */
 class UnnecassaryWhitespaces extends FilterAbstract
 {
     protected $baseTranslations = array();
diff --git a/core/Translate/Validate/CoreTranslations.php b/plugins/LanguagesManager/TranslationWriter/Validate/CoreTranslations.php
similarity index 97%
rename from core/Translate/Validate/CoreTranslations.php
rename to plugins/LanguagesManager/TranslationWriter/Validate/CoreTranslations.php
index bb52dc1ec8588fa421eded572fe246897af62994..eb888bf4341b1fb6dc458d943844ee5ba0867ac1 100644
--- a/core/Translate/Validate/CoreTranslations.php
+++ b/plugins/LanguagesManager/TranslationWriter/Validate/CoreTranslations.php
@@ -7,12 +7,10 @@
  *
  */
 
-namespace Piwik\Translate\Validate;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Validate;
 
 use Piwik\Common;
 
-/**
- */
 class CoreTranslations extends ValidateAbstract
 {
     /**
diff --git a/core/Translate/Validate/NoScripts.php b/plugins/LanguagesManager/TranslationWriter/Validate/NoScripts.php
similarity index 93%
rename from core/Translate/Validate/NoScripts.php
rename to plugins/LanguagesManager/TranslationWriter/Validate/NoScripts.php
index e7f032ff55d2155d931b4607a7a3ef642be56b4b..7705cd02d7adc2414fc8d8a58b261e59e7c04819 100644
--- a/core/Translate/Validate/NoScripts.php
+++ b/plugins/LanguagesManager/TranslationWriter/Validate/NoScripts.php
@@ -7,10 +7,8 @@
  *
  */
 
-namespace Piwik\Translate\Validate;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Validate;
 
-/**
- */
 class NoScripts extends ValidateAbstract
 {
     /**
diff --git a/core/Translate/Validate/ValidateAbstract.php b/plugins/LanguagesManager/TranslationWriter/Validate/ValidateAbstract.php
similarity index 90%
rename from core/Translate/Validate/ValidateAbstract.php
rename to plugins/LanguagesManager/TranslationWriter/Validate/ValidateAbstract.php
index c732d31d252cc2fb32570b7ca5290de84e2deb3e..df3612383966ea5dcee46efb75aa28d9ffeff22d 100644
--- a/core/Translate/Validate/ValidateAbstract.php
+++ b/plugins/LanguagesManager/TranslationWriter/Validate/ValidateAbstract.php
@@ -7,10 +7,8 @@
  *
  */
 
-namespace Piwik\Translate\Validate;
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter\Validate;
 
-/**
- */
 abstract class ValidateAbstract
 {
     protected $message = null;
diff --git a/core/Translate/Writer.php b/plugins/LanguagesManager/TranslationWriter/Writer.php
similarity index 96%
rename from core/Translate/Writer.php
rename to plugins/LanguagesManager/TranslationWriter/Writer.php
index 5ea57a70033dda72af31b46632d639ef25596d92..a4f0f06a23fbb8438cbb66e2dd2f6a192b40fb49 100644
--- a/core/Translate/Writer.php
+++ b/plugins/LanguagesManager/TranslationWriter/Writer.php
@@ -4,21 +4,19 @@
  *
  * @link http://piwik.org
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- *
  */
-namespace Piwik\Translate;
+
+namespace Piwik\Plugins\LanguagesManager\TranslationWriter;
 
 use Exception;
 use Piwik\Container\StaticContainer;
 use Piwik\Filesystem;
 use Piwik\Piwik;
-use Piwik\Translate\Filter\FilterAbstract;
-use Piwik\Translate\Validate\ValidateAbstract;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\FilterAbstract;
+use Piwik\Plugins\LanguagesManager\TranslationWriter\Validate\ValidateAbstract;
 
 /**
- * Writes clean translations to file
- *
+ * Writes translations to file.
  */
 class Writer
 {
@@ -30,7 +28,7 @@ class Writer
     protected $language = '';
 
     /**
-     * Name of a plugin (if set in contructor)
+     * Name of a plugin (if set in constructor)
      *
      * @var string|null
      */
diff --git a/plugins/ScheduledReports/API.php b/plugins/ScheduledReports/API.php
index 1eade02df06b73d980d9492c68523fbd5b1eeb21..d93e252793f6ab77cbbd484161000aa55dce97ae 100644
--- a/plugins/ScheduledReports/API.php
+++ b/plugins/ScheduledReports/API.php
@@ -11,6 +11,7 @@ namespace Piwik\Plugins\ScheduledReports;
 use Exception;
 use Piwik\Common;
 use Piwik\Config;
+use Piwik\Container\StaticContainer;
 use Piwik\Date;
 use Piwik\Db;
 use Piwik\Log;
@@ -23,6 +24,7 @@ use Piwik\ReportRenderer;
 use Piwik\Site;
 use Piwik\Tracker;
 use Piwik\Translate;
+use Piwik\Translation\Translator;
 
 /**
  * The ScheduledReports API lets you manage Scheduled Email reports, as well as generate, download or email any existing report.
@@ -270,7 +272,9 @@ class API extends \Piwik\Plugin\API
             $language = Translate::getLanguageDefault();
         }
 
-        Translate::reloadLanguage($language);
+        /** @var Translator $translator */
+        $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
+        $translator->setCurrentLanguage($language);
 
         $reports = $this->getReports($idSite = false, $_period = false, $idReport);
         $report = reset($reports);
diff --git a/tests/PHPUnit/Framework/Fixture.php b/tests/PHPUnit/Framework/Fixture.php
index 5cf7c1d59700c59b3f9dfe75f375102ee8b6edeb..751c4ce1abda7eb3f00edb114d4051d5ec7b6721 100644
--- a/tests/PHPUnit/Framework/Fixture.php
+++ b/tests/PHPUnit/Framework/Fixture.php
@@ -217,7 +217,7 @@ class Fixture extends \PHPUnit_Framework_Assert
 
         // Make sure translations are loaded to check messages in English
         if ($this->loadTranslations) {
-            Translate::reloadLanguage('en');
+            Translate::loadAllTranslations();
             APILanguageManager::getInstance()->setLanguageForUser('superUserLogin', 'en');
         }
 
@@ -314,7 +314,7 @@ class Fixture extends \PHPUnit_Framework_Assert
         EventDispatcher::getInstance()->clearAllObservers();
 
         $_GET = $_REQUEST = array();
-        Translate::unloadEnglishTranslation();
+        Translate::reset();
 
         Config::unsetInstance();
 
diff --git a/tests/PHPUnit/Framework/TestCase/SystemTestCase.php b/tests/PHPUnit/Framework/TestCase/SystemTestCase.php
index 6429f5cf4f0e65d1a1fcd83d41d422ba0c6acbf1..8e63da92b8bef3669a557128b6a7a84c505deab4 100755
--- a/tests/PHPUnit/Framework/TestCase/SystemTestCase.php
+++ b/tests/PHPUnit/Framework/TestCase/SystemTestCase.php
@@ -12,7 +12,7 @@ use Exception;
 use Piwik\ArchiveProcessor\Rules;
 use Piwik\Common;
 use Piwik\Config;
-use Piwik\DataAccess\ArchiveTableCreator;
+use Piwik\Container\StaticContainer;
 use Piwik\Db;
 use Piwik\DbHelper;
 use Piwik\ReportRenderer;
@@ -24,6 +24,7 @@ use Piwik\Translate;
 use Piwik\Log;
 use PHPUnit_Framework_TestCase;
 use Piwik\Tests\Framework\Fixture;
+use Piwik\Translation\Translator;
 
 require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';
 
@@ -492,8 +493,9 @@ abstract class SystemTestCase extends PHPUnit_Framework_TestCase
     {
         if ($this->lastLanguage != $langId) {
             $_GET['language'] = $langId;
-            Translate::reset();
-            Translate::reloadLanguage($langId);
+            /** @var Translator $translator */
+            $translator = StaticContainer::getContainer()->get('Piwik\Translation\Translator');
+            $translator->setCurrentLanguage($langId);
         }
 
         $this->lastLanguage = $langId;
diff --git a/tests/PHPUnit/Integration/CacheIdTest.php b/tests/PHPUnit/Integration/CacheIdTest.php
index 01d39d7e494b4cf901fd67e3f382b882a1560549..b549bc9668e4269a3925e682eb177f134b0b78dc 100644
--- a/tests/PHPUnit/Integration/CacheIdTest.php
+++ b/tests/PHPUnit/Integration/CacheIdTest.php
@@ -20,12 +20,12 @@ class CacheIdTest extends IntegrationTestCase
 {
     public function setUp()
     {
-        Translate::loadEnglishTranslation();
+        Translate::loadAllTranslations();
     }
 
     public function tearDown()
     {
-        Translate::unloadEnglishTranslation();
+        Translate::reset();
     }
 
     public function test_languageAware_shouldAppendTheLoadedLanguage()
diff --git a/tests/PHPUnit/Integration/ReportTest.php b/tests/PHPUnit/Integration/ReportTest.php
index 1e141db383eb4a0c318ae0bbb0ffc0fba15ee596..21e8a5254246909d80c30bee4dfad1c7c02fb790 100644
--- a/tests/PHPUnit/Integration/ReportTest.php
+++ b/tests/PHPUnit/Integration/ReportTest.php
@@ -116,7 +116,6 @@ class ReportTest extends IntegrationTestCase
     {
         WidgetsList::getInstance()->_reset();
         MenuReporting::getInstance()->unsetInstance();
-        Translate::unloadEnglishTranslation();
         unset($_GET['idSite']);
         parent::tearDown();
     }
@@ -157,14 +156,16 @@ class ReportTest extends IntegrationTestCase
 
     public function test_getWidgetTitle_shouldReturnTranslatedTitleIfSet()
     {
-        $this->loadEnglishTranslation();
+        Translate::loadAllTranslations();
         $this->assertEquals('Page Titles Following a Site Search', $this->advancedReport->getWidgetTitle());
+        Translate::reset();
     }
 
     public function test_getCategory_shouldReturnTranslatedCategory()
     {
-        $this->loadEnglishTranslation();
+        Translate::loadAllTranslations();
         $this->assertEquals('Goals', $this->advancedReport->getCategory());
+        Translate::reset();
     }
 
     public function test_configureWidget_shouldNotAddAWidgetIfNoWidgetTitleIsSet()
@@ -539,9 +540,4 @@ class ReportTest extends IntegrationTestCase
     {
         PluginManager::getInstance()->unloadPlugins();
     }
-
-    private function loadEnglishTranslation()
-    {
-        Translate::reloadLanguage('en');
-    }
 }
diff --git a/tests/PHPUnit/Integration/Tracker/ActionTest.php b/tests/PHPUnit/Integration/Tracker/ActionTest.php
index 1ca87f96785e7728847dc76592170b78c588590b..a00ea4c9b3ccdec40da9e5f09d0e39fa2fa8b1b3 100644
--- a/tests/PHPUnit/Integration/Tracker/ActionTest.php
+++ b/tests/PHPUnit/Integration/Tracker/ActionTest.php
@@ -40,7 +40,14 @@ class ActionTest extends IntegrationTestCase
 
         PluginManager::getInstance()->loadPlugins(array('SitesManager'));
 
-        Translate::loadEnglishTranslation();
+        Translate::loadAllTranslations();
+    }
+
+    public function tearDown()
+    {
+        parent::tearDown();
+
+        Translate::reset();
     }
 
     protected function setUpRootAccess()
diff --git a/tests/PHPUnit/Integration/WidgetsListTest.php b/tests/PHPUnit/Integration/WidgetsListTest.php
index fc00be7799000ae03c652ad2481723ab2d326231..22967a9d2d64bb97a0a8a1ff649c2205cad571e2 100644
--- a/tests/PHPUnit/Integration/WidgetsListTest.php
+++ b/tests/PHPUnit/Integration/WidgetsListTest.php
@@ -164,7 +164,7 @@ class WidgetsListTest extends IntegrationTestCase
         FakeAccess::$superUser = true;
         Access::setSingletonInstance($pseudoMockAccess);
 
-        Translate::loadEnglishTranslation();
+        Translate::loadAllTranslations();
 
         Fixture::createWebsite('2009-01-04 00:11:42', true);
 
@@ -175,5 +175,7 @@ class WidgetsListTest extends IntegrationTestCase
 
         $this->assertTrue(WidgetsList::isDefined('Actions', 'getPageUrls'));
         $this->assertFalse(WidgetsList::isDefined('Actions', 'inValiD'));
+
+        Translate::reset();
     }
 }
diff --git a/tests/PHPUnit/Unit/Metrics/Formatter/HtmlTest.php b/tests/PHPUnit/Unit/Metrics/Formatter/HtmlTest.php
index fd9b996f5056512e2965c8fbca2ad47d53633bfd..dd0c0a106b77061f58a61ddbc41f4ab23e76219e 100644
--- a/tests/PHPUnit/Unit/Metrics/Formatter/HtmlTest.php
+++ b/tests/PHPUnit/Unit/Metrics/Formatter/HtmlTest.php
@@ -7,6 +7,7 @@
  */
 namespace Piwik\Tests\Unit\Metrics\Formatter;
 
+use Piwik\Intl\Locale;
 use Piwik\Metrics\Formatter\Html;
 use Piwik\Translate;
 use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
@@ -34,18 +35,14 @@ class HtmlTest extends \PHPUnit_Framework_TestCase
 
         $this->formatter = new Html();
 
-        setlocale(LC_ALL, null);
-
-        Translate::loadEnglishTranslation();
+        Translate::loadAllTranslations();
         $this->setSiteManagerApiMock();
     }
 
     public function tearDown()
     {
-        Translate::unloadEnglishTranslation();
+        Translate::reset();
         $this->unsetSiteManagerApiMock();
-
-        setlocale(LC_ALL, null);
     }
 
     public function test_getPrettyTimeFromSeconds_DefaultsToShowingSentences_AndUsesNonBreakingSpaces()
diff --git a/tests/PHPUnit/Unit/Metrics/FormatterTest.php b/tests/PHPUnit/Unit/Metrics/FormatterTest.php
index ce8c268b08ba2e07ebdc8c4965277e12d13cf45a..36def63bb14ee1ea24c9796a597b52bec95f34a1 100644
--- a/tests/PHPUnit/Unit/Metrics/FormatterTest.php
+++ b/tests/PHPUnit/Unit/Metrics/FormatterTest.php
@@ -7,6 +7,7 @@
  */
 namespace Piwik\Tests\Unit\Metrics;
 
+use Piwik\Intl\Locale;
 use Piwik\Metrics\Formatter;
 use Piwik\Translate;
 use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
@@ -50,18 +51,14 @@ class FormatterTest extends \PHPUnit_Framework_TestCase
 
         $this->formatter = new Formatter();
 
-        setlocale(LC_ALL, null);
-
-        Translate::loadEnglishTranslation();
+        Translate::loadAllTranslations();
         $this->setSiteManagerApiMock();
     }
 
     public function tearDown()
     {
-        Translate::unloadEnglishTranslation();
+        Translate::reset();
         $this->unsetSiteManagerApiMock();
-
-        setlocale(LC_ALL, null);
     }
 
     /**
@@ -83,6 +80,7 @@ class FormatterTest extends \PHPUnit_Framework_TestCase
         }
 
         $this->assertEquals($expected, $this->formatter->getPrettyNumber($number, 2));
+        Locale::setDefaultLocale();
     }
 
     /**
diff --git a/tests/PHPUnit/Unit/Period/BasePeriodTest.php b/tests/PHPUnit/Unit/Period/BasePeriodTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a534b1bb2e4edaa6b9621a3832ef79db9038dc15
--- /dev/null
+++ b/tests/PHPUnit/Unit/Period/BasePeriodTest.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Tests\Unit\Period;
+
+use Piwik\Translate;
+
+abstract class BasePeriodTest extends \PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        parent::setUp();
+
+        Translate::loadAllTranslations();
+    }
+
+    public function tearDown()
+    {
+        parent::tearDown();
+
+        Translate::reset();
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Period/DayTest.php b/tests/PHPUnit/Unit/Period/DayTest.php
index ae1ba01844a7106942595a5105b30aeb1c56d08e..c2b4a05a5b867d4abb14413e50b3f6180faaa770 100644
--- a/tests/PHPUnit/Unit/Period/DayTest.php
+++ b/tests/PHPUnit/Unit/Period/DayTest.php
@@ -10,9 +10,8 @@ namespace Piwik\Tests\Unit\Period;
 
 use Piwik\Date;
 use Piwik\Period\Day;
-use Piwik\Translate;
 
-class Period_DayTest extends \PHPUnit_Framework_TestCase
+class DayTest extends BasePeriodTest
 {
     /**
      * @group Core
@@ -215,8 +214,6 @@ class Period_DayTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedShortString()
     {
-        $this->loadEnglishTranslation();
-
         $month = new Day(Date::factory('2024-10-09'));
         $shouldBe = 'Wed 9 Oct';
         $this->assertEquals($shouldBe, $month->getLocalizedShortString());
@@ -227,8 +224,6 @@ class Period_DayTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedLongString()
     {
-        $this->loadEnglishTranslation();
-
         $month = new Day(Date::factory('2024-10-09'));
         $shouldBe = 'Wednesday 9 October 2024';
         $this->assertEquals($shouldBe, $month->getLocalizedLongString());
@@ -239,15 +234,8 @@ class Period_DayTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetPrettyString()
     {
-        $this->loadEnglishTranslation();
-
         $month = new Day(Date::factory('2024-10-09'));
         $shouldBe = '2024-10-09';
         $this->assertEquals($shouldBe, $month->getPrettyString());
     }
-
-    private function loadEnglishTranslation()
-    {
-        Translate::reloadLanguage('en');
-    }
 }
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Period/MonthTest.php b/tests/PHPUnit/Unit/Period/MonthTest.php
index 916f925e6d08bfc0779b65d8315d1dc5dba36c87..6ef1c824e33ac963d91e86d1b29de30bc601ff88 100644
--- a/tests/PHPUnit/Unit/Period/MonthTest.php
+++ b/tests/PHPUnit/Unit/Period/MonthTest.php
@@ -10,9 +10,8 @@ namespace Piwik\Tests\Unit\Period;
 
 use Piwik\Date;
 use Piwik\Period\Month;
-use Piwik\Translate;
 
-class Period_MonthTest extends \PHPUnit_Framework_TestCase
+class MonthTest extends BasePeriodTest
 {
     /**
      * testing december
@@ -271,8 +270,6 @@ class Period_MonthTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedShortString()
     {
-        $this->loadEnglishTranslation();
-
         $month = new Month(Date::factory('2024-10-09'));
         $shouldBe = 'Oct 2024';
         $this->assertEquals($shouldBe, $month->getLocalizedShortString());
@@ -283,8 +280,6 @@ class Period_MonthTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedLongString()
     {
-        $this->loadEnglishTranslation();
-
         $month = new Month(Date::factory('2024-10-09'));
         $shouldBe = '2024, October';
         $this->assertEquals($shouldBe, $month->getLocalizedLongString());
@@ -295,15 +290,9 @@ class Period_MonthTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetPrettyString()
     {
-        $this->loadEnglishTranslation();
-
         $month = new Month(Date::factory('2024-10-09'));
         $shouldBe = '2024-10';
         $this->assertEquals($shouldBe, $month->getPrettyString());
     }
 
-    private function loadEnglishTranslation()
-    {
-        Translate::reloadLanguage('en');
-    }
 }
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Period/RangeTest.php b/tests/PHPUnit/Unit/Period/RangeTest.php
index 29c716d68bf03a055e4ce33efdbba2431ed66fb2..c7896b638fbb1ca640a9f98f0c382b26219aa0ba 100644
--- a/tests/PHPUnit/Unit/Period/RangeTest.php
+++ b/tests/PHPUnit/Unit/Period/RangeTest.php
@@ -14,9 +14,8 @@ use Piwik\Period\Month;
 use Piwik\Period\Range;
 use Piwik\Period\Week;
 use Piwik\Period\Year;
-use Piwik\Translate;
 
-class Period_RangeTest extends \PHPUnit_Framework_TestCase
+class RangeTest extends BasePeriodTest
 {
     // test range 1
     /**
@@ -1272,7 +1271,6 @@ class Period_RangeTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedShortString()
     {
-        $this->loadEnglishTranslation();
         $month = new Range('range', '2000-12-09,2001-02-01');
         $shouldBe = '9 Dec 00 - 1 Feb 01';
         $this->assertEquals($shouldBe, $month->getLocalizedShortString());
@@ -1283,7 +1281,6 @@ class Period_RangeTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedLongString()
     {
-        $this->loadEnglishTranslation();
         $month = new Range('range', '2023-05-09,2023-05-21');
         $shouldBe = '8 May 23 - 21 May 23';
         $this->assertEquals($shouldBe, $month->getLocalizedLongString());
@@ -1294,7 +1291,6 @@ class Period_RangeTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetPrettyString()
     {
-        $this->loadEnglishTranslation();
         $month = new Range('range', '2007-02-09,2007-03-15');
         $shouldBe = 'From 2007-02-09 to 2007-03-15';
         $this->assertEquals($shouldBe, $month->getPrettyString());
@@ -1322,9 +1318,4 @@ class Period_RangeTest extends \PHPUnit_Framework_TestCase
         $range = new Range($period, 'last' . $lastN);
         $this->assertEquals($expectedLastN, $range->getNumberOfSubperiods());
     }
-
-    private function loadEnglishTranslation()
-    {
-        Translate::reloadLanguage('en');
-    }
 }
diff --git a/tests/PHPUnit/Unit/Period/WeekTest.php b/tests/PHPUnit/Unit/Period/WeekTest.php
index c543a2db5d1fc5b6dc8b183ab48ee0fcf7bdfe5e..05f54b994292b0d2aaadc352167679bfc0f88a9c 100644
--- a/tests/PHPUnit/Unit/Period/WeekTest.php
+++ b/tests/PHPUnit/Unit/Period/WeekTest.php
@@ -10,9 +10,8 @@ namespace Piwik\Tests\Unit\Period;
 
 use Piwik\Date;
 use Piwik\Period\Week;
-use Piwik\Translate;
 
-class Period_WeekTest extends \PHPUnit_Framework_TestCase
+class WeekTest extends BasePeriodTest
 {
     /**
      * test week between 2 years
@@ -123,7 +122,6 @@ class Period_WeekTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedShortString()
     {
-        $this->loadEnglishTranslation();
         $week = new Week(Date::factory('2024-10-09'));
         $shouldBe = '7 Oct - 13 Oct 24';
         $this->assertEquals($shouldBe, $week->getLocalizedShortString());
@@ -134,7 +132,6 @@ class Period_WeekTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedLongString()
     {
-        $this->loadEnglishTranslation();
         $week = new Week(Date::factory('2024-10-09'));
         $shouldBe = 'Week 7 October - 13 October 2024';
         $this->assertEquals($shouldBe, $week->getLocalizedLongString());
@@ -145,14 +142,8 @@ class Period_WeekTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetPrettyString()
     {
-        $this->loadEnglishTranslation();
         $week = new Week(Date::factory('2024-10-09'));
         $shouldBe = 'From 2024-10-07 to 2024-10-13';
         $this->assertEquals($shouldBe, $week->getPrettyString());
     }
-
-    private function loadEnglishTranslation()
-    {
-        Translate::reloadLanguage('en');
-    }
 }
diff --git a/tests/PHPUnit/Unit/Period/YearTest.php b/tests/PHPUnit/Unit/Period/YearTest.php
index fad41535de8e89ce3bc6e0981adce7d46b778ede..13d688bda9f7b2db2a83aea4c3b14b5197f4f471 100644
--- a/tests/PHPUnit/Unit/Period/YearTest.php
+++ b/tests/PHPUnit/Unit/Period/YearTest.php
@@ -10,9 +10,8 @@ namespace Piwik\Tests\Unit\Period;
 
 use Piwik\Date;
 use Piwik\Period\Year;
-use Piwik\Translate;
 
-class Period_YearTest extends \PHPUnit_Framework_TestCase
+class YearTest extends BasePeriodTest
 {
     /**
      * test normal case
@@ -70,7 +69,6 @@ class Period_YearTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedShortString()
     {
-        Translate::loadEnglishTranslation();
         $year = new Year(Date::factory('2024-10-09'));
         $shouldBe = '2024';
         $this->assertEquals($shouldBe, $year->getLocalizedShortString());
@@ -81,7 +79,6 @@ class Period_YearTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetLocalizedLongString()
     {
-        Translate::loadEnglishTranslation();
         $year = new Year(Date::factory('2024-10-09'));
         $shouldBe = '2024';
         $this->assertEquals($shouldBe, $year->getLocalizedLongString());
@@ -92,7 +89,6 @@ class Period_YearTest extends \PHPUnit_Framework_TestCase
      */
     public function testGetPrettyString()
     {
-        Translate::loadEnglishTranslation();
         $year = new Year(Date::factory('2024-10-09'));
         $shouldBe = '2024';
         $this->assertEquals($shouldBe, $year->getPrettyString());
diff --git a/tests/PHPUnit/Unit/TranslateTest.php b/tests/PHPUnit/Unit/TranslateTest.php
index a26c9c3713b2f6a05038b285651fa8b2d4c6990a..4cbbe376d57bb0578361c3aa720b90ea7b1e613c 100644
--- a/tests/PHPUnit/Unit/TranslateTest.php
+++ b/tests/PHPUnit/Unit/TranslateTest.php
@@ -10,6 +10,9 @@ namespace Piwik\Tests\Unit;
 
 use Piwik\Translate;
 
+/**
+ * @group Translation
+ */
 class TranslateTest extends \PHPUnit_Framework_TestCase
 {
     /**
diff --git a/tests/PHPUnit/Unit/Translation/Loader/JsonFileLoaderTest.php b/tests/PHPUnit/Unit/Translation/Loader/JsonFileLoaderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d1770c491de7cf295c792bea5001ba81222a237b
--- /dev/null
+++ b/tests/PHPUnit/Unit/Translation/Loader/JsonFileLoaderTest.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Tests\Unit\Translation\Loader;
+
+use Piwik\Translation\Loader\JsonFileLoader;
+
+/**
+ * @group Translation
+ */
+class JsonFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+    public function test_shouldLoadJsonFile()
+    {
+        $loader = new JsonFileLoader();
+        $translations = $loader->load('en', array(__DIR__ . '/fixtures/dir1'));
+
+        $expected = array(
+            'General' => array(
+                'test1' => 'Hello',
+                'test2' => 'Hello',
+            ),
+        );
+
+        $this->assertEquals($expected, $translations);
+    }
+
+    public function test_shouldIgnoreMissingFiles()
+    {
+        $loader = new JsonFileLoader();
+        $translations = $loader->load('foo', array(__DIR__ . '/fixtures/dir1'));
+
+        $this->assertEquals(array(), $translations);
+    }
+
+    public function test_shouldMergeTranslations_ifLoadingMultipleFiles()
+    {
+        $loader = new JsonFileLoader();
+        $translations = $loader->load('en', array(__DIR__ . '/fixtures/dir1', __DIR__ . '/fixtures/dir2'));
+
+        $expected = array(
+            'General' => array(
+                'test1' => 'Hello',
+                'test2' => 'Hello 2', // the second file should overwrite the first one
+                'test3' => 'Hello 3',
+            ),
+        );
+
+        $this->assertEquals($expected, $translations);
+    }
+}
diff --git a/tests/PHPUnit/Unit/Translation/Loader/LoaderCacheTest.php b/tests/PHPUnit/Unit/Translation/Loader/LoaderCacheTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2b0033339679b90b4d690c8e170a12a5b02a4a2b
--- /dev/null
+++ b/tests/PHPUnit/Unit/Translation/Loader/LoaderCacheTest.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Tests\Unit\Translation\Loader;
+
+use Piwik\Cache\Backend\ArrayCache;
+use Piwik\Cache\Lazy;
+use Piwik\Translation\Loader\LoaderCache;
+
+/**
+ * @group Translation
+ */
+class LoaderCacheTest extends \PHPUnit_Framework_TestCase
+{
+    public function test_shouldNotLoad_ifInCache()
+    {
+        $cache = $this->getMock('Piwik\Cache\Lazy', array(), array(), '', false);
+        $cache->expects($this->any())
+            ->method('fetch')
+            ->willReturn(array('translations!'));
+        $wrappedLoader = $this->getMockForAbstractClass('Piwik\Translation\Loader\LoaderInterface');
+        $wrappedLoader->expects($this->never())
+            ->method('load');
+
+        $loader = new LoaderCache($wrappedLoader, $cache);
+        $translations = $loader->load('en', array('foo'));
+
+        $this->assertEquals(array('translations!'), $translations);
+    }
+
+    public function test_shouldLoad_ifNotInCache()
+    {
+        $cache = $this->getMock('Piwik\Cache\Lazy', array(), array(), '', false);
+        $cache->expects($this->any())
+            ->method('fetch')
+            ->willReturn(null);
+        $wrappedLoader = $this->getMockForAbstractClass('Piwik\Translation\Loader\LoaderInterface');
+        $wrappedLoader->expects($this->once())
+            ->method('load')
+            ->with('en', array('foo'))
+            ->willReturn(array('translations!'));
+
+        $loader = new LoaderCache($wrappedLoader, $cache);
+        $translations = $loader->load('en', array('foo'));
+
+        $this->assertEquals(array('translations!'), $translations);
+    }
+
+    public function test_shouldReLoad_ifDifferentDirectories()
+    {
+        $cache = new Lazy(new ArrayCache());
+
+        $wrappedLoader = $this->getMockForAbstractClass('Piwik\Translation\Loader\LoaderInterface');
+        $wrappedLoader->expects($this->exactly(2))
+            ->method('load')
+            ->willReturn(array('translations!'));
+
+        $loader = new LoaderCache($wrappedLoader, $cache);
+
+        // Should call the wrapped loader only once
+        $loader->load('en', array('foo'));
+        $loader->load('en', array('foo'));
+
+        // Should call the wrapped loader a second time
+        $loader->load('en', array('foo', 'bar'));
+    }
+}
diff --git a/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir1/en.json b/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir1/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..f7518b863daa4d7a4b2484f7592d364b4a7518dc
--- /dev/null
+++ b/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir1/en.json
@@ -0,0 +1,6 @@
+{
+  "General": {
+    "test1": "Hello",
+    "test2": "Hello"
+  }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir1/fr.json b/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir1/fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..b1a2f0aca926efab3ec18a8c61aa41b8c1064cb3
--- /dev/null
+++ b/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir1/fr.json
@@ -0,0 +1,5 @@
+{
+  "General": {
+    "test1": "Bonjour"
+  }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir2/en.json b/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir2/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..592e802d362124460e328adc88a3d7db6e930e51
--- /dev/null
+++ b/tests/PHPUnit/Unit/Translation/Loader/fixtures/dir2/en.json
@@ -0,0 +1,6 @@
+{
+  "General": {
+    "test2": "Hello 2",
+    "test3": "Hello 3"
+  }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Translation/TranslatorTest.php b/tests/PHPUnit/Unit/Translation/TranslatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e97223e7dbe0179228f8ffbbdf6b65906865967
--- /dev/null
+++ b/tests/PHPUnit/Unit/Translation/TranslatorTest.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Tests\Unit\Translation\Loader;
+
+use Piwik\Translation\Loader\JsonFileLoader;
+use Piwik\Translation\Translator;
+
+/**
+ * @group Translation
+ */
+class TranslatorTest extends \PHPUnit_Framework_TestCase
+{
+    public function test_translate_shouldReturnTranslationId_ifNoTranslationFound()
+    {
+        $loader = $this->createLoader();
+        $translator = new Translator($loader, array());
+
+        $this->assertEquals('General_foo', $translator->translate('General_foo'));
+    }
+
+    public function test_translate_shouldReturnTranslation()
+    {
+        $loader = $this->createLoader(array(
+            'General' => array(
+                'foo' => 'Hello world',
+            ),
+        ));
+        $translator = new Translator($loader, array());
+
+        $this->assertEquals('Hello world', $translator->translate('General_foo'));
+    }
+
+    public function test_translate_shouldReplacePlaceholders()
+    {
+        $loader = $this->createLoader(array(
+            'General' => array(
+                'foo' => 'Hello %s',
+            ),
+        ));
+        $translator = new Translator($loader, array());
+
+        $this->assertEquals('Hello John', $translator->translate('General_foo', 'John'));
+    }
+
+    public function test_translate_withADifferentLanguage()
+    {
+        $translator = new Translator(new JsonFileLoader(), array(__DIR__ . '/Loader/fixtures/dir1'));
+
+        $this->assertEquals('Hello', $translator->translate('General_test1'));
+
+        $translator->setCurrentLanguage('fr');
+        $this->assertEquals('fr', $translator->getCurrentLanguage());
+        $this->assertEquals('Bonjour', $translator->translate('General_test1'));
+    }
+
+    public function test_translate_shouldFallback_ifTranslationNotFound()
+    {
+        $translator = new Translator(new JsonFileLoader(), array(__DIR__ . '/Loader/fixtures/dir1'));
+        $translator->setCurrentLanguage('fr');
+        $this->assertEquals('Hello', $translator->translate('General_test2'));
+    }
+
+    public function test_addDirectory_shouldImportNewTranslations()
+    {
+        $translator = new Translator(new JsonFileLoader(), array(__DIR__ . '/Loader/fixtures/dir1'));
+        // translation not found
+        $this->assertEquals('General_test3', $translator->translate('General_test3'));
+
+        $translator->addDirectory(__DIR__ . '/Loader/fixtures/dir2');
+        // translation is now found
+        $this->assertEquals('Hello 3', $translator->translate('General_test3'));
+    }
+
+    public function test_addDirectory_shouldImportOverExistingTranslations()
+    {
+        $translator = new Translator(new JsonFileLoader(), array(__DIR__ . '/Loader/fixtures/dir1'));
+        $this->assertEquals('Hello', $translator->translate('General_test2'));
+
+        $translator->addDirectory(__DIR__ . '/Loader/fixtures/dir2');
+        $this->assertEquals('Hello 2', $translator->translate('General_test2'));
+    }
+
+    private function createLoader(array $translations = array())
+    {
+        $loader = $this->getMockForAbstractClass('Piwik\Translation\Loader\LoaderInterface');
+        $loader->expects($this->any())
+            ->method('load')
+            ->willReturn($translations);
+
+        return $loader;
+    }
+}
diff --git a/tests/PHPUnit/bootstrap.php b/tests/PHPUnit/bootstrap.php
index 1507ced00d7dfdaa6ef64bb18020e5ee20cf04d5..a0f9d5b349a56a3342bd7cbf297c29769395a6e1 100644
--- a/tests/PHPUnit/bootstrap.php
+++ b/tests/PHPUnit/bootstrap.php
@@ -3,6 +3,7 @@
 use Piwik\Container\StaticContainer;
 use Piwik\Http;
 use Piwik\Tests\Framework\Fixture;
+use Piwik\Intl\Locale;
 
 define('PIWIK_TEST_MODE', true);
 define('PIWIK_PRINT_ERROR_BACKTRACE', false);
@@ -58,6 +59,8 @@ foreach($fixturesToLoad as $fixturePath) {
     }
 }
 
+Locale::setDefaultLocale();
+
 function prepareServerVariables()
 {
     \Piwik\Config::getInstance()->init();