diff --git a/core/lib/Drupal/Component/Gettext/PoStreamReader.php b/core/lib/Drupal/Component/Gettext/PoStreamReader.php
index 74a9bae..83ad207 100644
--- a/core/lib/Drupal/Component/Gettext/PoStreamReader.php
+++ b/core/lib/Drupal/Component/Gettext/PoStreamReader.php
@@ -170,7 +170,7 @@ public function open() {
       $this->readHeader();
     }
     else {
-      throw new Exception('Cannot open stream without URI set.');
+      throw new \Exception('Cannot open stream without URI set.');
     }
   }
 
@@ -185,7 +185,7 @@ public function close() {
       fclose($this->_fd);
     }
     else {
-      throw new Exception('Cannot close stream that is not open.');
+      throw new \Exception('Cannot close stream that is not open.');
     }
   }
 
diff --git a/core/modules/locale/config/locale.settings.yml b/core/modules/locale/config/locale.settings.yml
index a1c9f35..c5ddd67 100644
--- a/core/modules/locale/config/locale.settings.yml
+++ b/core/modules/locale/config/locale.settings.yml
@@ -3,3 +3,7 @@ translation:
   check_disabled_modules: false
   default_filename: '%project-%version.%language.po'
   default_server_pattern: 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po'
+  overwrite_customized: false
+  overwrite_not_customized: true
+  update_interval_days: '0'
+
diff --git a/core/modules/locale/lib/Drupal/locale/Gettext.php b/core/modules/locale/lib/Drupal/locale/Gettext.php
index a7c37ca..40472f0 100644
--- a/core/modules/locale/lib/Drupal/locale/Gettext.php
+++ b/core/modules/locale/lib/Drupal/locale/Gettext.php
@@ -53,10 +53,10 @@ static function filesToArray($langcode, array $files) {
    *
    * @param stdClass $file
    *   File object with an URI property pointing at the file's path.
-   *
+   *   - "langcode": The language the strings will be added to.
+   *   - "uri": File URI.
    * @param array $options
    *   An array with options that can have the following elements:
-   *   - 'langcode': The language code, required.
    *   - 'overwrite_options': Overwrite options array as defined in
    *     Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
    *   - 'customized': Flag indicating whether the strings imported from $file
@@ -83,7 +83,7 @@ static function fileToDatabase($file, $options) {
     );
     // Instantiate and initialize the stream reader for this file.
     $reader = new PoStreamReader();
-    $reader->setLangcode($options['langcode']);
+    $reader->setLangcode($file->langcode);
     $reader->setURI($file->uri);
 
     try {
@@ -100,7 +100,7 @@ static function fileToDatabase($file, $options) {
 
     // Initialize the database writer.
     $writer = new PoDatabaseWriter();
-    $writer->setLangcode($options['langcode']);
+    $writer->setLangcode($file->langcode);
     $writer_options = array(
       'overwrite_options' => $options['overwrite_options'],
       'customized' => $options['customized'],
diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php
index 74cf40d..f2660fd 100644
--- a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php
+++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php
@@ -102,7 +102,7 @@ function getHeader() {
    *   Always, because you cannot set the PO header of a reader.
    */
   function setHeader(PoHeader $header) {
-    throw new Exception('You cannot set the PO header in a reader.');
+    throw new \Exception('You cannot set the PO header in a reader.');
   }
 
   /**
diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php
index b5b03c0..eb02890 100644
--- a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php
+++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php
@@ -160,14 +160,14 @@ function setHeader(PoHeader $header) {
     // Check for options.
     $options = $this->getOptions();
     if (empty($options)) {
-      throw new Exception("Options should be set before assigning a PoHeader.");
+      throw new \Exception('Options should be set before assigning a PoHeader.');
     }
     $overwrite_options = $options['overwrite_options'];
 
     // Check for langcode.
     $langcode = $this->_langcode;
     if (empty($langcode)) {
-      throw new Exception("Langcode should be set before assigning a PoHeader.");
+      throw new \Exception('Langcode should be set before assigning a PoHeader.');
     }
 
     if (array_sum($overwrite_options) || empty($locale_plurals[$langcode]['plurals'])) {
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php
deleted file mode 100644
index 017e2c5..0000000
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php
+++ /dev/null
@@ -1,234 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\locale\Tests\LocaleCompareTest.
- */
-
-namespace Drupal\locale\Tests;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests for comparing status of existing project translations with available translations.
- */
-class LocaleCompareTest extends WebTestBase {
-
-  /**
-   * The path of the translations directory where local translations are stored.
-   *
-   * @var string
-   */
-  private $tranlations_directory;
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('update', 'locale', 'locale_test');
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Compare project states',
-      'description' => 'Tests for comparing status of existing project translations with available translations.',
-      'group' => 'Locale',
-    );
-  }
-
-  /**
-   * Setup the test environment.
-   *
-   * We use German as default test language. Due to hardcoded configurations in
-   * the locale_test module, the language can not be chosen randomly.
-   */
-  function setUp() {
-    parent::setUp();
-    module_load_include('compare.inc', 'locale');
-    $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
-    $this->drupalLogin($admin_user);
-    $this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'de'), t('Add language'));
-  }
-
-  /**
-   * Set the value of the default translations directory.
-   *
-   * @param string $path
-   *   Path of the translations directory relative to the drupal installation
-   *   directory.
-   */
-  private function setTranslationsDirectory($path) {
-    $this->tranlations_directory = $path;
-    file_prepare_directory($path, FILE_CREATE_DIRECTORY);
-    variable_set('locale_translate_file_directory', $path);
-  }
-
-  /**
-   * Creates a translation file and tests its timestamp.
-   *
-   * @param string $path
-   *   Path of the file relative to the public file path.
-   * @param string $filename
-   *   Name of the file to create.
-   * @param integer $timestamp
-   *   Timestamp to set the file to. Defaults to current time.
-   * @param string $data
-   *   Translation data to put into the file. Po header data will be added.
-   */
-  private function makePoFile($path, $filename, $timestamp = NULL, $data = '') {
-    $timestamp = $timestamp ? $timestamp : REQUEST_TIME;
-    $path = 'public://' . $path;
-    $po_header = <<<EOF
-msgid ""
-msgstr ""
-"Project-Id-Version: Drupal 8\\n"
-"MIME-Version: 1.0\\n"
-"Content-Type: text/plain; charset=UTF-8\\n"
-"Content-Transfer-Encoding: 8bit\\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
-
-EOF;
-
-    file_prepare_directory($path, FILE_CREATE_DIRECTORY);
-    $file = entity_create('file', array(
-      'uid' => 1,
-      'filename' => $filename,
-      'uri' => $path . '/' . $filename,
-      'filemime' => 'text/x-gettext-translation',
-      'timestamp' => $timestamp,
-      'status' => FILE_STATUS_PERMANENT,
-    ));
-    file_put_contents($file->uri, $po_header . $data);
-    touch(drupal_realpath($file->uri), $timestamp);
-    $file->save();
-  }
-
-  /**
-   * Test for translation status storage and translation status comparison.
-   */
-  function testLocaleCompare() {
-    // Create and login user.
-    $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages'));
-    $this->drupalLogin($admin_user);
-
-    module_load_include('compare.inc', 'locale');
-
-    // Check if hidden modules are not included.
-    $projects = locale_translation_project_list();
-    $this->assertFalse(isset($projects['locale_test']), 'Hidden module not found');
-
-    // Make the test modules look like a normal custom module. i.e. make the
-    // modules not hidden. locale_test_system_info_alter() modifies the project
-    // info of the locale_test and locale_test_disabled modules.
-    state()->set('locale_translation_test_system_info_alter', TRUE);
-
-    // Reset static system list caches to reflect info changes.
-    drupal_static_reset('locale_translation_project_list');
-    system_list_reset();
-
-    // Check if interface translation data is collected from hook_info.
-    $projects = locale_translation_project_list();
-    $this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
-    $this->assertEqual($projects['locale_test']['name'] , 'locale_test', format_string('%key found in project info.', array('%key' => 'interface translation project')));
-
-    // Get the locale settings.
-    $config = config('locale.settings');
-
-    // Check if disabled modules are detected.
-    $config->set('translation.check_disabled_modules', TRUE)->save();
-    drupal_static_reset('locale_translation_project_list');
-    $projects = locale_translation_project_list();
-    $this->assertTrue(isset($projects['locale_test_disabled']), 'Disabled module found');
-
-    // Check the fully processed list of project data of both enabled and
-    // disabled modules.
-    $config->set('translation.check_disabled_modules', TRUE)->save();
-    drupal_static_reset('locale_translation_project_list');
-    $projects = locale_translation_get_projects();
-    $this->assertEqual($projects['drupal']->name, 'drupal', 'Core project found');
-    $this->assertEqual($projects['locale_test']->server_pattern, 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
-    $this->assertEqual($projects['locale_test_disabled']->status, '0', 'Disabled module found');
-    $config->delete('translation.check_disabled_modules');
-
-    // Return the locale test modules back to their hidden state.
-    state()->delete('locale_translation_test_system_info_alter');
-  }
-
-  /**
-   * Checks if local or remote translation sources are detected.
-   *
-   * This test requires a simulated environment for local and remote files.
-   * Normally remote files are located at a remote server (e.g. ftp.drupal.org).
-   * For testing we can not rely on this. A directory in the file system of the
-   * test site is designated for remote files and is addressed using an absolute
-   * URL. Because Drupal does not allow files with a po extension to be accessed
-   * (denied in .htaccess) the translation files get a txt extension. Another
-   * directory is designated for local translation files.
-   *
-   * The translation status process by default checks the status of the
-   * installed projects. For testing purpose a predefined set of modules with
-   * fixed file names and release versions is used. Using a
-   * hook_locale_translation_projects_alter implementation in the locale_test
-   * module this custom project definition is applied.
-   *
-   * This test generates a set of local and remote translation files in their
-   * respective local and remote translation directory. The test checks whether
-   * the most recent files are selected in the different check scenarios: check
-   * for local files only, check for remote files only, check for both local and
-   * remote files.
-   */
-  function testCompareCheckLocal() {
-    $config = config('locale.settings');
-
-    // A flag is set to let the locale_test module replace the project data with
-    // a set of test projects.
-    state()->set('locale_translation_test_projects', TRUE);
-
-    // Setup timestamps to identify old and new translation sources.
-    $timestamp_old = REQUEST_TIME - 100;
-    $timestamp_new = REQUEST_TIME;
-
-    // Set up the environment.
-    $public_path = variable_get('file_public_path', conf_path() . '/files');
-    $this->setTranslationsDirectory($public_path . '/local');
-    $config->set('translation.default_filename', '%project-%version.%language.txt')->save();
-
-    // Add a number of files to the local file system to serve as remote
-    // translation server and match the project definitions set in
-    // locale_test_locale_translation_projects_alter().
-    $this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de.txt', $timestamp_new);
-    $this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de.txt', $timestamp_old);
-    $this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de.txt', $timestamp_old);
-
-    // Add a number of files to the local file system to serve as local
-    // translation files and match the project definitions set in
-    // locale_test_locale_translation_projects_alter().
-    $this->makePoFile('local', 'contrib_module_one-8.x-1.1.de.txt', $timestamp_old);
-    $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de.txt', $timestamp_new);
-    $this->makePoFile('local', 'custom_module_one.de.po', $timestamp_new);
-
-    // Get status of translation sources at local file system.
-    $config->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)->save();
-    $this->drupalGet('admin/reports/translations/check');
-    $result = state()->get('locale_translation_status');
-    $this->assertEqual($result['contrib_module_one']['de']->type, 'local', 'Translation of contrib_module_one found');
-    $this->assertEqual($result['contrib_module_one']['de']->timestamp, $timestamp_old, 'Translation timestamp found');
-    $this->assertEqual($result['contrib_module_two']['de']->type, 'local', 'Translation of contrib_module_two found');
-    $this->assertEqual($result['contrib_module_two']['de']->timestamp, $timestamp_new, 'Translation timestamp found');
-    $this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of locale_test found');
-    $this->assertEqual($result['custom_module_one']['de']->type, 'local', 'Translation of custom_module_one found');
-
-    // Get status of translation sources at both local and remote the locations.
-    $config->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL)->save();
-    $this->drupalGet('admin/reports/translations/check');
-    $result = state()->get('locale_translation_status');
-    $this->assertEqual($result['contrib_module_one']['de']->type, 'remote', 'Translation of contrib_module_one found');
-    $this->assertEqual($result['contrib_module_one']['de']->timestamp, $timestamp_new, 'Translation timestamp found');
-    $this->assertEqual($result['contrib_module_two']['de']->type, 'local', 'Translation of contrib_module_two found');
-    $this->assertEqual($result['contrib_module_two']['de']->timestamp, $timestamp_new, 'Translation timestamp found');
-    $this->assertEqual($result['contrib_module_three']['de']->type, 'remote', 'Translation of contrib_module_three found');
-    $this->assertEqual($result['contrib_module_three']['de']->timestamp, $timestamp_old, 'Translation timestamp found');
-    $this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of locale_test found');
-    $this->assertEqual($result['custom_module_one']['de']->type, 'local', 'Translation of custom_module_one found');
-  }
-}
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php
deleted file mode 100644
index 06d292f..0000000
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php
+++ /dev/null
@@ -1,202 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\locale\Tests\LocaleFileImportStatus.
- */
-
-namespace Drupal\locale\Tests;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Functional tests for the import of translation files.
- */
-class LocaleFileImportStatus extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('locale');
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Translation file import status',
-      'description' => 'Tests the status of imported translation files.',
-      'group' => 'Locale',
-    );
-  }
-
-  function setUp() {
-    parent::setUp();
-    // Create and login user.
-    $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages'));
-    $this->drupalLogin($admin_user);
-
-    // Copy test po files to the translations directory.
-    file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE);
-    file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE);
-  }
-
-  /**
-   * Add a language.
-   *
-   * @param $langcode
-   *   The language of the langcode to add.
-   */
-  function addLanguage($langcode) {
-    $edit = array('predefined_langcode' => $langcode);
-    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
-    drupal_static_reset('language_list');
-    $this->assertTrue(language_load($langcode), t('Language %langcode added.', array('%langcode' => $langcode)));
-  }
-
-  /**
-   * Get translations for an array of strings.
-   *
-   * @param $strings
-   *   An array of strings to translate.
-   * @param $langcode
-   *   The language code of the language to translate to.
-   */
-  function checkTranslations($strings, $langcode) {
-    foreach ($strings as $source => $translation) {
-      $db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField();
-      $this->assertEqual((string) $translation, (string) $db_translation);
-    }
-  }
-
-  /**
-   * Import a single interface translation file.
-   *
-   * @param $langcode
-   *   Langcode of the po file and language to import.
-   * @param int $timestamp_difference
-   *  (optional) Timestamp offset, used to mock older or newer files.
-   *
-   * @return stdClass
-   *   A file object of type stdClass.
-   */
-  function mockImportedPoFile($langcode, $timestamp_difference = 0) {
-    $testfile_uri = 'translations://test.' . $langcode . '.po';
-
-    $file = locale_translate_file_create($testfile_uri);
-    $file->original_timestamp = $file->timestamp;
-    $file->timestamp = $file->timestamp + $timestamp_difference;
-    $file->langcode = $langcode;
-
-    // Fill the {locale_file} with a custom timestamp.
-    if ($timestamp_difference != 0) {
-      locale_translate_update_file_history($file);
-    }
-
-    $count = db_query('SELECT COUNT(*) FROM {locale_file} WHERE langcode = :langcode', array(':langcode' => $langcode))->fetchField();
-    $this->assertEqual(1, $count, format_plural($count, '@count file registered in {locale_file}.', '@count files registered in {locale_file}.'));
-
-    $result = db_query('SELECT langcode, uri FROM {locale_file}')->fetchAssoc();
-    $this->assertEqual($result['uri'], $testfile_uri, t('%uri is in {locale_file}.', array('%uri' => $result['uri'])));
-    $this->assertEqual($result['langcode'], $langcode, t('Langcode is %langcode.', array('%langcode' => $langcode)));
-    return $file;
-  }
-
-  /**
-   * Test the basic bulk import functionality.
-   */
-  function testBulkImport() {
-    $langcode = 'de';
-
-    // Translations should not exist.
-    $strings = array(
-      'Monday' => '',
-      'Tuesday' => '',
-    );
-    $this->checkTranslations($strings, $langcode);
-
-    // Add language.
-    $this->addLanguage($langcode);
-
-    // The file was imported, translations should exist.
-    $strings = array(
-      'Monday' => 'Montag',
-      'Tuesday' => 'Dienstag',
-    );
-    $this->checkTranslations($strings, $langcode);
-  }
-
-  /**
-   * Update a pre-existing file.
-   */
-  function testBulkImportUpdateExisting() {
-    $langcode = 'de';
-
-    // Translations should not exist.
-    $strings = array(
-      'Monday' => '',
-      'Tuesday' => '',
-    );
-    $this->checkTranslations($strings, $langcode);
-
-    // Fill the {locale_file} table with an older file.
-    $file = $this->mockImportedPoFile($langcode, -1);
-
-    // Add language.
-    $this->addLanguage($langcode);
-
-    // The file was imported, translations should exist.
-    $strings = array(
-      'Monday' => 'Montag',
-      'Tuesday' => 'Dienstag',
-    );
-    $this->checkTranslations($strings, $langcode);
-
-    $timestamp = db_query('SELECT timestamp FROM {locale_file} WHERE uri = :uri', array(':uri' => $file->uri))->fetchField();
-    // Ensure that $timestamp is now greater than the manipulated timestamp.
-    $this->assertTrue($timestamp > $file->timestamp, 'Timestamp on locale_file updated.');
-  }
-
-  /**
-   * Don't update a pre-existing file.
-   */
-  function testBulkImportNotUpdateExisting() {
-    $langcode = 'de';
-
-    // Translations should not exist.
-    $strings = array(
-      'Monday' => '',
-      'Tuesday' => '',
-    );
-    $this->checkTranslations($strings, $langcode);
-
-    // Fill the {locale_file} table with a newer file.
-    $file = $this->mockImportedPoFile($langcode, 1);
-
-    // Add language.
-    $this->addLanguage($langcode);
-
-    // The file was not imported, the translation should not exist.
-    $strings = array(
-      'Monday' => '',
-      'Tuesday' => '',
-    );
-    $this->checkTranslations($strings, $langcode);
-
-    $timestamp = db_query('SELECT timestamp FROM {locale_file} WHERE uri = :uri', array(':uri' => $file->uri))->fetchField();
-    $this->assertEqual($timestamp, $file->timestamp);
-  }
-
-  /**
-   * Delete translation files after deleting a language.
-   */
-  function testDeleteLanguage() {
-    $langcode = 'de';
-    $this->addLanguage($langcode);
-    $file_uri = 'translations://po_' . $this->randomName() . '.' . $langcode . '.po';
-    file_put_contents(drupal_realpath($file_uri), $this->randomName());
-    $this->assertTrue(is_file($file_uri), 'Translation file is created.');
-    language_delete($langcode);
-    $this->assertTrue($file_uri);
-    $this->assertFalse(is_file($file_uri), 'Translation file deleted after deleting language');
-  }
-}
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php
index 4dca9e7..fd3667c 100644
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php
@@ -58,7 +58,7 @@ function testStandalonePoFile() {
     $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.'));
 
     // The import should have created 8 strings.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
 
     // This import should have saved plural forms to have 2 variants.
     $locale_plurals = variable_get('locale_translation_plurals', array());
@@ -74,8 +74,9 @@ function testStandalonePoFile() {
     ));
 
     // The import should have created 1 string and rejected 2.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
-    $skip_message = format_plural(2, 'A translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
+
+    $skip_message = format_plural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
     $this->assertRaw($skip_message, t('Unsafe strings were skipped.'));
 
     // Try importing a zero byte sized .po file.
@@ -84,7 +85,7 @@ function testStandalonePoFile() {
     ));
 
     // The import should have created 0 string and rejected 0.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 0)), 'The empty translation file was successfully imported.');
+    $this->assertRaw(t('One translation file could not be imported. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog'))), 'The empty translation file was successfully imported.');
 
     // Try importing a .po file which doesn't exist.
     $name = $this->randomName(16);
@@ -103,7 +104,7 @@ function testStandalonePoFile() {
     ));
 
     // The import should have created 1 string.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
     // Ensure string wasn't overwritten.
     $search = array(
       'string' => 'Montag',
@@ -125,7 +126,7 @@ function testStandalonePoFile() {
     ));
 
     // The import should have updated 2 strings.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.'));
     // Ensure string was overwritten.
     $search = array(
       'string' => 'Montag',
@@ -145,7 +146,7 @@ function testStandalonePoFile() {
     ));
 
     // The import should have created 6 strings.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.'));
 
     // The database should now contain 6 customized strings (two imported
     // strings are not translated).
@@ -161,7 +162,7 @@ function testStandalonePoFile() {
     ));
 
     // The import should have created 1 string.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.'));
     // Ensure string wasn't overwritten.
     $search = array(
       'string' => 'januari',
@@ -180,7 +181,7 @@ function testStandalonePoFile() {
     ));
 
     // The import should have updated 2 strings.
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The customized translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The customized translation file was successfully imported.'));
     // Ensure string was overwritten.
     $search = array(
       'string' => 'januari',
@@ -193,60 +194,6 @@ function testStandalonePoFile() {
   }
 
   /**
-   * Test automatic import of a module's translation files.
-   */
-  function testAutomaticModuleTranslationImportLanguageEnable() {
-    // Code for the language - manually set to match the test translation file.
-    $langcode = 'xx';
-    // The English name for the language.
-    $name = $this->randomName(16);
-
-    // Create a custom language.
-    $edit = array(
-      'predefined_langcode' => 'custom',
-      'langcode' => $langcode,
-      'name' => $name,
-      'direction' => '0',
-    );
-    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
-
-    // Ensure the translation file was automatically imported when language was
-    // added.
-    $this->assertText(t('One translation file imported.'), t('Language file automatically imported.'));
-    $this->assertText(t('A translation string was skipped because of disallowed or malformed HTML'), t('Language file automatically imported.'));
-
-    // Ensure strings were successfully imported.
-    $search = array(
-      'string' => 'lundi',
-      'langcode' => $langcode,
-      'translation' => 'translated',
-    );
-    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
-    $this->assertNoText(t('No strings available.'), t('String successfully imported.'));
-
-    // Ensure multiline string was imported.
-    $search = array(
-      'string' => 'Source string for multiline translation',
-      'langcode' => $langcode,
-      'translation' => 'all',
-    );
-    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
-    $this->assertText('Multiline translation string to make sure that import works with it.', t('String successfully imported.'));
-
-    // Ensure 'Allowed HTML source string' was imported but the translation for
-    // 'Another allowed HTML source string' was not because it contains invalid
-    // HTML.
-    $search = array(
-      'string' => 'HTML source string',
-      'langcode' => $langcode,
-      'translation' => 'all',
-    );
-    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
-    $this->assertText('Allowed HTML source string', t('String successfully imported.'));
-    $this->assertNoText('Another allowed HTML source string', t('String with disallowed translation not imported.'));
-  }
-
-  /**
    * Test msgctxt context support.
    */
   function testLanguageContext() {
@@ -270,7 +217,7 @@ function testEmptyMsgstr() {
       'langcode' => $langcode,
     ));
 
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
     $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', t('String imported and translated.'));
 
     // Try importing a .po file.
@@ -278,7 +225,7 @@ function testEmptyMsgstr() {
       'langcode' => $langcode,
       'overwrite_options[not_customized]' => TRUE,
     ));
-    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.'));
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.'));
 
     $str = "Operations";
     $search = array(
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
new file mode 100644
index 0000000..6f96943
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
@@ -0,0 +1,797 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\Tests\LocaleCompareTest.
+ */
+
+namespace Drupal\locale\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests for comparing status of existing project translations with available translations.
+ */
+class LocaleUpdateTest extends WebTestBase {
+
+  /**
+   * The path of the translations directory where local translations are stored.
+   *
+   * @var string
+   */
+  private $tranlations_directory;
+
+  /**
+   * Timestamp for an old translation.
+   *
+   * @var integer
+   */
+  private $timestamp_old;
+
+  /**
+   * Timestamp for a medium aged translation.
+   *
+   * @var integer
+   */
+  private $timestamp_medium;
+
+  /**
+   * Timestamp for a new translation.
+   *
+   * @var integer
+   */
+  private $timestamp_new;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('update', 'locale', 'locale_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Update Interface translations',
+      'description' => 'Tests for updating the interface translations of projects.',
+      'group' => 'Locale',
+    );
+  }
+
+  /**
+   * Setup the test environment.
+   *
+   * We use German as default test language. Due to hardcoded configurations in
+   * the locale_test module, the language can not be chosen randomly.
+   */
+  function setUp() {
+    parent::setUp();
+    module_load_include('compare.inc', 'locale');
+    module_load_include('fetch.inc', 'locale');
+    $admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
+    $this->drupalLogin($admin_user);
+    $this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'de'), t('Add language'));
+
+    // Setup timestamps to identify old and new translation sources.
+    $this->timestamp_old = REQUEST_TIME - 300;
+    $this->timestamp_medium = REQUEST_TIME - 200;
+    $this->timestamp_new = REQUEST_TIME - 100;
+    $this->timestamp_now = REQUEST_TIME;
+  }
+
+  /**
+   * Sets the value of the default translations directory.
+   *
+   * @param string $path
+   *   Path of the translations directory relative to the drupal installation
+   *   directory.
+   */
+  private function setTranslationsDirectory($path) {
+    $this->tranlations_directory = $path;
+    file_prepare_directory($path, FILE_CREATE_DIRECTORY);
+    variable_set('locale_translate_file_directory', $path);
+  }
+
+  /**
+   * Adds a language.
+   *
+   * @param $langcode
+   *   The language code of the language to add.
+   */
+  function addLanguage($langcode) {
+    $edit = array('predefined_langcode' => $langcode);
+    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+    drupal_static_reset('language_list');
+    $this->assertTrue(language_load($langcode), t('Language %langcode added.', array('%langcode' => $langcode)));
+  }
+
+  /**
+   * Creates a translation file and tests its timestamp.
+   *
+   * @param string $path
+   *   Path of the file relative to the public file path.
+   * @param string $filename
+   *   Name of the file to create.
+   * @param integer $timestamp
+   *   Timestamp to set the file to. Defaults to current time.
+   * @param array $translations
+   *   Array of source/target value translation strings. Only singular strings
+   *   are supported, no plurals. No double quotes are allowed in source and
+   *   translations strings.
+   */
+  private function makePoFile($path, $filename, $timestamp = NULL, $translations = array()) {
+    $timestamp = $timestamp ? $timestamp : REQUEST_TIME;
+    $path = 'public://' . $path;
+    $text = '';
+    $po_header = <<<EOF
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+EOF;
+
+    // Convert array of translations to Gettext source and translation strings.
+    if ($translations) {
+      foreach ($translations as $source => $target) {
+        $text .= 'msgid "'. $source . '"' . "\n";
+        $text .= 'msgstr "'. $target . '"' . "\n";
+      }
+    }
+
+    file_prepare_directory($path, FILE_CREATE_DIRECTORY);
+    $file = entity_create('file', array(
+      'uid' => 1,
+      'filename' => $filename,
+      'uri' => $path . '/' . $filename,
+      'filemime' => 'text/x-gettext-translation',
+      'timestamp' => $timestamp,
+      'status' => FILE_STATUS_PERMANENT,
+    ));
+    file_put_contents($file->uri, $po_header . $text);
+    touch(drupal_realpath($file->uri), $timestamp);
+    $file->save();
+  }
+
+  /**
+   * Setup the environment containting local and remote translation files.
+   *
+   * Update tests require a simulated environment for local and remote files.
+   * Normally remote files are located at a remote server (e.g. ftp.drupal.org).
+   * For testing we can not rely on this. A directory in the file system of the
+   * test site is designated for remote files and is addressed using an absolute
+   * URL. Because Drupal does not allow files with a po extension to be accessed
+   * (denied in .htaccess) the translation files get a _po extension. Another
+   * directory is designated for local translation files.
+   *
+   * The environment is set up with the following files. File creation times are
+   * set to create different variations in test conditions.
+   *   contrib_module_one
+   *    - remote file: timestamp new
+   *    - local file:  timestamp old
+   *   contrib_module_two
+   *    - remote file: timestamp old
+   *    - local file:  timestamp new
+   *   contrib_module_three
+   *    - remote file: timestamp old
+   *    - local file:  timestamp old
+   *   custom_module_one
+   *    - local file:  timestamp new
+   * Time stamp of current translation set by setCurrentTranslations() is always
+   * timestamp medium. This makes it easy to predict which translation will be
+   * imported.
+   */
+  private function setTranslationFiles() {
+    $config = config('locale.settings');
+
+    // A flag is set to let the locale_test module replace the project data with
+    // a set of test projects which match the below project files.
+    state()->set('locale.test_projects_alter', TRUE);
+
+    // Setup the environment.
+    $public_path = variable_get('file_public_path', conf_path() . '/files');
+    $this->setTranslationsDirectory($public_path . '/local');
+    $config->set('translation.default_filename', '%project-%version.%language._po')->save();
+
+    // Setting up sets of translations for the translation files.
+    $translations_one = array('January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1');
+    $translations_two = array( 'February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2');
+    $translations_three = array('April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3');
+
+    // Add a number of files to the local file system to serve as remote
+    // translation server and match the project definitions set in
+    // locale_test_locale_translation_projects_alter().
+    $this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_new, $translations_one);
+    $this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_old, $translations_two);
+    $this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three);
+
+    // Add a number of files to the local file system to serve as local
+    // translation files and match the project definitions set in
+    // locale_test_locale_translation_projects_alter().
+    $this->makePoFile('local', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_old, $translations_one);
+    $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_new, $translations_two);
+    $this->makePoFile('local', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three);
+    $this->makePoFile('local', 'custom_module_one.de.po', $this->timestamp_new);
+  }
+
+  /**
+   * Setup existing translations in the database and set up the status of
+   * existing translations.
+   */
+  private function setCurrentTranslations() {
+    // Add non customized translations to the database.
+    $langcode = 'de';
+    $context = '';
+    $non_customized_translations = array(
+      'March' => 'Marz',
+      'June' => 'Juni',
+    );
+    foreach ($non_customized_translations as $source => $translation) {
+      $string = locale_storage()->createString(array('source' => $source, 'context' => $context))
+        ->save();
+      $target = locale_storage()->createTranslation(array(
+        'lid' => $string->getId(),
+        'language' => $langcode,
+        'translation' => $translation,
+        'customized' => LOCALE_NOT_CUSTOMIZED,
+      ))->save();
+    }
+
+    // Add customized translations to the database.
+    $customized_translations = array(
+      'January' => 'Januar_customized',
+      'February' => 'Februar_customized',
+      'May' => 'Mai_customized',
+    );
+    foreach ($customized_translations as $source => $translation) {
+      $string = locale_storage()->createString(array('source' => $source, 'context' => $context))
+        ->save();
+      $target = locale_storage()->createTranslation(array(
+        'lid' => $string->getId(),
+        'language' => $langcode,
+        'translation' => $translation,
+        'customized' => LOCALE_CUSTOMIZED,
+      ))->save();
+    }
+
+    // Add a state of current translations in locale_files.
+    $default = array(
+      'langcode' => $langcode,
+      'uri' => '',
+      'timestamp' => $this->timestamp_medium,
+      'last_checked' => $this->timestamp_medium,
+    );
+    $data[] = array(
+      'project' => 'contrib_module_one',
+      'filename' => 'contrib_module_one-8.x-1.1.de._po',
+      'version' => '8.x-1.1',
+    );
+    $data[] = array(
+      'project' => 'contrib_module_two',
+      'filename' => 'contrib_module_two-8.x-2.0-beta4.de._po',
+      'version' => '8.x-2.0-beta4',
+    );
+    $data[] = array(
+      'project' => 'contrib_module_three',
+      'filename' => 'contrib_module_three-8.x-1.0.de._po',
+      'version' => '8.x-1.0',
+    );
+    $data[] = array(
+      'project' => 'custom_module_one',
+      'filename' => 'custom_module_one.de.po',
+      'version' => '',
+    );
+    foreach ($data as $file) {
+      $file = (object) array_merge($default, $file);
+      drupal_write_record('locale_file', $file);
+    }
+  }
+
+  /**
+   * Checks the translation of a string.
+   *
+   * @param string $source
+   *   Translation source string
+   * @param string $translation
+   *   Translation to check. Use empty string to check for a not existing
+   *   translation.
+   * @param string $langcode
+   *   Language code of the language to translate to.
+   * @param string $message
+   *   (optional) A message to display with the assertion.
+   */
+  function assertTranslation($source, $translation, $langcode, $message = '') {
+    $db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField();
+    $db_translation = $db_translation == FALSE ? '' : $db_translation;
+    $this->assertEqual($translation, $db_translation, $message ? $message : format_string('Correct translation of %source (%language)', array('%source' => $source, '%language' => $langcode)));
+  }
+
+  /**
+   * Checks if a list of translatable projects gets build.
+   */
+  function testUpdateProjects() {
+    module_load_include('compare.inc', 'locale');
+
+    // Make the test modules look like a normal custom module. i.e. make the
+    // modules not hidden. locale_test_system_info_alter() modifies the project
+    // info of the locale_test and locale_test_translate modules.
+    state()->set('locale.test_system_info_alter', TRUE);
+
+    // Check if interface translation data is collected from hook_info.
+    $projects = locale_translation_project_list();
+    $this->assertFalse(isset($projects['locale_test_translate']), 'Hidden module not found');
+    $this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
+    $this->assertEqual($projects['locale_test']['name'] , 'locale_test', format_string('%key found in project info.', array('%key' => 'interface translation project')));
+  }
+
+  /**
+   * Check if a list of translatable projects can include hidden projects.
+   */
+  function testUpdateProjectsHidden() {
+    module_load_include('compare.inc', 'locale');
+    $config = config('locale.settings');
+
+    // Make the test modules look like a normal custom module.
+    state()->set('locale.test_system_info_alter', TRUE);
+
+    // Set test condition: include disabled modules when building a project list.
+    $edit = array(
+      'check_disabled_modules' => TRUE,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    $projects = locale_translation_project_list();
+    $this->assertTrue(isset($projects['locale_test_translate']), 'Disabled module found');
+    $this->assertTrue(isset($projects['locale_test']), 'Enabled module found');
+  }
+
+  /**
+   * Checks if local or remote translation sources are detected.
+   *
+   * The translation status process by default checks the status of the
+   * installed projects. For testing purpose a predefined set of modules with
+   * fixed file names and release versions is used. This custom project
+   * definition is applied using a hook_locale_translation_projects_alter
+   * implementation in the locale_test module.
+   *
+   * This test generates a set of local and remote translation files in their
+   * respective local and remote translation directory. The test checks whether
+   * the most recent files are selected in the different check scenarios: check
+   * for local files only, check for both local and remote files.
+   */
+  function testUpdateCheckStatus() {
+    $config = config('locale.settings');
+    // Set a flag to let the locale_test module replace the project data with a
+    // set of test projects.
+    state()->set('locale.test_projects_alter', TRUE);
+
+    // Create local and remote translations files.
+    $this->setTranslationFiles();
+    $config->set('translation.default_filename', '%project-%version.%language._po')->save();
+
+    // Set the test conditions.
+    $edit = array(
+      'use_source' => LOCALE_TRANSLATION_USE_SOURCE_LOCAL,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    // Get status of translation sources at local file system.
+    $this->drupalGet('admin/reports/translations/check');
+    $result = state()->get('locale.translation_status');
+    $this->assertEqual($result['contrib_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_one found');
+    $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestamp_old, 'Translation timestamp found');
+    $this->assertEqual($result['contrib_module_two']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found');
+    $this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
+    $this->assertEqual($result['locale_test']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of locale_test found');
+    $this->assertEqual($result['custom_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of custom_module_one found');
+
+    // Set the test conditions.
+    $edit = array(
+      'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    // Get status of translation sources at both local and remote locations.
+    $this->drupalGet('admin/reports/translations/check');
+    $result = state()->get('locale.translation_status');
+    $this->assertEqual($result['contrib_module_one']['de']->type, 'remote', 'Translation of contrib_module_one found');
+    $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
+    $this->assertEqual($result['contrib_module_two']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found');
+    $this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
+    $this->assertEqual($result['contrib_module_three']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_three found');
+    $this->assertEqual($result['contrib_module_three']['de']->timestamp, $this->timestamp_old, 'Translation timestamp found');
+    $this->assertEqual($result['locale_test']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of locale_test found');
+    $this->assertEqual($result['custom_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of custom_module_one found');
+  }
+
+  /**
+   * Tests translation import from remote sources.
+   *
+   * Test conditions:
+   *  - Source: remote and local files
+   *  - Import overwrite: all existing translations
+   *  - Translation directory: available
+   */
+  function testUpdateImportSourceRemote() {
+    $config = config('locale.settings');
+
+    // Build the test environment.
+    $this->setTranslationFiles();
+    $this-> setCurrentTranslations();
+    $config->set('translation.default_filename', '%project-%version.%language._po');
+
+    // Set the update conditions for this test.
+    $edit = array(
+      'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
+      'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    // Execute the translation update.
+    $this->drupalGet('admin/reports/translations/check');
+    $this->drupalPost('admin/reports/translations', array(), t('Update'));
+
+    // Check if the translation has been updated, using the status cache.
+    $status = state()->get('locale.translation_status');
+    $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found');
+    $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found');
+    $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found');
+
+    // Check the new translation status.
+    // The static cache needs to be flushed first to get the most recent data
+    // from the database. The function was called earlier during this test.
+    drupal_static_reset('locale_translation_get_file_history');
+    $history = locale_translation_get_file_history();
+    $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_now, 'Translation of contrib_module_one is imported');
+    $this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_one is updated');
+    $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported');
+    $this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated');
+    $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported');
+    $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated');
+
+    // Check whether existing translations have (not) been overwritten.
+    $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January');
+    $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
+    $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
+    $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
+    $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
+    $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
+    $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
+  }
+
+  /**
+   * Tests translation import from local sources.
+   *
+   * Test conditions:
+   *  - Source: local files only
+   *  - Import overwrite: all existing translations
+   *  - Translation directory: available
+   */
+  function testUpdateImportSourceLocal() {
+    $config = config('locale.settings');
+
+    // Build the test environment.
+    $this->setTranslationFiles();
+    $this-> setCurrentTranslations();
+    $config->set('translation.default_filename', '%project-%version.%language._po');
+
+    // Set the update conditions for this test.
+    $edit = array(
+      'use_source' => LOCALE_TRANSLATION_USE_SOURCE_LOCAL,
+      'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    // Execute the translation update.
+    $this->drupalGet('admin/reports/translations/check');
+    $this->drupalPost('admin/reports/translations', array(), t('Update'));
+
+    // Check if the translation has been updated, using the status cache.
+    $status = state()->get('locale.translation_status');
+    $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found');
+    $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found');
+    $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found');
+
+    // Check the new translation status.
+    // The static cache needs to be flushed first to get the most recent data
+    // from the database. The function was called earlier during this test.
+    drupal_static_reset('locale_translation_get_file_history');
+    $history = locale_translation_get_file_history();
+    $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_medium, 'Translation of contrib_module_one is imported');
+    $this->assertEqual($history['contrib_module_one']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_one is updated');
+    $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported');
+    $this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated');
+    $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported');
+    $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated');
+
+    // Check whether existing translations have (not) been overwritten.
+    $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
+    $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
+    $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
+    $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
+    $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
+    $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
+    $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
+  }
+
+  /**
+   * Tests translation import without a translations directory.
+   *
+   * Test conditions:
+   *  - Source: remote and local files
+   *  - Import overwrite: all existing translations
+   *  - Translation directory: not available
+   */
+  function testUpdateImportWithoutDirectory() {
+    $config = config('locale.settings');
+
+    // Build the test environment.
+    $this->setTranslationFiles();
+    $this-> setCurrentTranslations();
+    $config->set('translation.default_filename', '%project-%version.%language._po');
+
+    // Set the update conditions for this test.
+    $this->setTranslationsDirectory('');
+    $edit = array(
+      'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
+      'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    // Execute the translation update.
+    $this->drupalGet('admin/reports/translations/check');
+    $this->drupalPost('admin/reports/translations', array(), t('Update'));
+
+    // Check if the translation has been updated, using the status cache.
+    $status = state()->get('locale.translation_status');
+    $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found');
+    $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found');
+    $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found');
+
+    // Check the new translation status.
+    // The static cache needs to be flushed first to get the most recent data
+    // from the database. The function was called earlier during this test.
+    drupal_static_reset('locale_translation_get_file_history');
+    $history = locale_translation_get_file_history();
+    $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_now, 'Translation of contrib_module_one is imported');
+    $this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_one is updated');
+    $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_two is imported');
+    $this->assertEqual($history['contrib_module_two']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_two is updated');
+    $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported');
+    $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated');
+
+    // Check whether existing translations have (not) been overwritten.
+    $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January');
+    $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_1', 'Translation of February');
+    $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_1', 'Translation of March');
+    $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
+    $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
+    $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
+  }
+
+  /**
+   * Tests translation import with a translations directory and only overwrite
+   * non-customized translations.
+   *
+   * Test conditions:
+   *  - Source: remote and local files
+   *  - Import overwrite: only overwrite non-customized translations
+   *  - Translation directory: available
+   */
+  function testUpdateImportModeNonCustomized() {
+    $config = config('locale.settings');
+
+    // Build the test environment.
+    $this->setTranslationFiles();
+    $this-> setCurrentTranslations();
+    $config->set('translation.default_filename', '%project-%version.%language._po');
+
+    // Set the test conditions.
+    $edit = array(
+      'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
+      'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    // Execute translation update.
+    $this->drupalGet('admin/reports/translations/check');
+    $this->drupalPost('admin/reports/translations', array(), t('Update'));
+
+    // Check whether existing translations have (not) been overwritten.
+    $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
+    $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_customized', 'Translation of February');
+    $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
+    $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
+    $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
+    $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
+    $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
+  }
+
+  /**
+   * Tests translation import with a translations directory and don't overwrite
+   * any translation.
+   *
+   * Test conditions:
+   *  - Source: remote and local files
+   *  - Import overwrite: don't overwrite any existing translation
+   *  - Translation directory: available
+   */
+  function testUpdateImportModeNone() {
+    $config = config('locale.settings');
+
+    // Build the test environment.
+    $this->setTranslationFiles();
+    $this-> setCurrentTranslations();
+    $config->set('translation.default_filename', '%project-%version.%language._po');
+
+    // Set the test conditions.
+    $edit = array(
+      'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
+      'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NONE,
+    );
+    $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration'));
+
+    // Execute translation update.
+    $this->drupalGet('admin/reports/translations/check');
+    $this->drupalPost('admin/reports/translations', array(), t('Update'));
+
+    // Check whether existing translations have (not) been overwritten.
+    $this->assertTranslation('January', 'Januar_customized', 'de');
+    $this->assertTranslation('February', 'Februar_customized', 'de');
+    $this->assertTranslation('March', 'Marz', 'de');
+    $this->assertTranslation('April', 'April_2', 'de');
+    $this->assertTranslation('May', 'Mai_customized', 'de');
+    $this->assertTranslation('June', 'Juni', 'de');
+    $this->assertTranslation('Monday', 'Montag', 'de');
+  }
+
+  /**
+   * Tests automatic translation import when a module is enabled.
+   */
+  function testEnableDisableModule() {
+    // Make the hidden test modules look like a normal custom module.
+    state()->set('locale.test_system_info_alter', TRUE);
+
+    // Check if there is no translation yet.
+    $this->assertTranslation('Tuesday', '', 'de');
+
+    // Enable a module.
+    $edit = array(
+      'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate',
+    );
+    $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+
+    // Check if translations have been imported.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+      array('%number' => 7, '%update' => 0, '%delete' => 0)), 'One translation file imported.');
+    $this->assertTranslation('Tuesday', 'Dienstag', 'de');
+
+    // Disable and uninstall a module.
+    $edit = array(
+      'modules[Testing][locale_test_translate][enable]' => FALSE,
+    );
+    $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+    $edit = array(
+      'uninstall[locale_test_translate]' => 1,
+    );
+    $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall'));
+    $this->drupalPost(NULL, array(), t('Uninstall'));
+
+    // Check if the file data is removed from the database.
+    $history = locale_translation_get_file_history();
+    $this->assertFalse(isset($history['locale_test_translate']), 'Project removed from the file history');
+    $projects = locale_translation_get_projects();
+    $this->assertFalse(isset($projects['locale_test_translate']), 'Project removed from the project list');
+  }
+
+  /**
+   * Tests automatic translation import when a langauge is enabled.
+   *
+   * When a language is added, the system will check for translations files of
+   * enabled modules and will import them. When a language is removed the system
+   * will remove all translations of that langugue from the database.
+   */
+  function testEnableDisableLanguage() {
+    // Make the hidden test modules look like a normal custom module.
+    state()->set('locale.test_system_info_alter', TRUE);
+
+    // Enable a module.
+    $edit = array(
+      'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate',
+    );
+    $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+
+    // Check if there is no Dutch translation yet.
+    $this->assertTranslation('Extraday', '', 'nl');
+    $this->assertTranslation('Tuesday', 'Dienstag', 'de');
+
+    // Add a language.
+    $edit = array(
+      'predefined_langcode' => 'nl',
+    );
+    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+
+    // Check if the right number of translations are added.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+      array('%number' => 8, '%update' => 0, '%delete' => 0)), 'One language added.');
+    $this->assertTranslation('Extraday', 'extra dag', 'nl');
+
+    // Check if the language data is added to the database.
+    $result = db_query("SELECT project FROM {locale_file} WHERE langcode='nl'")->fetchField();
+    $this->assertTrue((boolean) $result, 'Files removed from file history');
+
+    // Remove a language.
+    $this->drupalPost('admin/config/regional/language/delete/nl', array(), t('Delete'));
+
+    // Check if the language data is removed from the database.
+    $result = db_query("SELECT project FROM {locale_file} WHERE langcode='nl'")->fetchField();
+    $this->assertFalse($result, 'Files removed from file history');
+
+    // Check that the Dutch translation is gone.
+    $this->assertTranslation('Extraday', '', 'nl');
+    $this->assertTranslation('Tuesday', 'Dienstag', 'de');
+  }
+
+  /**
+   * Tests automatic translation import when a custom langauge is enabled.
+   */
+  function testEnableCustomLanguage() {
+    // Make the hidden test modules look like a normal custom module.
+    state()->set('locale.test_system_info_alter', TRUE);
+
+    // Enable a module.
+    $edit = array(
+      'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate',
+    );
+    $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+
+    // Create and enable a custom language with language code 'xx' and a random
+    // name.
+    $langcode = 'xx';
+    $name = $this->randomName(16);
+    $edit = array(
+      'predefined_langcode' => 'custom',
+      'langcode' => $langcode,
+      'name' => $name,
+      'direction' => '0',
+    );
+    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
+
+    // Ensure the translation file is automatically imported when the language
+    // was added.
+    $this->assertText(t('One translation file imported.'), t('Language file automatically imported.'));
+    $this->assertText(t('One translation string was skipped because of disallowed or malformed HTML'), t('Language file automatically imported.'));
+
+    // Ensure the strings were successfully imported.
+    $search = array(
+      'string' => 'lundi',
+      'langcode' => $langcode,
+      'translation' => 'translated',
+    );
+    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+    $this->assertNoText(t('No strings available.'), t('String successfully imported.'));
+
+    // Ensure the multiline string was imported.
+    $search = array(
+      'string' => 'Source string for multiline translation',
+      'langcode' => $langcode,
+      'translation' => 'all',
+    );
+    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+    $this->assertText('Multiline translation string to make sure that import works with it.', t('String successfully imported.'));
+
+    // Ensure 'Allowed HTML source string' was imported but the translation for
+    // 'Another allowed HTML source string' was not because it contains invalid
+    // HTML.
+    $search = array(
+      'string' => 'HTML source string',
+      'langcode' => $langcode,
+      'translation' => 'all',
+    );
+    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+    $this->assertText('Allowed HTML source string', t('String successfully imported.'));
+    $this->assertNoText('Another allowed HTML source string', t('String with disallowed translation not imported.'));
+  }
+}
diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc
index a287e16..41679d9 100644
--- a/core/modules/locale/locale.batch.inc
+++ b/core/modules/locale/locale.batch.inc
@@ -6,40 +6,11 @@
  */
 
 /**
- * Build a batch to get the status of remote and local translation files.
- *
- * The batch process fetches the state of both remote and (if configured) local
- * translation files. The data of the most recent translation is stored per
- * per project and per language. This data is stored in a state variable
- * 'locale_translation_status'. The timestamp it was last updated is stored
- * in the state variable 'locale_translation_status_last_update'.
- *
- * @param array $sources
- *   Array of translation source objects for which to check the state of
- *   translation source files.
+ * Load the common translation API.
  */
-function locale_translation_batch_status_build($sources) {
-  $operations = array();
-
-  // Set the batch processes for remote sources.
-  foreach ($sources as $source) {
-    $operations[] = array('locale_translation_batch_status_fetch_remote', array($source));
-  }
-
-  // Check for local sources, compare the results of local and remote and store
-  // the most recent.
-  $operations[] = array('locale_translation_batch_status_fetch_local', array($sources));
-  $operations[] = array('locale_translation_batch_status_compare', array());
-
-  $batch = array(
-    'operations' => $operations,
-    'title' => t('Checking available translations'),
-    'finished' => 'locale_translation_batch_status_finished',
-    'error_message' => t('Error checking available interface translation updates.'),
-    'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
-  );
-  return $batch;
-}
+// @todo Combine functions differently in files to avoid unnecessary includes.
+// Follow-up issue http://drupal.org/node/1834298
+require_once DRUPAL_ROOT . '/core/modules/locale/locale.translation.inc';
 
 /**
  * Batch operation callback: Check the availability of a remote po file.
@@ -58,20 +29,32 @@ function locale_translation_batch_status_build($sources) {
  * @see locale_translation_batch_status_compare()
 */
 function locale_translation_batch_status_fetch_remote($source, &$context) {
+  $t = get_t();
   // Check the translation file at the remote server and update the source
   // data with the remote status.
-  if (isset($source->files['remote'])) {
-    $remote_file = $source->files['remote'];
-    $result = locale_translation_http_check($remote_file->url);
-
-    // Update the file object with the result data. In case of a redirect we
-    // store the resulting url.
-    if ($result && !empty($result->updated)) {
-      $remote_file->url = isset($result->redirect_url) ? $result->redirect_url : $remote_file->url;
-      $remote_file->timestamp = $result->updated;
-      $source->files['remote'] = $remote_file;
+  if (isset($source->files[LOCALE_TRANSLATION_REMOTE])) {
+    $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE];
+    $result = locale_translation_http_check($remote_file->uri);
+
+    if ($result) {
+      // Update the file object with the result data. In case of a redirect we
+      // store the resulting uri. If a file is not found we don't update the
+      // file object, and store it unchanged.
+      if (isset($result->updated)) {
+        $remote_file->uri = isset($result->redirect_uri) ? $result->redirect_uri : $remote_file->uri;
+        $remote_file->timestamp = $result->updated;
+        $source->files[LOCALE_TRANSLATION_REMOTE] = $remote_file;
+      }
+      // Record success.
+      $context['results']['files'][$source->name] = $source->name;
+    }
+    else {
+      // An error occured when checking the file. Record the failure for
+      // reporting at the end of the batch.
+      $context['results']['failed_files'][] = $source->name;
     }
-    $context['results'][$source->name][$source->language] = $source;
+    $context['results']['sources'][$source->name][$source->langcode] = $source;
+    $context['message'] = $t('Checked translation file: %name.', array('%name' => $source->name));
   }
 }
 
@@ -92,22 +75,25 @@ function locale_translation_batch_status_fetch_remote($source, &$context) {
  * @see locale_translation_batch_status_compare()
  */
 function locale_translation_batch_status_fetch_local($sources, &$context) {
-  module_load_include('compare.inc', 'locale');
-
+  $t = get_t();
   // Get the status of local translation files and store the result data in the
   // batch results for later processing.
   foreach ($sources as $source) {
-    if (isset($source->files['local'])) {
+    if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
       locale_translation_source_check_file($source);
 
       // If remote data was collected before, we merge it into the newly
       // collected result.
-      if (isset($context['results'][$source->name][$source->language])) {
-        $source->files['remote'] = $context['results'][$source->name][$source->language]->files['remote'];
+      if (isset($context['results']['sources'][$source->name][$source->langcode])) {
+        $source->files[LOCALE_TRANSLATION_REMOTE] = $context['results']['sources'][$source->name][$source->langcode]->files[LOCALE_TRANSLATION_REMOTE];
       }
-      $context['results'][$source->name][$source->language] = $source;
+
+      // Record success and store the updated source data.
+      $context['results']['files'][$source->name] = $source->name;
+      $context['results']['sources'][$source->name][$source->langcode] = $source;
     }
   }
+  $context['message'] = $t('Checked translation files.');
 }
 
 /**
@@ -116,7 +102,7 @@ function locale_translation_batch_status_fetch_local($sources, &$context) {
  * In the preceding batch processes data of remote and local translation sources
  * is collected. Here we compare the collected results and update the source
  * object with the data of the most recent translation file. The end result is
- * stored in the 'locale_translation_status' state variable. Other
+ * stored in the 'locale.translation_status' state variable. Other
  * processes can collect this data after the batch process is completed.
  *
  * @param array $context
@@ -127,27 +113,51 @@ function locale_translation_batch_status_fetch_local($sources, &$context) {
  * @see locale_translation_batch_status_fetch_local()
  */
 function locale_translation_batch_status_compare(&$context) {
-  module_load_include('compare.inc', 'locale');
+  $history = locale_translation_get_file_history();
   $results = array();
 
-  foreach ($context['results'] as $project => $langcodes) {
-    foreach ($langcodes as $langcode => $source) {
-      $local = isset($source->files['local']) ? $source->files['local'] : NULL;
-      $remote = isset($source->files['remote']) ? $source->files['remote'] : NULL;
-
-      // The available translation files are compare and data of the most recent
-      // file is used to update the source object.
-      $file = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local;
-      if (isset($file->timestamp)) {
-        $source->type = $file->type;
-        $source->timestamp = $file->timestamp;
+  if (isset($context['results']['sources'])) {
+    foreach ($context['results']['sources'] as $project => $langcodes) {
+      foreach ($langcodes as $langcode => $source) {
+        $local = isset($source->files[LOCALE_TRANSLATION_LOCAL]) ? $source->files[LOCALE_TRANSLATION_LOCAL] : NULL;
+        $remote = isset($source->files[LOCALE_TRANSLATION_REMOTE]) ? $source->files[LOCALE_TRANSLATION_REMOTE] : NULL;
+
+        // The available translation files are compared and data of the most
+        // recent file is used to update the source object.
+        $file = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local;
+        if (isset($file->timestamp)) {
+          $source->type = $file->type;
+          $source->timestamp = $file->timestamp;
+        }
+
+        // Compare the available translation with the current translations
+        // status. If the project/language was translated before and it is more
+        // recent than the most recent translation, the translation is up to
+        // date. Which is marked in the source object with type "current".
+        if (isset($history[$source->project][$source->langcode])) {
+          $current = $history[$source->project][$source->langcode];
+          // Add the current translation to the source object to save it in
+          // the status cache.
+          $source->files[LOCALE_TRANSLATION_CURRENT] = $current;
+
+          if (isset($source->type)) {
+            $available = $source->files[$source->type];
+            $result = _locale_translation_source_compare($current, $available) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $available : $current;
+            $source->type = $result->type;
+            $source->timestamp = $result->timestamp;
+          }
+          else {
+            $source->type = $current->type;
+            $source->timestamp = $current->timestamp;
+          }
+        }
+
         $results[$project][$langcode] = $source;
       }
     }
   }
 
-  state()->set('locale_translation_status', $results);
-  state()->set('locale_translation_status_last_update', REQUEST_TIME);
+  locale_translation_status_save($results);
 }
 
 /**
@@ -160,14 +170,26 @@ function locale_translation_batch_status_compare(&$context) {
  */
 function locale_translation_batch_status_finished($success, $results) {
   $t = get_t();
-  if($success) {
-    if ($results) {
+  if ($success) {
+    if (isset($results['failed_files'])) {
+        if (module_exists('dblog')) {
+          $message = format_plural(count($results['failed_files']), 'One translation file could not be checked. <a href="@url">See the log</a> for details.', '@count translation files could not be checked. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
+        }
+        else {
+          $message = format_plural(count($results['failed_files']), 'One translation files could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.');
+        }
+        drupal_set_message($message, 'error');
+    }
+    if (isset($results['files'])) {
       drupal_set_message(format_plural(
-        count($results),
+        count($results['sources']),
         'Checked available interface translation updates for one project.',
         'Checked available interface translation updates for @count projects.'
       ));
     }
+    if (!isset($results['failed_files']) && !isset($results['files'])) {
+      drupal_set_message(t('Nothing to check.'));
+    }
   }
   else {
     drupal_set_message($t('An error occurred trying to check available interface translation updates.'), 'error');
@@ -175,20 +197,302 @@ function locale_translation_batch_status_finished($success, $results) {
 }
 
 /**
+ * Loads translation source data for the projects to be updated.
+ *
+ * Source data is loaded from cache and stored in the context results array.
+ * Source data contains the translations status per project / per language
+ * and whether translation updates are available and where the updates can be
+ * retrieved from. The data is stored in the $context['results'] parameter
+ * so that other batch operations can take this data as input for their
+ * operation.
+ *
+ * @see locale_translation_batch_fetch_download()
+ * @see locale_translation_batch_fetch_import()
+ * @see locale_translation_batch_fetch_update_status()
+ * @see locale_translation_batch_status_compare()
+ */
+function locale_translation_batch_fetch_sources($projects, $langcodes, &$context) {
+  $context['results']['input'] = locale_translation_load_sources($projects, $langcodes);
+
+  // If this batch operation is preceded by the status check operations, the
+  // results of those operation are stored in the context. We remove them here
+  // to keep the result records clean.
+  unset($context['results']['files']);
+  unset($context['results']['failed_files']);
+}
+
+/**
+ * Batch operation: Download a remote translation file.
+ *
+ * This operation downloads a remote gettext file and saves it in the temporary
+ * directory. The remote file URL is taken from the input data in
+ * $context['results']['input']. The result of the operation is stored in
+ * $context['results']['sources'] and contains the URL of the temporary file.
+ *
+ * @param object $project
+ *   Source object of the translatable project.
+ * @param string $langcode
+ *   Language code.
+ * @param $context
+ *   Batch context array.
+ *
+ * @see locale_translation_batch_fetch_sources()
+ * @see locale_translation_batch_fetch_import()
+ * @see locale_translation_batch_fetch_update_status()
+ * @see locale_translation_batch_status_compare()
+ */
+function locale_translation_batch_fetch_download($project, $langcode, &$context) {
+  $sources = $context['results']['input'];
+  if (isset($sources[$project . ':' . $langcode])) {
+    $source = $sources[$project . ':' . $langcode];
+    if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) {
+      $t = get_t();
+      if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE])) {
+        $context['message'] = $t('Imported translation file: %name.', array('%name' => $file->filename));
+        $source->files[LOCALE_TRANSLATION_DOWNLOADED] = $file;
+      }
+      else {
+        $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE];
+      }
+      $context['results']['sources'][$project][$langcode] = $source;
+    }
+  }
+}
+
+/**
+ * Batch process: Import translation file.
+ *
+ * This batch operation imports either a local gettext file or a downloaded
+ * remote gettext file. In case of a downloaded file the location of the
+ * temporary file is found in the $context['results']['sources']. The temporary
+ * file will be deleted after importing or will be moved to the local
+ * translations directory. In case of a local file the file will just be
+ * imported.
+ *
+ * @param object $project
+ *   Source object of the translatable project.
+ * @param string $langcode
+ *   Language code.
+ * @param array $options
+ *   Array of import options.
+ * @param $context
+ *   Batch context array.
+ *
+ * @see locale_translate_batch_import_files()
+ * @see locale_translation_batch_fetch_sources()
+ * @see locale_translation_batch_fetch_download()
+ * @see locale_translation_batch_fetch_update_status()
+ * @see locale_translation_batch_status_compare()
+ */
+function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) {
+  $sources = $context['results']['input'];
+  if (isset($sources[$project . ':' . $langcode])) {
+    $source = $sources[$project . ':' . $langcode];
+    if (isset($source->type)) {
+      if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) {
+
+        $t = get_t();
+        // If we are working on a remote file we will import the downloaded
+        // file. If the file was local just mark the result as such.
+        if ($source->type == LOCALE_TRANSLATION_REMOTE) {
+          if (isset($context['results']['sources'][$source->project][$source->langcode]->files[LOCALE_TRANSLATION_DOWNLOADED])) {
+            $import_type = LOCALE_TRANSLATION_DOWNLOADED;
+            $source_result = $context['results']['sources'][$source->project][$source->langcode];
+          }
+        }
+        else {
+          $import_type = LOCALE_TRANSLATION_LOCAL;
+          $source_result = $source;
+        }
+
+        $file = $source_result->files[$import_type];
+        module_load_include('bulk.inc', 'locale');
+        $options += array(
+          'message' => $t('Importing translation file: %name', array('%name' => $file->filename)),
+        );
+        // Import the translation file. For large files the batch operations is
+        // progressive and will be called repeatedly untill finished.
+        locale_translate_batch_import($file, $options, $context);
+
+        // The import is finished.
+        if (isset($context['finished']) && $context['finished'] == 1) {
+          // The import is successfull.
+          if (isset($context['results']['files'][$file->uri])) {
+            $context['message'] = $t('Imported translation file: %name.', array('%name' => $file->filename));
+
+            // Keep the data of imported source. In the following batch
+            // operation it will be saved in the {locale_file} table.
+            $source_result->files[LOCALE_TRANSLATION_IMPORTED] = $source_result->files[$source->type];
+
+            // Downloaded files are stored in the temporary files directory. If
+            // files should be kept locally, they will be moved to the local
+            // translations after successfull import. Otherwise the temporary
+            // file is deleted after being imported.
+            if ($import_type == LOCALE_TRANSLATION_DOWNLOADED && variable_get('locale_translate_file_directory', conf_path() . '/files/translations') && isset($source_result->files[LOCALE_TRANSLATION_LOCAL])) {
+              if (file_unmanaged_move($file->uri, $source_result->files[LOCALE_TRANSLATION_LOCAL]->uri, FILE_EXISTS_REPLACE)) {
+                // The downloaded file is now moved to the local file location.
+                // From this point forward we can treat it as if we imported a
+                // local file.
+                $import_type = LOCALE_TRANSLATION_LOCAL;
+              }
+            }
+            // The downloaded file is imported but will not be stored locally.
+            // Store the timestamp and delete the file.
+            if ($import_type == LOCALE_TRANSLATION_DOWNLOADED) {
+              $timestamp = filemtime($source_result->files[$import_type]->uri);
+              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->timestamp = $timestamp;
+              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->last_checked = REQUEST_TIME;
+              file_unmanaged_delete($file->uri);
+            }
+            // If the translation file is stored in the local directory. The
+            // timestamp of the file is stored.
+            if ($import_type == LOCALE_TRANSLATION_LOCAL) {
+              $timestamp = filemtime($source_result->files[$import_type]->uri);
+              $source_result->files[LOCALE_TRANSLATION_LOCAL]->timestamp = $timestamp;
+              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->timestamp = $timestamp;
+              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->last_checked = REQUEST_TIME;
+
+            }
+          }
+          else {
+            // File import failed. We can delete the temporary file.
+            if ($import_type == LOCALE_TRANSLATION_DOWNLOADED) {
+              file_unmanaged_delete($file->uri);
+            }
+          }
+        }
+        $context['results']['sources'][$source->project][$source->langcode] = $source_result;
+      }
+    }
+  }
+}
+
+/**
+ * Batch process: Update the download history table.
+ *
+ * This batch process updates the {local_file} table with the data of imported
+ * gettext files. Import data is taken from $context['results']['sources'].
+ *
+ * @param $context
+ *   Batch context array.
+ *
+ * @see locale_translation_batch_fetch_sources()
+ * @see locale_translation_batch_fetch_download()
+ * @see locale_translation_batch_fetch_import()
+ * @see locale_translation_batch_status_compare()
+ */
+function locale_translation_batch_fetch_update_status(&$context) {
+  $t = get_t();
+  $results = array();
+
+  if (isset($context['results']['sources'])) {
+    foreach ($context['results']['sources'] as $project => $langcodes) {
+      foreach ($langcodes as $langcode => $source) {
+
+        // Store the state of the imported translations in {locale_file} table.
+        // During the batch execution the data of the imported files is
+        // temporary stored in $context['results']['sources']. Now it will be
+        // stored in the database. Afterwards the temporary import and download
+        // data can be deleted.
+        if (isset($source->files[LOCALE_TRANSLATION_IMPORTED])) {
+          $file = $source->files[LOCALE_TRANSLATION_IMPORTED];
+          locale_translation_update_file_history($file);
+          unset($source->files[LOCALE_TRANSLATION_IMPORTED]);
+        }
+        unset($source->files[LOCALE_TRANSLATION_DOWNLOADED]);
+
+        // The source data is now up to date. Data of local and/or remote source
+        // file is up to date including an updated time stamp. In a next batch
+        // operation this can be used to update the translation status.
+        $context['results']['sources'][$project][$langcode] = $source;
+      }
+    }
+    $context['message'] = $t('Updated translations.');
+  }
+}
+
+/**
+ * Batch finished callback: Set result message.
+ *
+ * @param boolean $success
+ *   TRUE if batch succesfully completed.
+ * @param array
+ *   Batch results.
+ */
+function locale_translation_batch_fetch_finished($success, $results) {
+  module_load_include('bulk.inc', 'locale');
+  return locale_translate_batch_finished($success, $results);
+}
+
+/**
  * Check if remote file exists and when it was last updated.
  *
- * @param string $url
- *   URL of remote file.
+ * @param string $uri
+ *   URI of remote file.
  * @param array $headers
  *   HTTP request headers.
  * @return stdClass
  *   Result object containing the HTTP request headers, response code, headers,
  *   data, redirect status and updated timestamp.
+ *   TRUE if the file is not found.
+ *   FALSE if a fault occured.
+ */
+function locale_translation_http_check($uri, $headers = array()) {
+  $result = drupal_http_request($uri, array('headers' => $headers, 'method' => 'HEAD'));
+  if (!isset($result->error)) {
+    if ($result->code == 200) {
+      $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0;
+    }
+    return $result;
+  }
+  else {
+    switch ($result->code) {
+      case 404:
+        // File not found occurs when a translation file is not yet available
+        // at the translation server. But also if a custom module or custom
+        // theme does not define the location of a translation file. By default
+        // the file is checked at the translation server, but it will not be
+        // found there.
+        watchdog('locale', 'File not found: @uri.', array('@uri' => $uri));
+        return TRUE;
+      case 0:
+        watchdog('locale', 'Error occurred when trying to check @remote: @errormessage.', array('@errormessage' => $result->error, '@remote' => $uri), WATCHDOG_ERROR);
+        break;
+      default:
+        watchdog('locale', 'HTTP error @errorcode occurred when trying to check @remote.', array('@errorcode' => $result->code, '@remote' => $uri), WATCHDOG_ERROR);
+        break;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Downloads source file from a remote server.
+ *
+ * The downloaded file is stored in the temporary files directory.
+ *
+ * @param object $source_file
+ *   Source file object with at least:
+ *   - "uri": uri to download the file from.
+ *   - "project": Project name.
+ *   - "langcode": Translation language.
+ *   - "version": Project version.
+ *   - "filename": File name.
+ *
+ * @return object
+ *   File object if download was successful. FALSE on failure.
  */
-function locale_translation_http_check($url, $headers = array()) {
-  $result = drupal_http_request($url, array('headers' => $headers, 'method' => 'HEAD'));
-  if ($result && $result->code == '200') {
-    $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0;
+function locale_translation_download_source($source_file) {
+  if ($uri = system_retrieve_file($source_file->uri, 'temporary://')) {
+    $file = new stdClass();
+    $file->project = $source_file->project;
+    $file->langcode = $source_file->langcode;
+    $file->version = $source_file->version;
+    $file->type = LOCALE_TRANSLATION_DOWNLOADED;
+    $file->uri = $uri;
+    $file->filename = $source_file->filename;
+    return $file;
   }
-  return $result;
+  watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source->files[LOCALE_TRANSLATION_REMOTE]->uri), WATCHDOG_ERROR);
+  return FALSE;
 }
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index 0be2aec..385f38f 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -124,6 +124,7 @@ function locale_translate_import_form_submit($form, &$form_state) {
       'overwrite_options' => $form_state['values']['overwrite_options'],
       'customized' => $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED,
     );
+    $file = locale_translate_file_attach_properties($file, $options);
     $batch = locale_translate_batch_build(array($file->uri => $file), $options);
     batch_set($batch);
   }
@@ -265,112 +266,42 @@ function locale_translate_export_form_submit($form, &$form_state) {
 }
 
 /**
- * Sets a batch for a newly-added language.
+ * Get interface translation files present in the translations directory.
  *
- * @param array $options
- *   An array with options that can have the following elements:
- *   - 'langcode': The language code, required.
- *   - 'overwrite_options': Overwrite options array as defined in
- *     Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
- *   - 'customized': Flag indicating whether the strings imported from $file
- *     are customized translations or come from a community source. Use
- *     LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
- *     LOCALE_NOT_CUSTOMIZED.
- *   - 'finish_feedback': Whether or not to give feedback to the user when the
- *     batch is finished. Optional, defaults to TRUE.
- */
-function locale_translate_add_language_set_batch($options) {
-  $options += array(
-    'overwrite_options' => array(),
-    'customized' => LOCALE_NOT_CUSTOMIZED,
-    'finish_feedback' => TRUE,
-  );
-  // See if we have language files to import for the newly added language,
-  // collect and import them.
-  if ($batch = locale_translate_batch_import_files($options)) {
-    batch_set($batch);
-  }
-}
-
-/**
- * Prepare a batch to import all translations.
- *
- * @param array $options
- *   An array with options that can have the following elements:
- *   - 'langcode': The language code. Optional, defaults to NULL, which means
- *     that the language will be detected from the name of the files.
- *   - 'overwrite_options': Overwrite options array as defined in
- *     Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
- *   - 'customized': Flag indicating whether the strings imported from $file
- *     are customized translations or come from a community source. Use
- *     LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
- *     LOCALE_NOT_CUSTOMIZED.
- *   - 'finish_feedback': Whether or not to give feedback to the user when the
- *     batch is finished. Optional, defaults to TRUE.
- *
- * @param $force
- *   (optional) Import all available files, even if they were imported before.
+ * @param array $projects
+ *   Project names from which to get the translation files and history.
+ *   Defaults to all projects.
+ * @param array $langcodes
+ *   Language codes from which to  get the translation files and history.
+ *   Defaults to all languagues
  *
- * @todo
- *   Integrate with update status to identify projects needed and integrate
- *   l10n_update functionality to feed in translation files alike.
- *   See http://drupal.org/node/1191488.
+ * @return array
+ *   An array of interface translation files keyed by their URI.
  */
-function locale_translate_batch_import_files($options, $force = FALSE) {
-  $options += array(
-    'overwrite_options' => array(),
-    'customized' => LOCALE_NOT_CUSTOMIZED,
-    'finish_feedback' => TRUE,
-  );
+function locale_translate_get_interface_translation_files($projects = array(), $langcodes = array()) {
+  module_load_include('compare.inc', 'locale');
   $files = array();
-  if (!empty($options['langcode'])) {
-    $langcodes = array($options['langcode']);
-  }
-  else {
-    // If langcode was not provided, make sure to only import files for the
-    // languages we have enabled.
-    $langcodes = array_keys(language_list());
-  }
-  foreach ($langcodes as $langcode) {
-    $files = array_merge($files, locale_translate_get_interface_translation_files($langcode));
-  }
-  if (!$force) {
-    $result = db_select('locale_file', 'lf')
-      ->fields('lf', array('langcode', 'uri', 'timestamp'))
-      ->condition('langcode', $langcodes)
-      ->execute()
-      ->fetchAllAssoc('uri');
-    foreach ($result as $uri => $info) {
-      if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
-        // The file is already imported and not changed since the last import.
-        // Remove it from file list and don't import it again.
-        unset($files[$uri]);
+  $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
+  $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+
+  // Scan the translations directory for files matching a name pattern
+  // containing a project name and language code: {project}.{langcode}.po or
+  // {project}-{version}.{langcode}.po.
+  // Only files of known projects and languages will be returned.
+  if ($directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations')) {
+    $result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', array('recurse' => FALSE));
+
+    foreach ($result as $filepath => $file) {
+      // Update the file object with project name and version from the file name.
+      $file = locale_translate_file_attach_properties($file);
+      if (in_array($file->project, $projects)) {
+        if (in_array($file->langcode, $langcodes)) {
+          $files[$file->uri] = $file;
+        }
       }
     }
   }
-  return locale_translate_batch_build($files, $options);
-}
-
-/**
- * Get an array of available interface translation file.
- *
- * @param $langcode
- *   The langcode for the interface translation files. Pass NULL to get all
- *   available interface translation files.
- *
- * @return array
- *   An array of interface translation files.
- */
-function locale_translate_get_interface_translation_files($langcode = NULL) {
-  $directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations');
-  $return = file_scan_directory($directory, '!' . (!empty($langcode) ? '\.' . preg_quote($langcode, '!') : '') . '\.po$!', array('recurse' => FALSE));
-
-  foreach ($return as $filepath => $file) {
-    $file->uri = 'translations://' . $file->filename;
-    $return[$file->uri] = $file;
-    unset($return[$filepath]);
-  }
-  return $return;
+  return $files;
 }
 
 /**
@@ -406,8 +337,11 @@ function locale_translate_batch_build($files, $options) {
     $operations = array();
     foreach ($files as $file) {
       // We call locale_translate_batch_import for every batch operation.
-      $operations[] = array('locale_translate_batch_import', array($file->uri, $options));
+      $operations[] = array('locale_translate_batch_import', array($file, $options));
     }
+    // Save the translation status of all files.
+    $operations[] =  array('locale_translate_batch_import_save', array());
+
     $batch = array(
       'operations'    => $operations,
       'title'         => $t('Importing interface translations'),
@@ -426,42 +360,34 @@ function locale_translate_batch_build($files, $options) {
 /**
  * Perform interface translation import as a batch step.
  *
- * The given filepath is matched against ending with '{langcode}.po'. When
- * matched the filepath is added to batch context.
- *
- * @param $filepath
- *   Path to a file to import.
+ * @param object $file
+ *   A file object of the gettext file to be imported. The file object must
+ *   contain a language parameter (other than LANGUAGE_NOT_SPECIFIED). This
+ *   is used as the language of the import.
  *
  * @param array $options
  *   An array with options that can have the following elements:
- *   - 'langcode': The language code, required.
+ *   - 'langcode': The language code.
  *   - 'overwrite_options': Overwrite options array as defined in
  *     Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
  *   - 'customized': Flag indicating whether the strings imported from $file
  *     are customized translations or come from a community source. Use
  *     LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
  *     LOCALE_NOT_CUSTOMIZED.
+ *   - 'message': Alternative message to display during import.
  *
  * @param $context
  *   Contains a list of files imported.
  */
-function locale_translate_batch_import($filepath, $options, &$context) {
+function locale_translate_batch_import($file, $options, &$context) {
   // Merge the default values in the $options array.
   $options += array(
     'overwrite_options' => array(),
     'customized' => LOCALE_NOT_CUSTOMIZED,
   );
-  // The filename is either {langcode}.po or {prefix}.{langcode}.po, so
-  // we can extract the language code to use for the import from the end.
-  if (isset($options['langcode']) && $options['langcode'] ||
-      preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $matches)) {
-    $basename = drupal_basename($filepath);
-    $file = entity_create('file', array('filename' => $basename, 'uri' => 'translations://'. $basename));
-    // We need only the last match, but only if the langcode is not explicitly
-    // specified in the $options array.
-    if (!$options['langcode'] && is_array($matches)) {
-      $options['langcode'] = array_pop($matches);
-    }
+
+  if (isset($file->langcode) && $file->langcode != LANGUAGE_NOT_SPECIFIED) {
+
     try {
       if (empty($context['sandbox'])) {
         $context['sandbox']['parse_state'] = array(
@@ -477,41 +403,75 @@ function locale_translate_batch_import($filepath, $options, &$context) {
       // If not yet finished with reading, mark progress based on size and
       // position.
       if ($report['seek'] < filesize($file->uri)) {
+
         $context['sandbox']['parse_state']['seek'] = $report['seek'];
         // Maximize the progress bar at 95% before completion, the batch API
         // could trigger the end of the operation before file reading is done,
         // because of floating point inaccuracies. See
         // http://drupal.org/node/1089472
         $context['finished'] = min(0.95, $report['seek'] / filesize($file->uri));
-        $context['message'] = t('Importing file: %filename (@percent%)', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)));
+        if (isset($options['message'])) {
+          $context['message'] = t('@message (@percent%).', array('@message' => $options['message'], '@percent' => (int) ($context['finished'] * 100)));
+        }
+        else {
+          $context['message'] = t('Importing translation file: %filename (@percent%).', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)));
+        }
       }
       else {
         // We are finished here.
         $context['finished'] = 1;
-        $file->langcode = $options['langcode'];
+
+        // Store the file data for processing by the next batch operation.
         $file->timestamp = filemtime($file->uri);
-        locale_translate_update_file_history($file);
-        $context['results']['files'][$filepath] = $filepath;
-        $context['results']['languages'][$filepath] = $file->langcode;
+        $context['results']['files'][$file->uri] = $file;
+        $context['results']['languages'][$file->uri] = $file->langcode;
       }
-      // Add the values from the report to the stats for this file.
-      if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$filepath])) {
-        $context['results']['stats'][$filepath] = array();
+
+      // Add the reported values to the statistics for this file.
+      // Each import iteration reports statistics in an array. The results of
+      // each iteration are added and merged here and stored per file.
+      if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) {
+        $context['results']['stats'][$file->uri] = array();
       }
       foreach ($report as $key => $value) {
-        if (is_numeric($value)) {
-          $context['results']['stats'][$filepath] += array($key => 0);
-          $context['results']['stats'][$filepath][$key] += $value;
+        if (is_numeric($report[$key])) {
+          if (!isset($context['results']['stats'][$file->uri][$key])) {
+            $context['results']['stats'][$file->uri][$key] = 0;
+          }
+          $context['results']['stats'][$file->uri][$key] += $report[$key];
         }
         elseif (is_array($value)) {
-          $context['results']['stats'][$filepath] += array($key => array());
-          $context['results']['stats'][$filepath][$key] = array_merge($context['results']['stats'][$filepath][$key], $value);
+          $context['results']['stats'][$file->uri] += array($key => array());
+          $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value);
         }
       }
     }
     catch (Exception $exception) {
-      $context['results']['files'][$filepath] = $filepath;
-      $context['results']['failed_files'][$filepath] = $filepath;
+      // Import failed. Store the data of the failing file.
+      $context['results']['failed_files'][] = $file;
+      watchdog('locale', 'Unable to import translations file: @file', array('@file' => $file->uri));
+    }
+  }
+}
+
+/**
+ * Batch callback: Save data of imported files.
+ *
+ * @param $context
+ *   Contains a list of imported files.
+ */
+function locale_translate_batch_import_save($context) {
+  if (isset($context['results']['files'])) {
+    foreach ($context['results']['files'] as $file) {
+      // Update the file history if both project and version are known. This
+      // table is used by the automated translation update function which tracks
+      // translation status of module and themes in the system. Other
+      // translation files are not tracked and are therefore not stored in this
+      // table.
+      if ($file->project && $file->version) {
+        $file->last_checked = REQUEST_TIME;
+        locale_translation_update_file_history($file);
+      }
     }
   }
 }
@@ -523,41 +483,56 @@ function locale_translate_batch_finished($success, $results) {
   if ($success) {
     $additions = $updates = $deletes = $skips = 0;
     $strings = $langcodes = array();
-    drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.'));
-    $skipped_files = array();
-    // If there are no results and/or no stats (eg. coping with an empty .po
-    // file), simply do nothing.
-    if ($results && isset($results['stats'])) {
-      foreach ($results['stats'] as $filepath => $report) {
-        $additions += $report['additions'];
-        $updates += $report['updates'];
-        $deletes += $report['deletes'];
-        $skips += $report['skips'];
-        if ($report['skips'] > 0) {
-          $skipped_files[] = $filepath;
+    if (isset($results['failed_files'])) {
+        if (module_exists('dblog')) {
+          $message = format_plural(count($results['failed_files']), 'One translation file could not be imported. <a href="@url">See the log</a> for details.', '@count translation files could not be imported. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
         }
-        $strings = array_merge($strings, $report['strings']);
-      }
-      // Get list of unique string identifiers and language codes updated.
-      $strings = array_unique($strings);
-      $langcodes = array_unique(array_values($results['languages']));
+        else {
+          $message = format_plural(count($results['failed_files']), 'One translation files could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.');
+        }
+        drupal_set_message($message, 'error');
     }
-    drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
-    watchdog('locale', 'The translation was succesfully imported. %number new strings added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
-    if ($skips) {
-      if (module_exists('dblog')) {
-        $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
+    if (isset($results['files'])) {
+      $skipped_files = array();
+      // If there are no results and/or no stats (eg. coping with an empty .po
+      // file), simply do nothing.
+      if ($results && isset($results['stats'])) {
+        foreach ($results['stats'] as $filepath => $report) {
+          $additions += $report['additions'];
+          $updates += $report['updates'];
+          $deletes += $report['deletes'];
+          $skips += $report['skips'];
+          if ($report['skips'] > 0) {
+            $skipped_files[] = $filepath;
+          }
+          $strings = array_merge($strings, $report['strings']);
+        }
+        // Get list of unique string identifiers and language codes updated.
+        $strings = array_unique($strings);
+        $langcodes = array_unique(array_values($results['languages']));
       }
-      else {
-        $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
+      drupal_set_message(format_plural(count($results['files']),
+        'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+        '@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.',
+        array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)
+      ));
+      watchdog('locale', 'Translations imported: %number added, %update updated, %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
+
+      if ($skips) {
+        if (module_exists('dblog')) {
+          $message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
+        }
+        else {
+          $message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
+        }
+        drupal_set_message($message, 'warning');
+        watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING);
       }
-      drupal_set_message($skip_message, 'error');
-      watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING);
-    }
 
-    if ($strings) {
-      // Clear cache and force refresh of JavaScript translations.
-      _locale_refresh_translations($langcodes, $strings);
+      if ($strings) {
+        // Clear cache and force refresh of JavaScript translations.
+        _locale_refresh_translations($langcodes, $strings);
+      }
     }
   }
 }
@@ -580,50 +555,77 @@ function locale_translate_file_create($filepath) {
 }
 
 /**
- * Update the {locale_file} table.
+ * Generate file properties from filename and options.
  *
- * @param $file
- *   Object representing the file just imported.
+ * An attempt is made to determine the translation language, project name and
+ * project version from the file name. Supported file name patterns are:
+ * {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po.
+ * Alternatively the translation language can be set using the $options.
  *
- * @return integer
- *   FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
+ * @param object $file
+ *   A file object of the gettext file to be imported.
+ * @param array $options
+ *   An array with options:
+ *   - 'langcode': The language code. Overrides the file language.
  *
- * @see drupal_write_record()
+ * @return object
+ *   Modified file object.
  */
-function locale_translate_update_file_history($file) {
-  // Update or write new record.
-  if (db_query("SELECT uri FROM {locale_file} WHERE uri = :uri AND langcode = :langcode", array(':uri' => $file->uri, ':langcode' => $file->langcode))->fetchField()) {
-    $update = array('uri', 'langcode');
+function locale_translate_file_attach_properties($file, $options = array()) {
+  // Extract project, verion and language code from the file name. Supported:
+  // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po
+  preg_match('!
+    (                       # project OR project and version OR emtpy (group 1)
+      ([a-z_]+)             # project name (group 2)
+      \.                    # .
+      |                     # OR
+      ([a-z_]+)             # project name (group 3)
+      \-                    # -
+      ([0-9a-z\.\-\+]+)     # version (group 4)
+      \.                    # .
+      |                     # OR
+    )                       # (empty)
+    ([^\./]+)               # language code (group 5)
+    \.                      # .
+    po                      # po extension
+    $!x', $file->filename, $matches);
+  if (isset($matches[5])) {
+    $file->project = $matches[2] . $matches[3];
+    $file->version = $matches[4];
+    $file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5];
   }
   else {
-    $update = array();
+    $file->langcode = LANGUAGE_NOT_SPECIFIED;
   }
-  return drupal_write_record('locale_file', $file, $update);
+  return $file;
 }
 
 /**
- * Deletes all interface translation files depending on the langcode.
+ * Deletes interface translation files and translation history records.
+ *
+ * @param array $projects
+ *   Project names from which to delete the translation files and history.
+ *   Defaults to all projects.
+ * @param array $langcodes
+ *   Language codes from which to delete the translation files and history.
+ *   Defaults to all languagues
  *
- * @param $langcode
- *   A langcode or NULL. Pass NULL to delete all interface translation files.
+ * @return boolean
+ *   TRUE if files are removed sucessfully. FALSE if one or more files could
+ *   not be deleted.
  */
-function locale_translate_delete_translation_files($langcode) {
-  $files = locale_translate_get_interface_translation_files($langcode);
-  $return = TRUE;
-  if (!empty($files)) {
+function locale_translate_delete_translation_files($projects = array(), $langcodes = array()) {
+  $fail = FALSE;
+  locale_translation_file_history_delete($projects, $langcodes);
+
+  // Delete all translation files from the translations directory.
+  if ($files = locale_translate_get_interface_translation_files($projects, $langcodes)) {
     foreach ($files as $file) {
       $success = file_unmanaged_delete($file->uri);
       if (!$success) {
-        $return = FALSE;
-      }
-      else {
-        // Remove the registered translation file if any.
-        db_delete('locale_file')
-          ->condition('langcode', $langcode)
-          ->condition('uri', $file->uri)
-          ->execute();
+        $fail = TRUE;
       }
     }
   }
-  return $return;
+  return !$fail;
 }
diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc
index efe944b..dc998fb 100644
--- a/core/modules/locale/locale.compare.inc
+++ b/core/modules/locale/locale.compare.inc
@@ -5,86 +5,21 @@
  * The API for comparing project translation status with available translation.
  */
 
-/**
- * Threshold for timestamp comparison.
- *
- * Eliminates a difference between the download time and the actual .po file
- * timestamp in seconds. The download time is stored in the database in
- * {locale_file}.timestamp.
- */
-const LOCALE_TRANSLATION_TIMESTAMP_THRESHOLD = 2;
-
-/**
- * Comparison result of source files timestamps.
- *
- * Timestamp of source 1 is less than the timestamp of source 2.
- * @see _locale_translation_source_compare()
- */
-const LOCALE_TRANSLATION_SOURCE_COMPARE_LT = -1;
-
-/**
- * Comparison result of source files timestamps.
- *
- * Timestamp of source 1 is equal to the timestamp of source 2.
- * @see _locale_translation_source_compare()
- */
-const LOCALE_TRANSLATION_SOURCE_COMPARE_EQ = 0;
-
-/**
- * Comparison result of source files timestamps.
- *
- * Timestamp of source 1 is greater than the timestamp of source 2.
- * @see _locale_translation_source_compare()
- */
-const LOCALE_TRANSLATION_SOURCE_COMPARE_GT = 1;
-
 use Drupal\Core\Cache;
 
 /**
- * Get array of projects which are available for interface translation.
- *
- * This project data contains all projects which will be checked for available
- * interface translations.
- *
- * For full functionality this function depends on Update module.
- * When Update module is enabled the project data will contain the most recent
- * module status; both in enabled status as in version. When Update module is
- * disabled this function will return the last known module state. The status
- * will only be updated once Update module is enabled.
- *
- * @see locale_translation_build_projects().
- *
- * @return array
- *   Array of project data for translation update. See
- *   locale_translation_build_projects() for details.
+ * Load the common translation API.
  */
-function locale_translation_get_projects() {
-  $projects = &drupal_static(__FUNCTION__, array());
-
-  if (empty($projects)) {
-    // Get project data from the database.
-    $result = db_query('SELECT name, project_type, core, version, server_pattern, status FROM {locale_project}');
-
-    // http://drupal.org/node/1777106 is a follow-up issue to make the check for
-    // possible out-of-date project information more robust.
-    if ($result->rowCount() == 0 && module_exists('update')) {
-      // At least the core project should be in the database, so we build the
-      // data if none are found.
-      locale_translation_build_projects();
-      $result = db_query('SELECT name, project_type, core, version, server_pattern, status FROM {locale_project}');
-    }
-
-    foreach ($result as $project) {
-      $projects[$project->name] = $project;
-    }
-  }
-  return $projects;
-}
+// @todo Combine functions differently in files to avoid unnecessary includes.
+// Follow-up issue http://drupal.org/node/1834298
+require_once DRUPAL_ROOT . '/core/modules/locale/locale.translation.inc';
 
 /**
  * Clear the project data table.
  */
 function locale_translation_flush_projects() {
+  // Followup issue: http://drupal.org/node/1842362
+  // Replace {locale_project} table by state() variable(s).
   db_truncate('locale_project')->execute();
 }
 
@@ -164,7 +99,7 @@ function locale_translation_build_projects() {
       'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
       // A project can provide the path and filename pattern to download the
       // gettext file. Use the default if not.
-      'server_pattern' => isset($data['info']['interface translation server pattern']) ? $data['info']['interface translation server pattern'] : $default_server['pattern'],
+      'server_pattern' => isset($data['info']['interface translation server pattern']) && $data['info']['interface translation server pattern'] ? $data['info']['interface translation server pattern'] : $default_server['pattern'],
       'status' => !empty($data['project_status']) ? 1 : 0,
     );
     $project = (object) $data;
@@ -182,6 +117,9 @@ function locale_translation_build_projects() {
         'status' => $project->status,
       ))
       ->execute();
+
+      // Invalidate the cache of translatable projects.
+      locale_translation_clear_cache_projects();
   }
   return $projects;
 }
@@ -260,12 +198,16 @@ function _locale_translation_prepare_project_list($data, $type) {
  *
  * @return array
  *   Array of server parameters:
- *   - "server_pattern": URL containing po file pattern.
+ *   - "server_pattern": URI containing po file pattern.
  */
 function locale_translation_default_translation_server() {
-  $config = config('locale.settings');
+  $pattern = config('locale.settings')->get('translation.default_server_pattern');
+  // An additional check is required here. During the upgrade process
+  // config()->get() returns NULL. We use the defined value as fallback.
+  $pattern  = $pattern ? $pattern : LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN;
+
   return array(
-    'pattern' => $config->get('translation.default_server_pattern'),
+    'pattern' => $pattern,
   );
 }
 
@@ -289,7 +231,7 @@ function locale_translation_build_server_pattern($project, $template) {
     '%project' => $project->name,
     '%version' => $project->version,
     '%core' => $project->core,
-    '%language' => isset($project->language) ? $project->language : '%language',
+    '%language' => isset($project->langcode) ? $project->langcode : '%language',
   );
   return strtr($template, $variables);
 }
@@ -298,16 +240,14 @@ function locale_translation_build_server_pattern($project, $template) {
  * Check for the latest release of project translations.
  *
  * @param array $projects
- *   Projects to check (objects).
+ *   Array of project names to check. Defaults to all translatable projects.
  * @param string $langcodes
- *   Array of language codes to check for. Leave empty to check all languages.
+ *   Array of language codes. Defaults to all translatable languages.
  *
  * @return array
  *   Available sources indexed by project and language.
  */
-function locale_translation_check_projects($projects, $langcodes = NULL) {
-  module_load_include('batch.inc', 'locale');
-
+function locale_translation_check_projects($projects = array(), $langcodes = array()) {
   if (config('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL) {
     // Retrieve the status of both remote and local translation sources by
     // using a batch process.
@@ -325,27 +265,81 @@ function locale_translation_check_projects($projects, $langcodes = NULL) {
  * A batch process is used to check for po files at remote locations and (when
  * configured) to check for po files in the local file system. The most recent
  * translation source states are stored in the state variable
- * 'locale_translation_status'.
+ * 'locale.translation_status'.
+ *
+ * @param array $projects
+ *   Array of project names to check. Defaults to all translatable projects.
+ * @param string $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
+ */
+function locale_translation_check_projects_batch($projects = array(), $langcodes = array()) {
+  // Build and set the batch process.
+  $batch = locale_translation_batch_status_build($projects, $langcodes);
+  batch_set($batch);
+}
+
+/**
+ * Builds a batch to get the status of remote and local translation files.
+ *
+ * The batch process fetches the state of both remote and (if configured) local
+ * translation files. The data of the most recent translation is stored per
+ * per project and per language. This data is stored in a state variable
+ * 'locale.translation_status'. The timestamp it was last updated is stored
+ * in the state variable 'locale.translation_last_checked'.
  *
- * @params array $projects
- *   Array of translatable projects.
- * @params array $langcodes
- *   Array of language codes to check for. Leave empty to check all languages.
+ * @param array $projects
+ *   Array of project names for which to check the state of translation files.
+ *   Defaults to all translatable projects.
+ * @param array $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
+ *
+ * @return array
+ *   Batch definition array.
  */
-function locale_translation_check_projects_batch($projects, $langcodes = NULL) {
+function locale_translation_batch_status_build($projects = array(), $langcodes = array()) {
+  $t = get_t();
+  $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
   $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
-  $sources = array();
-  foreach ($projects as $name => $project) {
-    foreach ($langcodes as $langcode) {
-      $source = locale_translation_source_build($project, $langcode);
-      $sources[] = $source;
-    }
+
+  $operations = _locale_translation_batch_status_operations($projects, $langcodes);
+
+  $batch = array(
+    'operations' => $operations,
+    'title' => $t('Checking translations'),
+    'finished' => 'locale_translation_batch_status_finished',
+    'error_message' => $t('Error checking translation updates.'),
+    'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
+  );
+  return $batch;
+}
+
+/**
+ * Helper function to construct batch operations checking remote translation
+ * status.
+ *
+ * @param array projects
+ *   Array of project names to be processed.
+ * @param array langcodes
+ *   Array of language codes.
+ *
+ * @return array
+ *   Array of batch operations.
+ */
+function _locale_translation_batch_status_operations($projects, $langcodes) {
+  $operations = array();
+
+  // Set the batch processes for remote sources.
+  $sources = locale_translation_build_sources($projects, $langcodes);
+  foreach ($sources as $source) {
+    $operations[] = array('locale_translation_batch_status_fetch_remote', array($source));
   }
 
-  // Build and set the batch process.
-  module_load_include('batch.inc', 'locale');
-  $batch = locale_translation_batch_status_build($sources);
-  batch_set($batch);
+  // Check for local sources, compare the results of local and remote and store
+  // the most recent.
+  $operations[] = array('locale_translation_batch_status_fetch_local', array($sources));
+  $operations[] = array('locale_translation_batch_status_compare', array());
+
+  return $operations;
 }
 
 /**
@@ -353,7 +347,7 @@ function locale_translation_check_projects_batch($projects, $langcodes = NULL) {
  *
  * Only po files in the local file system are checked. Any remote translation
  * sources will be ignored. Results are stored in the state variable
- * 'locale_translation_status'.
+ * 'locale.translation_status'.
  *
  * Projects may contain a server_pattern option containing a pattern of the
  * path to the po source files. If no server_pattern is defined the default
@@ -361,13 +355,16 @@ function locale_translation_check_projects_batch($projects, $langcodes = NULL) {
  * defined the specified location is checked. The server_pattern can be set in
  * the module's .info file or by using hook_locale_translation_projects_alter().
  *
- * @params array $projects
- *   Array of translatable projects.
- * @params array $langcodes
- *   Array of language codes to check for. Leave empty to check all languages.
+ * @param array $projects
+ *   Array of project names for which to check the state of translation files.
+ *   Defaults to all translatable projects.
+ * @param array $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
  */
-function locale_translation_check_projects_local($projects, $langcodes = NULL) {
+function locale_translation_check_projects_local($projects = array(), $langcodes = array()) {
+  $projects = locale_translation_get_projects($projects);
   $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+  $history = locale_translation_get_file_history();
   $results = array();
 
   // For each project and each language we check if a local po file is
@@ -379,186 +376,32 @@ function locale_translation_check_projects_local($projects, $langcodes = NULL) {
       if (locale_translation_source_check_file($source)) {
         $source->type = 'local';
         $source->timestamp = $source->files['local']->timestamp;
-        $results[$name][$langcode] = $source;
       }
-    }
-  }
-
-  state()->set('locale_translation_status', $results);
-  state()->set('locale_translation_status_last_update', REQUEST_TIME);
-}
 
-/**
- * Check whether a po file exists in the local filesystem.
- *
- * It will search in the directory set in the translation source. Which defaults
- * to the "translations://" stream wrapper path. The directory may contain any
- * valid stream wrapper.
- *
- * The "local" files property of the source object contains the definition of a
- * po file we are looking for. The file name defaults to
- * LOCALE_TRANSLATION_DEFAULT_FILENAME. Per project this value
- * can be overridden using the server_pattern directive in the module's .info
- * file or by using hook_locale_translation_projects_alter().
- *
- * @param stdClass $source
- *   Translation source object.
- *   @see locale_translation_source_build()
- *
- * @return stdClass
- *   File object (filename, basename, name) updated with data of the po file.
- *   On success the files property of the source object is updated.
- *   files['local']:
- *   - "uri": File name and path.
- *   - "timestamp": Last updated time of the po file.
- *   FALSE if the file is not found.
- */
-function locale_translation_source_check_file(&$source) {
-  if (isset($source->files['local'])) {
-    $directory = $source->files['local']->directory;
-    $filename = '/' . preg_quote($source->files['local']->filename) . '$/';
-
-    // If the directory contains a stream wrapper, it is converted to a real
-    // path. This is required for file_scan_directory() which can not handle
-    // stream wrappers.
-    if ($scheme = file_uri_scheme($directory)) {
-      $directory = str_replace($scheme . '://', drupal_realpath($scheme . '://'), $directory);
-    }
-
-    if ($files = file_scan_directory($directory, $filename, array('key' => 'name'))) {
-      $file = current($files);
-      $source->files['local']->uri = $file->uri;
-      $source->files['local']->timestamp = filemtime($file->uri);
-      return $file;
-    }
-  }
-  return FALSE;
-}
-
-/**
- * Build abstract translation source.
- *
- * @param stdClass $project
- *   Project object.
- * @param string $langcode
- *   Language code.
- * @param string $filename
- *   File name of translation file. May contains placeholders.
- *
- * @return object
- *   Source object:
- *   - "project": Project name.
- *   - "name": Project name (inherited from project).
- *   - "language": Language code.
- *   - "core": Core version (inherited from project).
- *   - "version": Project version (inherited from project).
- *   - "project_type": Project type (inherited from project).
- *   - "files": Array of file objects containing properties of local and remote
- *     translation files.
- *   Other processes can add the following properties:
- *   - "type": Most recent file type 'remote' or 'local'. Corresponding with
- *     a key of the "files" array.
- *   - "timestamp": Timestamp of the most recent translation file.
- *   The "files" array contains file objects with the following properties:
- *   - "uri": Local file path.
- *   - "url": Remote file URL for downloads.
- *   - "directory": Directory of the local po file.
- *   - "filename": File name.
- *   - "timestamp": Timestamp of the file.
- *   - "keep": TRUE to keep the downloaded file.
- */
-// @todo Move this file?
-function locale_translation_source_build($project, $langcode, $filename = NULL) {
-  // Create a source object with data of the project object.
-  $source = clone $project;
-  $source->project = $project->name;
-  $source->language = $langcode;
-
-  $filename = $filename ? $filename : config('locale.settings')->get('translation.default_filename');
-
-  // If the server_pattern contains a remote file path we will check for a
-  // remote file. The local version of this file will will only be checked is a
-  // translations directory has been defined. If the server_pattern is a local
-  // file path we will only check for a file in the local file system.
-  $files = array();
-  if (_locale_translation_file_is_remote($source->server_pattern)) {
-    $files['remote'] = (object) array(
-      'type' => 'remote',
-      'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
-      'url' => locale_translation_build_server_pattern($source, $source->server_pattern),
-    );
-    if (variable_get('locale_translate_file_directory', conf_path() . '/files/translations')) {
-      $files['local'] = (object) array(
-        'type' => 'local',
-        'directory' => 'translations://',
-        'filename' => locale_translation_build_server_pattern($source, $filename),
-      );
-    }
-  }
-  else {
-    $files['local'] = (object) array(
-      'type' => 'local',
-      'directory' => locale_translation_build_server_pattern($source, drupal_dirname($source->server_pattern)),
-      'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
-    );
-  }
-  $source->files = $files;
-
-  return $source;
-}
-
-/**
- * Determine if a file is a remote file.
- *
- * @param string $url
- *   The URL or URL pattern of the file.
- *
- * @return boolean
- *   TRUE if the $url is a remote file.
- */
-function _locale_translation_file_is_remote($url) {
-  $scheme = file_uri_scheme($url);
-  if ($scheme) {
-    return !drupal_realpath($scheme . '://');
-  }
-  return FALSE;
-}
+      // Compare the available translation with the current translations status.
+      // If the project/language was translated before and it is more recent
+      // than the most recent translation, the translation is up to date. Which
+      // is marked in the source object with type "current".
+      if (isset($history[$source->project][$source->langcode])) {
+        $current = $history[$source->project][$source->langcode];
+        // Add the current translation to the source object to save it in
+        // the status cache.
+        $source->files[LOCALE_TRANSLATION_CURRENT] = $current;
+
+        if (isset($source->type)) {
+          $available = $source->files[$source->type];
+          $result = _locale_translation_source_compare($current, $available) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $available : $current;
+          $source->type = $result->type;
+          $source->timestamp = $result->timestamp;
+        }
+        else {
+          $source->type = $current->type;
+          $source->timestamp = $current->timestamp;
+        }
+      }
 
-/**
- * Compare two update sources, looking for the newer one.
- *
- * The timestamp property of the source objects are used to determine which is
- * the newer one.
- *
- * @param stdClass $source1
- *   Source object of the first translation source.
- * @param stdClass $source2
- *   Source object of available update.
- *
- * @return integer
- *   - "LOCALE_TRANSLATION_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1
- *     is missing.
- *   - "LOCALE_TRANSLATION_SOURCE_COMPARE_EQ":  $source1 == $source2 OR both
- *     $source1 and $source2 are missing.
- *   - "LOCALE_TRANSLATION_SOURCE_COMPARE_GT":  $source1 > $source2 OR $source2
- *     is missing.
- */
-function _locale_translation_source_compare($source1, $source2) {
-  if (isset($source1->timestamp) && isset($source2->timestamp)) {
-    if (abs($source1->timestamp - $source2->timestamp) < LOCALE_TRANSLATION_TIMESTAMP_THRESHOLD) {
-      return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
+      $results[$name][$langcode] = $source;
     }
-    else {
-      return $source1->timestamp > $source2->timestamp ? LOCALE_TRANSLATION_SOURCE_COMPARE_GT : LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
-    }
-  }
-  elseif (isset($source1->timestamp) && !isset($source2->timestamp)) {
-    return LOCALE_TRANSLATION_SOURCE_COMPARE_GT;
-  }
-  elseif (!isset($source1->timestamp) && isset($source2->timestamp)) {
-    return LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
-  }
-  else {
-    return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
   }
+  locale_translation_status_save($results);
 }
diff --git a/core/modules/locale/locale.fetch.inc b/core/modules/locale/locale.fetch.inc
new file mode 100755
index 0000000..7542086
--- /dev/null
+++ b/core/modules/locale/locale.fetch.inc
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @file
+ * The API for download and import of translations from remote and local sources.
+ */
+
+/**
+ * Load the common translation API.
+ */
+// @todo Combine functions differently in files to avoid unnecessary includes.
+// Follow-up issue http://drupal.org/node/1834298
+require_once DRUPAL_ROOT . '/core/modules/locale/locale.translation.inc';
+
+/**
+ * Builds a batch to check, download and import project translations.
+ *
+ * @param array $projects
+ *   Array of project names for which to update the translations. Defaults to
+ *   all translatable projects.
+ * @param array $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
+ * @param array $options
+ *   Array of import options. See locale_translate_batch_import_files().
+ *
+ * @return array
+ *   Batch definition array.
+ */
+function locale_translation_batch_update_build($projects = array(), $langcodes = array(), $options = array()) {
+  module_load_include('compare.inc', 'locale');
+  $t = get_t();
+  $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
+  $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+
+  $operations = _locale_translation_batch_status_operations($projects, $langcodes);
+  $operations = array_merge($operations, _locale_translation_fetch_operations($projects, $langcodes, $options));
+
+  $batch = array(
+    'operations' => $operations,
+    'title' => $t('Updating translations'),
+    'init_message' => $t('Downloading and importing translation files.'),
+    'error_message' => $t('Error importing translation files'),
+    'finished' => 'locale_translation_batch_fetch_finished',
+    'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
+  );
+  return $batch;
+}
+
+/**
+ * Builds a batch to download and import project translations.
+ *
+ * @param array $projects
+ *   Array of project names for which to check the state of translation files.
+ *   Defaults to all translatable projects.
+ * @param array $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
+ * @param array $options
+ *   Array of import options. See locale_translate_batch_import_files().
+ *
+ * @return array
+ *   Batch definition array.
+ */
+function locale_translation_batch_fetch_build($projects = array(), $langcodes = array(), $options = array()) {
+  $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
+  $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+
+  $t = get_t();
+  $batch = array(
+    'operations' => _locale_translation_fetch_operations($projects, $langcodes, $options),
+    'title' => $t('Updating translations.'),
+    'init_message' => $t('Downloading and importing translation files.'),
+    'error_message' => $t('Error importing translation files'),
+    'finished' => 'locale_translation_batch_fetch_finished',
+    'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
+  );
+  return $batch;
+}
+
+/**
+ * Helper function to construct the batch operations to fetch translations.
+ *
+ * @param array $projects
+ *   Array of project names for which to check the state of translation files.
+ *   Defaults to all translatable projects.
+ * @param array $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
+ * @param array $options
+ *   Array of import options.
+ *
+ * @return array
+ *   Array of batch operations.
+ */
+function _locale_translation_fetch_operations($projects, $langcodes, $options) {
+  $operations = array();
+  $config = config('locale.settings');
+
+  $operations[] = array('locale_translation_batch_fetch_sources', array($projects, $langcodes));
+
+  foreach ($projects as $project) {
+    foreach ($langcodes as $langcode) {
+      $operations[] = array('locale_translation_batch_fetch_download', array($project, $langcode));
+      $operations[] = array('locale_translation_batch_fetch_import', array($project, $langcode, $options));
+    }
+  }
+
+  // Update and save the translation status.
+  $operations[] = array('locale_translation_batch_fetch_update_status', array());
+
+  // Update and save the source status. New translation files have been
+  // downloaded, so other sources will be newer. We update the status now.
+  $operations[] = array('locale_translation_batch_status_compare', array());
+
+  return $operations;
+}
diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install
index f01935a..3b26178 100644
--- a/core/modules/locale/locale.install
+++ b/core/modules/locale/locale.install
@@ -171,34 +171,55 @@ function locale_schema() {
   $schema['locale_file'] = array(
     'description' => 'File import status information for interface translation files.',
     'fields' => array(
+      'project' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'A unique short name to identify the project the file belongs to.',
+      ),
       'langcode' => array(
-        'description' => 'Reference to the {languages}.langcode for this translation.',
         'type' => 'varchar',
         'length' => '12',
         'not null' => TRUE,
+        'default' => '',
+        'description' => 'Language code of this translation. References {language}.langcode.',
       ),
       'filename' => array(
-        'description' => 'Filename for importing the file.',
         'type' => 'varchar',
         'length' => 255,
         'not null' => TRUE,
         'default' => '',
+        'description' => 'Filename of the imported file.',
+      ),
+      'version' => array(
+        'type' => 'varchar',
+        'length' => '128',
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Version tag of the imported file.',
       ),
       'uri' => array(
-        'description' => 'File system path for importing the file.',
         'type' => 'varchar',
         'length' => 255,
         'not null' => TRUE,
         'default' => '',
+        'description' => 'URI of the remote file, the resulting local file or the locally imported file.',
       ),
       'timestamp' => array(
-        'description' => 'Unix timestamp of the file itself from the point when it was last imported.',
         'type' => 'int',
         'not null' => FALSE,
         'default' => 0,
+        'description' => 'Unix timestamp of the imported file.',
+      ),
+      'last_checked' => array(
+        'type' => 'int',
+        'not null' => FALSE,
+        'default' => 0,
+        'description' => 'Unix timestamp of the last time this translation was confirmed to be the most recent release available.',
       ),
     ),
-    'primary key' => array('uri', 'langcode'),
+    'primary key' => array('project', 'langcode'),
   );
 
   $schema['locale_project'] = array(
@@ -687,7 +708,6 @@ function locale_update_8010() {
         'default' => '',
       ),
       'uri' => array(
-        'description' => 'File system path for importing the file.',
         'type' => 'varchar',
         'length' => 255,
         'not null' => TRUE,
@@ -788,6 +808,70 @@ function locale_update_8014() {
 }
 
 /**
+ * Build a new {locale_file} table.
+ */
+function locale_update_8015() {
+  // The existing table has a primary key on uri and langcode. The new key
+  // is on project and langcode. There is no project data in the existing table,
+  // and it may not be possible to generate this reliably. Therefore we drop
+  // the table and build it again.
+  db_drop_table('locale_file');
+
+  $table = array(
+    'description' => 'File import status information for interface translation files.',
+    'fields' => array(
+      'project' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'A unique short name to identify the project the file belongs to.',
+      ),
+      'langcode' => array(
+        'type' => 'varchar',
+        'length' => '12',
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Language code of this translation. References {language}.langcode.',
+      ),
+      'filename' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Filename of the imported file.',
+      ),
+      'version' => array(
+        'type' => 'varchar',
+        'length' => '128',
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Version tag of the imported file.',
+      ),
+      'uri' => array(
+        'description' => 'File system path for importing the file.',
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'timestamp' => array(
+        'type' => 'int',
+        'not null' => FALSE,
+        'default' => 0,
+        'description' => 'Unix timestamp of the imported file.',
+      ),
+      'last_checked' => array(
+        'type' => 'int',
+        'not null' => FALSE,
+        'default' => 0,
+        'description' => 'Unix timestamp of the last time this translation was confirmed to be the most recent release available.',
+      ),
+    ),
+    'primary key' => array('project', 'langcode'),
+  );
+  db_create_table('locale_file', $table);
+}
+
+/**
  * @} End of "addtogroup updates-7.x-to-8.x".
  * The next series of updates should start at 9000.
  */
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index 85ed5d1..8b7235c 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -94,13 +94,51 @@
 const LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN = 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po';
 
 /**
- * Default file name of translation files stored in the local file system.
- *
- * The file name containing placeholders which are also used by the server
- * pattern. See locale_translation_build_server_pattern() for supported
- * placeholders.
+ * The number of seconds that the translations status entry should be considered.
+ */
+const LOCALE_TRANSLATION_STATUS_TTL = 600;
+
+/**
+ * UI option for override of existing translations. Override any translation.
+ */
+const LOCALE_TRANSLATION_OVERWRITE_ALL = 'all';
+
+/**
+ * UI option for override of existing translations. Only override non-customized
+ * translations.
+ */
+const LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED = 'non_customized';
+
+/**
+ * UI option for override of existing translations. Don't override existing
+ * translations.
+ */
+const LOCALE_TRANSLATION_OVERWRITE_NONE = 'none';
+
+/**
+ * Translation source is a remote file.
+ */
+const LOCALE_TRANSLATION_REMOTE = 'remote';
+
+/**
+ * Translation source is a local file.
+ */
+const LOCALE_TRANSLATION_LOCAL = 'local';
+
+/**
+ * Translation source is the current translation.
+ */
+const LOCALE_TRANSLATION_CURRENT = 'current';
+
+/**
+ * Translation source is a downloaded file.
+ */
+const LOCALE_TRANSLATION_DOWNLOADED = 'download';
+
+/**
+ * Translation source is an imported file.
  */
-const LOCALE_TRANSLATION_DEFAULT_FILENAME = '%project-%version.%language.po';
+const LOCALE_TRANSLATION_IMPORTED = 'import';
 
 /**
  * Implements hook_help().
@@ -176,10 +214,20 @@ function locale_menu() {
     'type' => MENU_LOCAL_TASK,
     'file' => 'locale.bulk.inc',
   );
+  $items['admin/config/regional/translate/settings'] = array(
+    'title' => 'Settings',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('locale_translate_settings'),
+    'access arguments' => array('translate interface'),
+    'weight' => 40,
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'locale.pages.inc',
+  );
   $items['admin/reports/translations'] = array(
     'title' => 'Available translation updates',
     'description' => 'Get a status report about available interface translations for your installed modules and themes.',
-    'page callback' => 'locale_translation_status',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('locale_translation_status_form'),
     'access arguments' => array('translate interface'),
     'file' => 'locale.pages.inc',
   );
@@ -227,7 +275,7 @@ function locale_stream_wrappers() {
       'name' => t('Translation files'),
       'class' => 'Drupal\locale\TranslationsStream',
       'description' => t('Translation files'),
-      'type' => STREAM_WRAPPERS_LOCAL_NORMAL,
+      'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
     ),
   );
   return $wrappers;
@@ -264,11 +312,10 @@ function locale_language_delete($language) {
 
   // Remove interface translation files.
   module_load_include('inc', 'locale', 'locale.bulk');
-  locale_translate_delete_translation_files($language->langcode);
-
-  _locale_invalidate_js($language->langcode);
+  locale_translate_delete_translation_files(array(), array($language->langcode));
 
   // Changing the language settings impacts the interface:
+  _locale_invalidate_js($language->langcode);
   cache('page')->flush();
 
   // Clearing all locale cache from database
@@ -424,6 +471,13 @@ function locale_modules_installed($modules) {
 }
 
 /**
+ * Implements hook_modules_uninstalled().
+ */
+function locale_modules_uninstalled($modules) {
+  locale_system_remove($modules);
+}
+
+/**
  * Implements hook_themes_enabled().
  *
  * @todo This is technically wrong. We must not import upon enabling, but upon
@@ -434,34 +488,80 @@ function locale_themes_enabled($themes) {
 }
 
 /**
+ * Implements hook_themes_disabled().
+ */
+function locale_themes_disabled($themes) {
+  locale_system_remove($themes);
+}
+
+/**
  * Imports translations when new modules or themes are installed.
  *
- * This function will either import translation for the component change
- * right away, or start a batch if more files need to be imported.
+ * This function will start a batch to import translations for the added
+ * components.
  *
- * @param $components
+ * @param array $components
  *   An array of component (theme and/or module) names to import
  *   translations for.
- *
- * @todo
- *   This currently imports all .po files available, independent of
- *   $components. Once we integrated with update status for project
- *   identification, we should revisit and only import files for the
- *   identified projects for the components.
  */
 function locale_system_update($components) {
   // Skip running the translation imports if in the installer,
   // because it would break out of the installer flow. We have
   // built-in support for translation imports in the installer.
   if (!drupal_installation_attempted()) {
-    include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc';
-    if ($batch = locale_translate_batch_import_files(array(), TRUE)) {
+    module_load_include('compare.inc', 'locale');
+
+    // Update the list of translatable projects and start the import batch.
+    // Only when new projects are added the update batch will be triggered. Not
+    // each enabled module will introduce a new project. E.g. sub modules.
+    $projects = array_keys(locale_translation_build_projects());
+    if ($components = array_intersect($components, $projects)) {
+      module_load_include('fetch.inc', 'locale');
+      // Get translation status of the projects, download and update translations.
+      $options = _locale_translation_default_update_options();
+      $batch = locale_translation_batch_update_build($components, array(), $options);
       batch_set($batch);
     }
   }
 }
 
 /**
+ * Delete translation history of modules and themes.
+ *
+ * Only the translation history is removed, not the source strings or
+ * translations. This is not possible because strings are shared between
+ * modules and we have no record of which string is used by which module.
+ *
+ * @param array $components
+ *   An array of component (theme and/or module) names to remove
+ *   translation history.
+ */
+function locale_system_remove($components) {
+  module_load_include('compare.inc', 'locale');
+
+  // Only when projects are removed, the translation files and records will be
+  // deleted. Not each disabled module will remove a project. E.g. sub modules.
+  $projects = array_keys(locale_translation_get_projects());
+  if ($components = array_intersect($components, $projects)) {
+    locale_translation_file_history_delete($components);
+
+    // Remove translation files.
+    module_load_include('inc', 'locale', 'locale.bulk');
+    locale_translate_delete_translation_files($components, array());
+
+    // Remove translatable projects.
+    // Followup issue http://drupal.org/node/1842362 to replace the
+    // {locale_project} table. Then change this to a function call.
+    db_delete('locale_project')
+      ->condition('name', $components)
+      ->execute();
+
+    // Clear the translation status.
+    locale_translation_clear_status();
+  }
+}
+
+/**
  * Implements hook_js_alter().
  *
  * This function checks all JavaScript files currently added via drupal_add_js()
@@ -652,8 +752,11 @@ function locale_form_language_admin_add_form_alter_submit($form, $form_state) {
     $langcode = $form_state['values']['predefined_langcode'];
   }
 
-  include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc';
-  locale_translate_add_language_set_batch(array('langcode' => $langcode));
+  // Download and import translations for the newly added language.
+  module_load_include('fetch.inc', 'locale');
+  $options = _locale_translation_default_update_options();
+  $batch = locale_translation_batch_update_build(array(), array($langcode), $options);
+  batch_set($batch);
 }
 
 /**
@@ -698,7 +801,7 @@ function locale_form_system_file_system_settings_alter(&$form, $form_state) {
     '#title' => t('Interface translations directory'),
     '#default_value' => variable_get('locale_translate_file_directory', conf_path() . '/files/translations'),
     '#maxlength' => 255,
-    '#description' => t('A local file system path where interface translation files are looked for. This directory must exist.'),
+    '#description' => t('A local file system path where interface translation files will be stored.'),
     '#after_build' => array('system_check_directory'),
     '#weight' => 10,
   );
@@ -745,11 +848,97 @@ function locale_preprocess_node(&$variables) {
 }
 
 /**
+ * Gets current translation status from the {locale_file} table.
+ *
+ * @return array
+ *   Array of translation file objects.
+ */
+function locale_translation_get_file_history() {
+  $history = &drupal_static(__FUNCTION__, array());
+
+  if (empty($history)) {
+    // Get file history from the database.
+    $result = db_query('SELECT project, langcode, filename, version, uri, timestamp, last_checked FROM {locale_file}');
+    foreach ($result as $file) {
+      $file->type = LOCALE_TRANSLATION_CURRENT;
+      $history[$file->project][$file->langcode] = $file;
+    }
+  }
+  return $history;
+}
+
+/**
+ * Updates the {locale_file} table.
+ *
+ * @param object $file
+ *   Object representing the file just imported.
+ *
+ * @return integer
+ *   FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
+ *
+ * @see drupal_write_record()
+ */
+function locale_translation_update_file_history($file) {
+  // Update or write new record.
+  if (db_query("SELECT project FROM {locale_file} WHERE project = :project AND langcode = :langcode", array(':project' => $file->project, ':langcode' => $file->langcode))->fetchField()) {
+    $update = array('project', 'langcode');
+  }
+  else {
+    $update = array();
+  }
+  return drupal_write_record('locale_file', $file, $update);
+}
+
+/**
+ * Deletes the history of downloaded translations.
+ *
+ * @param array $projects
+ *   Project name(s) to be deleted from the file history. If both project(s) and
+ *   language code(s) are specified the conditions will be ANDed.
+ * @param array $langcode
+ *   Language code(s) to be deleted from the file history.
+ */
+function locale_translation_file_history_delete($projects = array(), $langcodes = array()) {
+  $query = db_delete('locale_file');
+  if (!empty($projects)) {
+    $query->condition('project', $projects);
+  }
+  if (!empty($langcodes)) {
+    $query->condition('langcode', $langcodes);
+  }
+  $query->execute();
+}
+
+/**
+ * Saves the status of translation sources in static cache.
+ *
+ * @param array $data
+ *   Array of translation source data, structured by project name and langcode.
+ */
+function locale_translation_status_save($data) {
+  // Followup issue: http://drupal.org/node/1842362
+  // Split status storage per module/language and expire individually. This will
+  // improve performance for large sites.
+  $status = state()->get('locale.translation_status');
+  $status = empty($status) ? array() : $status;
+
+  // Merge the new data into the existing structured status array.
+  foreach ($data as $project => $languages) {
+    foreach ($languages as $langcode => $source) {
+      $status[$project][$langcode] = $source;
+    }
+  }
+
+  state()->set('locale.translation_status', $status);
+  state()->set('locale.translation_last_checked', REQUEST_TIME);
+}
+
+/**
  * Clear the translation status cache.
  */
 function locale_translation_clear_status() {
-  state()->delete('locale_translation_status');
-  state()->delete('locale_translation_status_last_update');
+  state()->delete('locale.translation_status');
+  state()->delete('locale.translation_last_checked');
 }
 
 /**
diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc
index 8ea8780..ad9f1f6 100644
--- a/core/modules/locale/locale.pages.inc
+++ b/core/modules/locale/locale.pages.inc
@@ -455,11 +455,14 @@ function locale_translate_edit_form_submit($form, &$form_state) {
 function locale_translation_manual_status() {
   module_load_include('compare.inc', 'locale');
 
+  // Check translation status of all translatable project in all languages.
+  // First we clear the cached list of projects. Although not strictly
+  // nescessary, this is helpfull in case the project list is out of sync.
   locale_translation_flush_projects();
-  $projects = locale_translation_get_projects();
-  locale_translation_check_projects($projects);
+  locale_translation_check_projects();
 
-  // Execute a batch if required.
+  // Execute a batch if required. A batch is only used when remote files
+  // are checked.
   if (batch_get()) {
     batch_process('admin/reports/translations');
   }
@@ -467,19 +470,183 @@ function locale_translation_manual_status() {
 }
 
 /**
+ * Page callback: Configuration for interface translation.
+ *
+ * @see locale_menu()
+ */
+function locale_translate_settings($form, &$form_state) {
+  $config = config('locale.settings');
+
+  $form['update_interval_days'] = array(
+    '#type' => 'radios',
+    '#title' => t('Check for updates'),
+    '#default_value' => $config->get('translation.update_interval_days'),
+    '#options' => array(
+      '0' => t('Never (manually)'),
+      '1' => t('Daily'),
+      '7' => t('Weekly'),
+    ),
+    '#description' => t('Select how frequently you want to check for new interface translations for your currently installed modules and themes. <a href="@url">Check updates now</a>.', array('@url' => url('admin/reports/translations/check'))),
+  );
+
+  $form['check_disabled_modules'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Check for updates of disabled modules and themes'),
+    '#default_value' => $config->get('translation.check_disabled_modules'),
+  );
+
+  if ($directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations')) {
+    $description = t('Translation files are stored locally in the  %path directory. You can change this directory on the <a href="@url">File system</a> configuration page.', array('%path' => $directory, '@url' => url('admin/config/media/file-system')));
+  }
+  else {
+    $description = t('Translation files will not be stored locally. Change the Interface translation directory on the <a href="@url">File system configuration</a> page.', array('@url' => url('admin/config/media/file-system')));
+  }
+  $form['#translation_directory'] = $directory;
+  $form['use_source'] = array(
+    '#type' => 'radios',
+    '#title' => t('Translation source'),
+    '#default_value' => $config->get('translation.use_source'),
+    '#options' => array(
+      LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL => t('Drupal translation server and local files'),
+      LOCALE_TRANSLATION_USE_SOURCE_LOCAL => t('Local files only'),
+    ),
+    '#description' => t('The source of translation files for automatic interface translation.') . ' ' . $description,
+  );
+
+  if ($config->get('translation.overwrite_not_customized') == FALSE) {
+    $default = LOCALE_TRANSLATION_OVERWRITE_NONE;
+  }
+  elseif ($config->get('translation.overwrite_customized') == TRUE) {
+    $default = LOCALE_TRANSLATION_OVERWRITE_ALL;
+  }
+  else {
+    $default = LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED;
+  }
+  $form['overwrite'] = array(
+    '#type' => 'radios',
+    '#title' => t('Import behaviour'),
+    '#default_value' => $default,
+    '#options' => array(
+      LOCALE_TRANSLATION_OVERWRITE_NONE => t("Don't overwrite existing translations."),
+      LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED => t('Only overwrite imported translations, customized translations are kept.'),
+      LOCALE_TRANSLATION_OVERWRITE_ALL => t('Overwrite existing translations.'),
+    ),
+    '#description' => t('How to treat existing translations when automatically updating the interface translations.'),
+  );
+
+  return system_config_form($form, $form_state);
+}
+
+/**
+ * Form validation handler for locale_translate_settings().
+ *
+ * @see locale_translate_settings()
+ */
+function locale_translate_settings_validate($form, &$form_state) {
+  if (empty($form['#translation_directory']) && $form_state['values']['use_source'] == LOCALE_TRANSLATION_USE_SOURCE_LOCAL) {
+    form_set_error('use_source', t('You have selected local translation source, but no <a href="@url">Interface translation directory</a> was configured.', array('@url' => url('admin/config/media/file-system'))));
+  }
+}
+
+/**
+ * Form submission handler for locale_translate_settings().
+ *
+ * @see locale_translate_settings()
+ */
+function locale_translate_settings_submit($form, &$form_state) {
+  $values = $form_state['values'];
+
+  $config = config('locale.settings');
+  $config->set('translation.update_interval_days', $values['update_interval_days'])->save();
+  $config->set('translation.use_source', $values['use_source'])->save();
+
+  switch ($values['overwrite']) {
+    case LOCALE_TRANSLATION_OVERWRITE_ALL:
+      $config
+        ->set('translation.overwrite_customized', TRUE)
+        ->set('translation.overwrite_not_customized', TRUE);
+      break;
+    case LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED:
+      $config
+        ->set('translation.overwrite_customized', FALSE)
+        ->set('translation.overwrite_not_customized', TRUE);
+      break;
+    case LOCALE_TRANSLATION_OVERWRITE_NONE:
+      $config
+        ->set('translation.overwrite_customized', FALSE)
+        ->set('translation.overwrite_not_customized', FALSE);
+      break;
+  }
+
+  $config
+    ->set('translation.check_disabled_modules', $values['check_disabled_modules'])
+    ->save();
+
+  // Invalidate the cached translation status when the configuration setting of
+  // 'use_source' and 'check_disabled_modules' change.
+  if ($form['use_source']['#default_value'] != $form_state['values']['use_source'] ||
+      $form['check_disabled_modules']['#default_value'] != $form_state['values']['check_disabled_modules']) {
+    locale_translation_clear_status();
+  }
+}
+
+/**
  * Page callback: Display the current translation status.
  *
  * @see locale_menu()
  */
-function locale_translation_status() {
+function locale_translation_status_form($form, &$form_state) {
+  module_load_include('compare.inc', 'locale');
+
   $languages = locale_translatable_language_list();
+  $status = state()->get('locale.translation_status');
+
   if (!$languages) {
     drupal_set_message(t('No translatable languages available. <a href="@add_lanuage">Add language</a> first.', array('@add_lanuage' => url('admin/config/regional/language'))), 'warning');
   }
 
-  // @todo Calculate and display the translation status here. See the follow-up
-  // issue for translation interface: http://drupal.org/node/1804702
-  return 'TODO: Show the translation status here';
+  // @todo Add user interface update for translation update.
+  // Followup issues: Display translation status http://drupal.org/node/1804702
+  // and Translations update feature UX http://drupal.org/node/1029554
+  $form['langcodes'] = array(
+    '#type' => 'value',
+    '#value' => drupal_map_assoc(array_keys($languages)),
+  );
+
+  $form['actions'] = array(
+    '#type' => 'actions'
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Update')
+  );
+  return $form;
+}
+
+/**
+ * Form submission handler for locale_translation_status().
+ */
+function locale_translation_status_form_submit($form, &$form_state) {
+  module_load_include('fetch.inc', 'locale');
+  $langcodes = array_filter($form_state['values']['langcodes']);
+
+  // Set the translation import options. This determines if existing
+  // translations will be overwritten by imported strings.
+  $options = _locale_translation_default_update_options();
+
+  // If the status was updated recently we can immediately start fetching the
+  // translation updates. If the status is expired we clear it an run a batch to
+  // update the status and then fetch the translation updates.
+  $last_checked = state()->get('locale.translation_last_checked');
+  if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) {
+    locale_translation_clear_status();
+    $batch = locale_translation_batch_update_build(array(), $langcodes, $options);
+    batch_set($batch);
+  }
+  else {
+    $batch = locale_translation_batch_fetch_build(array(), $langcodes, $options);
+    batch_set($batch);
+  }
 }
 
 /**
diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc
new file mode 100644
index 0000000..d6060f6
--- /dev/null
+++ b/core/modules/locale/locale.translation.inc
@@ -0,0 +1,358 @@
+<?php
+
+/**
+ * @file
+ * Common API for interface translation.
+ */
+
+/**
+ * Comparison result of source files timestamps.
+ *
+ * Timestamp of source 1 is less than the timestamp of source 2.
+ * @see _locale_translation_source_compare()
+ */
+const LOCALE_TRANSLATION_SOURCE_COMPARE_LT = -1;
+
+/**
+ * Comparison result of source files timestamps.
+ *
+ * Timestamp of source 1 is equal to the timestamp of source 2.
+ * @see _locale_translation_source_compare()
+ */
+const LOCALE_TRANSLATION_SOURCE_COMPARE_EQ = 0;
+
+/**
+ * Comparison result of source files timestamps.
+ *
+ * Timestamp of source 1 is greater than the timestamp of source 2.
+ * @see _locale_translation_source_compare()
+ */
+const LOCALE_TRANSLATION_SOURCE_COMPARE_GT = 1;
+
+/**
+ * Get array of projects which are available for interface translation.
+ *
+ * This project data contains all projects which will be checked for available
+ * interface translations.
+ *
+ * For full functionality this function depends on Update module.
+ * When Update module is enabled the project data will contain the most recent
+ * module status; both in enabled status as in version. When Update module is
+ * disabled this function will return the last known module state. The status
+ * will only be updated once Update module is enabled.
+ *
+ *  @params array $project_names
+ *    Array of names of the projects to get.
+ *
+ * @return array
+ *   Array of project data for translation update.
+ *
+ * @see locale_translation_build_projects()
+ */
+function locale_translation_get_projects($project_names = array()) {
+  $projects = &drupal_static(__FUNCTION__, array());
+
+  if (empty($projects)) {
+    // Get project data from the database.
+    $result = db_query('SELECT name, project_type, core, version, server_pattern, status FROM {locale_project}');
+
+    // http://drupal.org/node/1777106 is a follow-up issue to make the check for
+    // possible out-of-date project information more robust.
+    if ($result->rowCount() == 0 && module_exists('update')) {
+      module_load_include('compare.inc', 'locale');
+      // At least the core project should be in the database, so we build the
+      // data if none are found.
+      locale_translation_build_projects();
+      $result = db_query('SELECT name, project_type, core, version, server_pattern, status FROM {locale_project}');
+    }
+
+    foreach ($result as $project) {
+      $projects[$project->name] = $project;
+    }
+  }
+
+  // Return the requested project names or all projects.
+  if ($project_names) {
+    return array_intersect_key($projects, drupal_map_assoc($project_names));
+  }
+  return $projects;
+}
+
+/**
+ * Clears the projects cache.
+ */
+function locale_translation_clear_cache_projects() {
+  drupal_static('locale_translation_get_projects', array());
+}
+
+/**
+ * Loads cached translation sources containing current translation status.
+ *
+ * @param array $projects
+ *   Array of project names. Defaults to all translatable projects.
+ * @param array $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
+ *
+ * @return array
+ *   Array of source objects. Keyed with <project name>:<language code>.
+ *
+ * @see locale_translation_source_build()
+ */
+function locale_translation_load_sources($projects = NULL, $langcodes = NULL) {
+  $sources = array();
+  $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
+  $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+
+  // Load source data from locale_translation_status cache.
+  $status = state()->get('locale.translation_status');
+
+  // Use only the selected projects and languages for update.
+  foreach($projects as $project) {
+    foreach ($langcodes as $langcode) {
+      $sources[$project . ':' . $langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL;
+    }
+  }
+  return $sources;
+}
+
+/**
+ * Build translation sources.
+ *
+ * @param array $projects
+ *   Array of project names. Defaults to all translatable projects.
+ * @param array $langcodes
+ *   Array of language codes. Defaults to all translatable languages.
+ *
+ * @return array
+ *   Array of source objects. Keyed with <project name>:<language code>.
+ *
+ * @see locale_translation_source_build()
+ */
+function locale_translation_build_sources($projects = array(), $langcodes = array()) {
+  $sources = array();
+  $projects = locale_translation_get_projects($projects);
+  $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
+
+  foreach ($projects as $project) {
+    foreach ($langcodes as $langcode) {
+      $source = locale_translation_source_build($project, $langcode);
+      $sources[$source->name . ':' . $source->langcode] = $source;
+    }
+  }
+  return $sources;
+}
+
+/**
+ * Checks whether a po file exists in the local filesystem.
+ *
+ * It will search in the directory set in the translation source. Which defaults
+ * to the "translations://" stream wrapper path. The directory may contain any
+ * valid stream wrapper.
+ *
+ * The "local" files property of the source object contains the definition of a
+ * po file we are looking for. The file name defaults to
+ * %project-%version.%language.po. Per project this value can be overridden
+ * using the server_pattern directive in the module's .info file or by using
+ * hook_locale_translation_projects_alter().
+ *
+ * @param object $source
+ *   Translation source object.
+ *
+ * @return stdClass
+ *   File object (filename, basename, name) updated with data of the po file.
+ *   On success the files property of the source object is updated.
+ *   files[LOCALE_TRANSLATION_LOCAL]:
+ *   - "uri": File name and path.
+ *   - "timestamp": Last updated time of the po file.
+ *   FALSE if the file is not found.
+ *
+ * @see locale_translation_source_build()
+ */
+function locale_translation_source_check_file(&$source) {
+  if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
+    $directory = $source->files[LOCALE_TRANSLATION_LOCAL]->directory;
+    $filename = '/' . preg_quote($source->files[LOCALE_TRANSLATION_LOCAL]->filename) . '$/';
+
+    // If the directory contains a stream wrapper, it is converted to a real
+    // path. This is required for file_scan_directory() which can not handle
+    // stream wrappers.
+    if ($scheme = file_uri_scheme($directory)) {
+      $directory = str_replace($scheme . '://', drupal_realpath($scheme . '://'), $directory);
+    }
+
+    if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) {
+      $file = current($files);
+      $source->files[LOCALE_TRANSLATION_LOCAL]->uri = $file->uri;
+      $source->files[LOCALE_TRANSLATION_LOCAL]->timestamp = filemtime($file->uri);
+      return $file;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Builds abstract translation source.
+ *
+ * @param object $project
+ *   Project object.
+ * @param string $langcode
+ *   Language code.
+ * @param string $filename
+ *   File name of translation file. May contain placeholders.
+ *
+ * @return object
+ *   Source object:
+ *   - "project": Project name.
+ *   - "name": Project name (inherited from project).
+ *   - "language": Language code.
+ *   - "core": Core version (inherited from project).
+ *   - "version": Project version (inherited from project).
+ *   - "project_type": Project type (inherited from project).
+ *   - "files": Array of file objects containing properties of local and remote
+ *     translation files.
+ *   Other processes can add the following properties:
+ *   - "type": Most recent file type LOCALE_TRANSLATION_REMOTE or
+ *      LOCALE_TRANSLATION_LOCAL. Corresponding with a key of the
+ *      "files" array.
+ *   - "timestamp": Timestamp of the most recent translation file.
+ *   The "files" array can hold file objects of type:
+ *   LOCALE_TRANSLATION_LOCAL, LOCALE_TRANSLATION_REMOTE,
+ *   LOCALE_TRANSLATION_DOWNLOADED, LOCALE_TRANSLATION_IMPORTED and
+ *   LOCALE_TRANSLATION_CURRENT. Each contains following properties:
+ *   - "type": The object type (LOCALE_TRANSLATION_LOCAL,
+ *     LOCALE_TRANSLATION_REMOTE, etc. see above).
+ *   - "project": Project name.
+ *   - "langcode": Language code.
+ *   - "version": Project version.
+ *   - "uri": Local or remote file path.
+ *   - "directory": Directory of the local po file.
+ *   - "filename": File name.
+ *   - "timestamp": Timestamp of the file.
+ *   - "keep": TRUE to keep the downloaded file.
+ */
+function locale_translation_source_build($project, $langcode, $filename = NULL) {
+  // Followup issue: http://drupal.org/node/1842380
+  // Convert $source object to a TranslatableProject class and use a typed class
+  // for $source-file.
+
+  // Create a source object with data of the project object.
+  $source = clone $project;
+  $source->project = $project->name;
+  $source->langcode = $langcode;
+
+  $filename = $filename ? $filename : config('locale.settings')->get('translation.default_filename');
+
+  // If the server_pattern contains a remote file path we will check for a
+  // remote file. The local version of this file will only be checked if a
+  // translations directory has been defined. If the server_pattern is a local
+  // file path we will only check for a file in the local file system.
+  $files = array();
+  if (_locale_translation_file_is_remote($source->server_pattern)) {
+    $files[LOCALE_TRANSLATION_REMOTE] = (object) array(
+      'project' => $project->name,
+      'langcode' => $langcode,
+      'version' => $project->version,
+      'type' => LOCALE_TRANSLATION_REMOTE,
+      'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
+      'uri' => locale_translation_build_server_pattern($source, $source->server_pattern),
+    );
+    if (variable_get('locale_translate_file_directory', conf_path() . '/files/translations')) {
+      $files[LOCALE_TRANSLATION_LOCAL] = (object) array(
+        'project' => $project->name,
+        'langcode' => $langcode,
+        'version' => $project->version,
+        'type' => LOCALE_TRANSLATION_LOCAL,
+        'filename' => locale_translation_build_server_pattern($source, $filename),
+        'directory' => 'translations://',
+      );
+      $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . $files[LOCALE_TRANSLATION_LOCAL]->filename;
+    }
+  }
+  else {
+    $files[LOCALE_TRANSLATION_LOCAL] = (object) array(
+      'project' => $project->name,
+      'langcode' => $langcode,
+      'version' => $project->version,
+      'type' => LOCALE_TRANSLATION_LOCAL,
+      'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
+      'directory' => locale_translation_build_server_pattern($source, drupal_dirname($source->server_pattern)),
+    );
+    $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . '/' . $files[LOCALE_TRANSLATION_LOCAL]->filename;
+  }
+  $source->files = $files;
+
+  return $source;
+}
+
+/**
+ * Determine if a file is a remote file.
+ *
+ * @param string $uri
+ *   The URI or URI pattern of the file.
+ *
+ * @return boolean
+ *   TRUE if the $uri is a remote file.
+ */
+function _locale_translation_file_is_remote($uri) {
+  $scheme = file_uri_scheme($uri);
+  if ($scheme) {
+    return !drupal_realpath($scheme . '://');
+  }
+  return FALSE;
+}
+
+/**
+ * Compare two update sources, looking for the newer one.
+ *
+ * The timestamp property of the source objects are used to determine which is
+ * the newer one.
+ *
+ * @param object $source1
+ *   Source object of the first translation source.
+ * @param object $source2
+ *   Source object of available update.
+ *
+ * @return integer
+ *   - "LOCALE_TRANSLATION_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1
+ *     is missing.
+ *   - "LOCALE_TRANSLATION_SOURCE_COMPARE_EQ":  $source1 == $source2 OR both
+ *     $source1 and $source2 are missing.
+ *   - "LOCALE_TRANSLATION_SOURCE_COMPARE_GT":  $source1 > $source2 OR $source2
+ *     is missing.
+ */
+function _locale_translation_source_compare($source1, $source2) {
+  if (isset($source1->timestamp) && isset($source2->timestamp)) {
+    if ($source1->timestamp == $source2->timestamp) {
+      return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
+    }
+    else {
+      return $source1->timestamp > $source2->timestamp ? LOCALE_TRANSLATION_SOURCE_COMPARE_GT : LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
+    }
+  }
+  elseif (isset($source1->timestamp) && !isset($source2->timestamp)) {
+    return LOCALE_TRANSLATION_SOURCE_COMPARE_GT;
+  }
+  elseif (!isset($source1->timestamp) && isset($source2->timestamp)) {
+    return LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
+  }
+  else {
+    return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
+  }
+}
+
+/**
+ * Returns default import options for translation update.
+ *
+ * @return array
+ *   Array of translation import options.
+ */
+function _locale_translation_default_update_options() {
+  $config = config('locale.settings');
+  return array(
+    'customized' => LOCALE_NOT_CUSTOMIZED,
+    'overwrite_options' => array(
+      'not_customized' => $config->get('translation.overwrite_not_customized'),
+      'customized' => $config->get('translation.overwrite_customized'),
+    ),
+  );
+}
diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.install b/core/modules/locale/tests/modules/locale_test/locale_test.install
index 8f04e0f..3d9dff3 100644
--- a/core/modules/locale/tests/modules/locale_test/locale_test.install
+++ b/core/modules/locale/tests/modules/locale_test/locale_test.install
@@ -10,6 +10,6 @@
  */
 function locale_test_uninstall() {
   // Clear variables.
-  state()->delete('locale_translation_test_system_info_alter');
-  state()->delete('locale_translation_test_projects');
+  state()->delete('locale.test_system_info_alter');
+  state()->delete('locale.test_projects_alter');
 }
diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.module b/core/modules/locale/tests/modules/locale_test/locale_test.module
index 03e455d..8f87ddc 100644
--- a/core/modules/locale/tests/modules/locale_test/locale_test.module
+++ b/core/modules/locale/tests/modules/locale_test/locale_test.module
@@ -16,8 +16,8 @@ function locale_test_system_info_alter(&$info, $file, $type) {
   // By default the locale_test modules are hidden and have a project specified.
   // To test the module detection proces by locale_project_list() the
   // test modules should mimic a custom module. I.e. be non-hidden.
-  if (state()->get('locale_translation_test_system_info_alter')) {
-    if ($file->name == 'locale_test' || $file->name == 'locale_test_disabled') {
+  if (state()->get('locale.test_system_info_alter')) {
+    if ($file->name == 'locale_test' || $file->name == 'locale_test_translate') {
       // Don't hide the module.
       $info['hidden'] = FALSE;
     }
@@ -33,11 +33,11 @@ function locale_test_system_info_alter(&$info, $file, $type) {
  * names, versions, timestamps etc must be fixed because they must match the
  * files created by the test script.
  *
- * The "locale_translation_test_projects" state variable must be set by the
+ * The "locale.test_projects_alter" state variable must be set by the
  * test script in order for this hook to take effect.
  */
 function locale_test_locale_translation_projects_alter(&$projects) {
-  if (state()->get('locale_translation_test_projects')) {
+  if (state()->get('locale.test_projects_alter')) {
 
     // Instead of the default ftp.drupal.org we use the file system of the test
     // instance to simulate a remote file location.
@@ -52,7 +52,7 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'drupal',
         'info' => array (
           'name' => 'Drupal',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt',
+          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
           'package' => 'Core',
           'version' => '8.0',
           'project' => 'drupal',
@@ -67,7 +67,7 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'contrib_module_one',
         'info' => array (
           'name' => 'Contributed module one',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt',
+          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
           'package' => 'Other',
           'version' => '8.x-1.1',
           'project' => 'contrib_module_one',
@@ -82,7 +82,7 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'contrib_module_two',
         'info' => array (
           'name' => 'Contributed module two',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt',
+          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
           'package' => 'Other',
           'version' => '8.x-2.0-beta4',
           'project' => 'contrib_module_two',
@@ -97,7 +97,7 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'contrib_module_three',
         'info' => array (
           'name' => 'Contributed module three',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt',
+          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
           'package' => 'Other',
           'version' => '8.x-1.0',
           'project' => 'contrib_module_three',
diff --git a/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.info b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.info
deleted file mode 100644
index 7eddf25..0000000
--- a/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.info
+++ /dev/null
@@ -1,10 +0,0 @@
-name = Disabled locale test
-description = Disabled support module for locale module testing.
-package = Testing
-version = VERSION
-core = 8.x
-hidden = TRUE
-project = locale_test_disabled
-
-; Definitions for interface translation.
-interface translation project = locale_test_disabled
diff --git a/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.module b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.module
deleted file mode 100644
index 0003e96..0000000
--- a/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.module
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * @file
- * Simulate a disabled contrib module for Locale test scripts.
- */
diff --git a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info
new file mode 100644
index 0000000..32dcd52
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info
@@ -0,0 +1,10 @@
+name = Locale test translate
+description = Translation test module for locale module testing.
+package = Testing
+version = 1.3
+core = 8.x
+hidden = TRUE
+
+; Definitions for interface translations.
+interface translation project = locale_test_translate
+interface translation server pattern = core/modules/locale/tests/test.%language.po
diff --git a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module
new file mode 100644
index 0000000..f4f1972
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module
@@ -0,0 +1,6 @@
+<?php
+
+/**
+ * @file
+ * Simulates a custom module with a local po file.
+ */
diff --git a/core/modules/locale/tests/test.nl.po b/core/modules/locale/tests/test.nl.po
new file mode 100644
index 0000000..2e956c1
--- /dev/null
+++ b/core/modules/locale/tests/test.nl.po
@@ -0,0 +1,31 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 7\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "Monday"
+msgstr "maandag"
+
+msgid "Tuesday"
+msgstr "dinsdag"
+
+msgid "Wednesday"
+msgstr "woensdag"
+
+msgid "Thursday"
+msgstr "donderdag"
+
+msgid "Extraday"
+msgstr "extra dag"
+
+msgid "Friday"
+msgstr "vrijdag"
+
+msgid "Saturday"
+msgstr "zaterdag"
+
+msgid "Sunday"
+msgstr "zondag"
