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..d8ba738 100644
--- a/core/modules/locale/config/locale.settings.yml
+++ b/core/modules/locale/config/locale.settings.yml
@@ -3,3 +3,6 @@ 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
+
diff --git a/core/modules/locale/lib/Drupal/locale/Gettext.php b/core/modules/locale/lib/Drupal/locale/Gettext.php
index a7c37ca..43432e4 100644
--- a/core/modules/locale/lib/Drupal/locale/Gettext.php
+++ b/core/modules/locale/lib/Drupal/locale/Gettext.php
@@ -53,10 +53,11 @@ static function filesToArray($langcode, array $files) {
    *
    * @param stdClass $file
    *   File object with an URI property pointing at the file's path.
+   *   @todo: Describe Required parts of the file object.
+   *   - 'langcode': The language code.
    *
    * @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 +84,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 +101,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..337bd90 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
index 017e2c5..01d1e09 100644
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php
@@ -45,6 +45,7 @@ public static function getInfo() {
   function setUp() {
     parent::setUp();
     module_load_include('compare.inc', 'locale');
+    module_load_include('fetch.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'));
@@ -162,7 +163,7 @@ function testLocaleCompare() {
    * 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
+   * (denied in .htaccess) the translation files get a _po extension. Another
    * directory is designated for local translation files.
    *
    * The translation status process by default checks the status of the
@@ -191,20 +192,25 @@ function testCompareCheckLocal() {
     // 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();
+    $config->set('translation.default_filename', '%project-%version.%language._po')->save();
+
+    // Setting up sets of translations for the translation files.
+    $translations_one = array('Monday' => 'Montag1', 'Tuesday' => 'Dienstag1', 'Wednesday' => 'Mittwoch1');
+    $translations_two = array( 'Tuesday' => 'Dienstag2', 'Wednesday' => 'Mittwoch2', 'Thursday' => 'Donnerstag2');
+    $translations_three = array('Thursday' => 'Donnerstag3', 'Friday' => 'Freitag3', 'Saturday' => 'Samstag3');
 
     // 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);
+    $this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $timestamp_new, $translations_one);
+    $this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de._po', $timestamp_old, $translations_two);
+    $this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de._po', $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.txt', $timestamp_old);
-    $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de.txt', $timestamp_new);
+    $this->makePoFile('local', 'contrib_module_one-8.x-1.1.de._po', $timestamp_old, $translations_one);
+    $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de._po', $timestamp_new, $translations_two);
     $this->makePoFile('local', 'custom_module_one.de.po', $timestamp_new);
 
     // Get status of translation sources at local file system.
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php
index 06d292f..5d7ce3a 100644
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php
@@ -89,7 +89,7 @@ function mockImportedPoFile($langcode, $timestamp_difference = 0) {
 
     // Fill the {locale_file} with a custom timestamp.
     if ($timestamp_difference != 0) {
-      locale_translate_update_file_history($file);
+      locale_translate_updation_file_history($file);
     }
 
     $count = db_query('SELECT COUNT(*) FROM {locale_file} WHERE langcode = :langcode', array(':langcode' => $langcode))->fetchField();
diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc
index a287e16..c4b6acd 100644
--- a/core/modules/locale/locale.batch.inc
+++ b/core/modules/locale/locale.batch.inc
@@ -6,42 +6,6 @@
  */
 
 /**
- * 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.
- */
-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;
-}
-
-/**
  * Batch operation callback: Check the availability of a remote po file.
  *
  * Checks the presence and creation time of one po file per batch process. The
@@ -62,16 +26,16 @@ function locale_translation_batch_status_fetch_remote($source, &$context) {
   // data with the remote status.
   if (isset($source->files['remote'])) {
     $remote_file = $source->files['remote'];
-    $result = locale_translation_http_check($remote_file->url);
+    $result = locale_translation_http_check($remote_file->uri);
 
     // Update the file object with the result data. In case of a redirect we
-    // store the resulting url.
+    // store the resulting uri.
     if ($result && !empty($result->updated)) {
-      $remote_file->url = isset($result->redirect_url) ? $result->redirect_url : $remote_file->url;
+      $remote_file->uri = isset($result->redirect_uri) ? $result->redirect_uri : $remote_file->uri;
       $remote_file->timestamp = $result->updated;
       $source->files['remote'] = $remote_file;
     }
-    $context['results'][$source->name][$source->language] = $source;
+    $context['results']['sources'][$source->name][$source->language] = $source;
   }
 }
 
@@ -102,10 +66,10 @@ function locale_translation_batch_status_fetch_local($sources, &$context) {
 
       // 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->language])) {
+        $source->files['remote'] = $context['results']['sources'][$source->name][$source->language]->files['remote'];
       }
-      $context['results'][$source->name][$source->language] = $source;
+      $context['results']['sources'][$source->name][$source->language] = $source;
     }
   }
 }
@@ -128,9 +92,10 @@ function locale_translation_batch_status_fetch_local($sources, &$context) {
  */
 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 ($context['results']['sources'] 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;
@@ -141,13 +106,36 @@ function locale_translation_batch_status_compare(&$context) {
       if (isset($file->timestamp)) {
         $source->type = $file->type;
         $source->timestamp = $file->timestamp;
-        $results[$project][$langcode] = $source;
       }
+
+      // 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->language])) {
+        $current = $history[$source->project][$source->language];
+        // Add the current translation to the source object to save it in
+        // the status cache.
+        $source->files['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);
 }
 
 /**
@@ -161,9 +149,9 @@ function locale_translation_batch_status_compare(&$context) {
 function locale_translation_batch_status_finished($success, $results) {
   $t = get_t();
   if($success) {
-    if ($results) {
+    if ($results['sources']) {
       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.'
       ));
@@ -175,18 +163,184 @@ function locale_translation_batch_status_finished($success, $results) {
 }
 
 /**
+ * Batch operation: Download a remote translation file.
+ *
+ * @param $translation source object
+ *   @todo
+ * @param $context
+ *   Batch context array.
+ */
+function locale_translation_batch_fetch_download($source, &$context) {
+  $t = get_t();
+  module_load_include('fetch.inc', 'locale');
+  if ($file = locale_translation_download_source($source->files['remote'])) {
+    $context['message'] = $t('Importing: %name.', array('%name' => $file->filename));
+    $source->files['download'] = $file;
+  }
+  else {
+    // @todo Error handling when download fails.
+    watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source->files['remote']->uri), WATCHDOG_ERROR);
+    $context['results']['failed_files'][] = $source->files['remote'];
+  }
+  $context['results']['sources'][$source->name][$source->language] = $source;
+}
+
+/**
+ * Batch process: Import translation file.
+ *
+ * This takes a file parameter or continues from previous batch
+ * which should have downloaded a file.
+ *
+ * @param $id
+ *   Batch id to identify batch results.
+ *   Result of this batch function are stored in $context['result']
+ *   identified by this $id.
+ * @param $file
+ *   File to be imported. If empty, the file will be taken from $context['results']['sources'].
+ * @param $mode
+ *   Import mode. How to treat existing and modified translations.
+ * @param $context
+ *   Batch context array.
+ */
+function locale_translation_batch_fetch_import($source, $options, &$context) {
+  $t = get_t();
+  // If we are working on a remote file we will import the downloaded file and
+  // use the result data from the
+  if ($source->type == 'remote') {
+    if (isset($context['results']['sources'][$source->project][$source->language]->files['download'])) {
+      $import_type = 'download';
+      $source_type = 'remote';
+      $source_result = $context['results']['sources'][$source->project][$source->language];
+    }
+  }
+  if ($source->type == 'local') {
+    $import_type = 'local';
+    $source_type = 'local';
+    $source_result = $source;
+  }
+
+// @todo Fault handling using try/catch. For other batch functions too?
+
+  if (isset($import_type)) {
+    $file = $source_result->files[$import_type];
+    module_load_include('bulk.inc', 'locale');
+    $options += array(
+      'customized' => LOCALE_NOT_CUSTOMIZED,
+      'message' => 'Importing: ' . $file->filename,
+    );
+
+    locale_translate_batch_import($file, $options, $context);
+    // Import is finished.
+    if (isset($context['finished']) && $context['finished'] == 1) {
+      // Import is successfull.
+      if (isset($context['results']['files'][$file->uri])) {
+        $context['message'] = $t('Imported: %name.', array('%name' => $file->filename));
+
+        // Store the data of imported source. This data can later be saved in {locale_file}.
+        $source_result->files['import'] = $source_result->files[$source_type];
+
+        // Downloaded files are stored in the temporary files directory. On
+        // succesfull import, the files are moved to the local destination
+        // directory if files should be kept locally. Otherwise the temporary
+        // files are deleted.
+        if ($import_type == 'download' && variable_get('locale_translate_file_directory', conf_path() . '/files/translations') && isset($source_result->files['local'])) {
+          // Move the temporary file to the local translations directory.
+          // @todo What to do if the file can not be moved? If it throws an error (watchdog), should we set a batch error?
+          if (file_unmanaged_move($file->uri, $source_result->files['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 = 'local';
+          }
+        }
+        if ($import_type == 'download') {
+          // The temporary file is now imported and will not be used further.
+          // Store the timestamp and delete the file.
+          $timestamp = filemtime($source_result->files[$import_type]->uri);
+          $source_result->files['import']->timestamp = $timestamp;
+          $source_result->files['import']->last_checked = $timestamp;
+          file_unmanaged_delete($file->uri);
+        }
+        if ($import_type == 'local') {
+          // Import type is 'local'.
+          $timestamp = filemtime($source_result->files[$import_type]->uri);
+          $source_result->files['local']->timestamp = $timestamp;
+          $source_result->files['import']->timestamp = $timestamp;
+          $source_result->files['import']->last_checked = $timestamp;
+
+        }
+      }
+      else {
+        // File import failed. We can delete the temporary file.
+        file_unmanaged_delete($file->uri);
+      }
+    }
+    $context['results']['sources'][$source->project][$source->language] = $source_result;
+  }
+}
+
+/**
+ * Batch process: Update the download history table.
+ *
+ * @param $id
+ *   Batch id to identify batch results.
+ *   Result of this batch function are stored in $context['result']
+ *   identified by this $id.
+ * @param $context
+ *   Batch context array.
+ */
+function locale_translation_batch_fetch_update_status(&$context) {
+  $results = array();
+
+  foreach ($context['results']['sources'] as $project => $langcodes) {
+    foreach ($langcodes as $langcode => $source) {
+
+      // Store the state of the translation in {locale_file} table. During the
+      // batch execution the data of imported file is kept in
+      // $context['results']['sources'] will now be stored in the database.
+      if (isset($source->files['import'])) {
+        $file = $source->files['import'];
+        locale_translation_update_file_history($file);
+        // @todo Unset. Leave it disabled for debugging.
+        //unset($source->files['import']);
+      }
+      // @todo Unset. Leave it disabled for debugging.
+      //unset($source->files['download']);
+      // 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 use to update the translation status.
+      $context['results']['sources'][$project][$langcode] = $source;
+    }
+  }
+}
+
+/**
+ * Batch finished callback: Set result message.
+ *
+ * @param $success
+ *   TRUE if batch succesfully completed.
+ * @param $results
+ *   Batch results.
+ */
+function locale_translation_batch_fetch_finished($success, $results) {
+  // @todo move locale_translate_batch_finished() into locale.batch.inc.
+  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.
  */
-function locale_translation_http_check($url, $headers = array()) {
-  $result = drupal_http_request($url, array('headers' => $headers, 'method' => 'HEAD'));
+function locale_translation_http_check($uri, $headers = array()) {
+  $result = drupal_http_request($uri, array('headers' => $headers, 'method' => 'HEAD'));
   if ($result && $result->code == '200') {
     $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0;
   }
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index 0be2aec..6168845 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -405,9 +405,25 @@ function locale_translate_batch_build($files, $options) {
   if (count($files)) {
     $operations = array();
     foreach ($files as $file) {
+      // @todo: Check if regex works with missing project and version.
+      // @todo: move the language part to the form submit function?
+      // The filename is either {langcode}.po or {prefix}.{langcode}.po.
+      // Try to get the project name, project version and language code file name.
+      // The filename is either {langcode}.po or {project}-{version}.{langcode}.po.
+      // We use the language from either the $options or the file name.
+      preg_match('!([a-z_]+)\-([0-9a-z\.\-\+]+)\.([^\./]+)\.po$!', $file->filename, $matches);
+      if (is_array($matches)) {
+        $file->project = isset($matches[1]) ? $matches[1] : '';
+        $file->version = isset($matches[2]) ? $matches[2] : '';
+        $file->langcode = isset($options['langcode']) ? $options['langcode'] : isset($matches[3]) ? $matches[3] : LANGUAGE_NOT_SPECIFIED;
+      }
+
       // 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,11 +442,12 @@ function locale_translate_batch_build($files, $options) {
 /**
  * Perform interface translation import as a batch step.
  *
+ * @todo Rework description.
  * 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 $file
+ *   File object ... @todo
  *
  * @param array $options
  *   An array with options that can have the following elements:
@@ -441,27 +458,19 @@ function locale_translate_batch_build($files, $options) {
  *     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,87 +486,125 @@ 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 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']['files'][$file->uri] = $file;
         $context['results']['languages'][$filepath] = $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();
+      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;
+          $context['results']['stats'][$file->uri] += array($key => 0);
+          $context['results']['stats'][$file->uri][$key] += $value;
         }
         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);
+        }
+        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];
         }
       }
     }
     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) {
+  foreach ($context['results']['files'] as $file) {
+    $file->last_checked = $file->timestamp;
+    locale_translation_update_file_history($file);
+  }
+}
+
+/**
  * Finished callback of system page locale import batch.
  */
 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'])) {
+      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;
+          }
+          $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(t('The translations were successfully imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
+      watchdog('locale', 'The translations were successfully imported. %number translations added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
+      if ($skips) {
+        if (module_exists('dblog')) {
+          $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')));
+        }
+        else {
+          $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($message, 'error');
+        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,28 +627,6 @@ function locale_translate_file_create($filepath) {
 }
 
 /**
- * Update the {locale_file} table.
- *
- * @param $file
- *   Object representing the file just imported.
- *
- * @return integer
- *   FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
- *
- * @see drupal_write_record()
- */
-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');
-  }
-  else {
-    $update = array();
-  }
-  return drupal_write_record('locale_file', $file, $update);
-}
-
-/**
  * Deletes all interface translation files depending on the langcode.
  *
  * @param $langcode
diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc
index efe944b..13604c6 100644
--- a/core/modules/locale/locale.compare.inc
+++ b/core/modules/locale/locale.compare.inc
@@ -12,6 +12,7 @@
  * timestamp in seconds. The download time is stored in the database in
  * {locale_file}.timestamp.
  */
+// @todo Is this still required since we use filemtime() to get the timestamp of downloaded and imported files?
 const LOCALE_TRANSLATION_TIMESTAMP_THRESHOLD = 2;
 
 /**
@@ -260,7 +261,7 @@ 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');
@@ -308,6 +309,7 @@ function locale_translation_build_server_pattern($project, $template) {
 function locale_translation_check_projects($projects, $langcodes = NULL) {
   module_load_include('batch.inc', 'locale');
 
+// @todo What happens if LOCALE_TRANSLATION_USE_SOURCE_LOCAL and no local file is available??
   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.
@@ -343,12 +345,47 @@ function locale_translation_check_projects_batch($projects, $langcodes = NULL) {
   }
 
   // Build and set the batch process.
-  module_load_include('batch.inc', 'locale');
   $batch = locale_translation_batch_status_build($sources);
   batch_set($batch);
 }
 
 /**
+ * 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.
+ */
+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;
+}
+
+/**
  * Check and store the status and timestamp of local po files.
  *
  * Only po files in the local file system are checked. Any remote translation
@@ -379,8 +416,8 @@ 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;
       }
+      $results[$name][$langcode] = $source;
     }
   }
 
@@ -425,7 +462,7 @@ function locale_translation_source_check_file(&$source) {
       $directory = str_replace($scheme . '://', drupal_realpath($scheme . '://'), $directory);
     }
 
-    if ($files = file_scan_directory($directory, $filename, array('key' => 'name'))) {
+    if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) {
       $file = current($files);
       $source->files['local']->uri = $file->uri;
       $source->files['local']->timestamp = filemtime($file->uri);
@@ -459,19 +496,24 @@ function locale_translation_source_check_file(&$source) {
  *   - "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.
+ *   The "files" array can hold file objects of type: 'local', 'remote',
+ *   'download', 'import' and 'current'. Each contains following properties:
+ *   - "type": The object type ('local', '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.
  */
-// @todo Move this file?
+// @todo Move this function?
 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;
+  // @todo Change this to langcode.
   $source->language = $langcode;
 
   $filename = $filename ? $filename : config('locale.settings')->get('translation.default_filename');
@@ -483,24 +525,35 @@ function locale_translation_source_build($project, $langcode, $filename = NULL)
   $files = array();
   if (_locale_translation_file_is_remote($source->server_pattern)) {
     $files['remote'] = (object) array(
+      'project' => $project->name,
+      'langcode' => $langcode,
+      'version' => $project->version,
       'type' => 'remote',
       'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
-      'url' => locale_translation_build_server_pattern($source, $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['local'] = (object) array(
+        'project' => $project->name,
+        'langcode' => $langcode,
+        'version' => $project->version,
         'type' => 'local',
-        'directory' => 'translations://',
         'filename' => locale_translation_build_server_pattern($source, $filename),
+        'directory' => 'translations://',
       );
+      $files['local']->uri = $files['local']->directory . $files['local']->filename;
     }
   }
   else {
     $files['local'] = (object) array(
+      'project' => $project->name,
+      'langcode' => $langcode,
+      'version' => $project->version,
       '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)),
+      'directory' => locale_translation_build_server_pattern($source, drupal_dirname($source->server_pattern)),
     );
+    $files['local']->uri = $files['local']->directory . '/' . $files['local']->filename;
   }
   $source->files = $files;
 
@@ -510,14 +563,14 @@ function locale_translation_source_build($project, $langcode, $filename = NULL)
 /**
  * Determine if a file is a remote file.
  *
- * @param string $url
- *   The URL or URL pattern of the file.
+ * @param string $uri
+ *   The URI or URI pattern of the file.
  *
  * @return boolean
- *   TRUE if the $url is a remote file.
+ *   TRUE if the $uri is a remote file.
  */
-function _locale_translation_file_is_remote($url) {
-  $scheme = file_uri_scheme($url);
+function _locale_translation_file_is_remote($uri) {
+  $scheme = file_uri_scheme($uri);
   if ($scheme) {
     return !drupal_realpath($scheme . '://');
   }
diff --git a/core/modules/locale/locale.fetch.inc b/core/modules/locale/locale.fetch.inc
new file mode 100755
index 0000000..e628faa
--- /dev/null
+++ b/core/modules/locale/locale.fetch.inc
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @file
+ * The API for download and import of translations from remote and local sources.
+ */
+
+/**
+ * Build a batch to download and import project translations.
+ *
+ * @param array $sources
+ *   Array of project translation states.
+ */
+function locale_translation_batch_fetch_build($sources) {
+  $t = get_t();
+  $batch = array(
+    'operations' => _locale_translation_fetch_operations($sources),
+    'title' => $t('Updating translation.'),
+    'init_message' => $t('Downloading and importing files.'),
+    'error_message' => $t('Error importing interface translations'),
+    '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 $sources
+ *   Array of project translation states.
+ */
+function _locale_translation_fetch_operations($sources) {
+  $operations = array();
+  $config = config('locale.settings');
+  // @todo Make follow up issue for different UI options. Back to radio's, not current checkboxes.
+  $options = array(
+    'overwrite_options' => array(
+      'not_customized' => $config->get('translation.overwrite_not_customized'),
+      'customized' => $config->get('translation.overwrite_customized'),
+    ),
+  );
+
+  foreach ($sources as $source) {
+    if (isset($source->type)) {
+      if ($source->type == 'remote') {
+        // Download + import
+        $operations[] = array('locale_translation_batch_fetch_download', array($source));
+        $operations[] = array('locale_translation_batch_fetch_import', array($source, $options));
+      }
+      elseif ($source->type == 'local') {
+        // Import local files.
+        $operations[] = array('locale_translation_batch_fetch_import', array($source, $options));
+      }
+    }
+  }
+  if (count($operations)) {
+    // 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;
+}
+
+/**
+ * Download 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 successful.
+ *   FALSE on failure.
+ */
+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 = 'download';
+    $file->uri = $uri;
+    $file->filename = $source_file->filename;
+    return $file;
+  }
+  return FALSE;
+}
diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install
index ce6e1e5..67b7db5 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(
@@ -796,6 +817,72 @@ 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(
+        '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(
+        '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 3ebe97d..14b7788 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -103,6 +103,11 @@
 const LOCALE_TRANSLATION_DEFAULT_FILENAME = '%project-%version.%language.po';
 
 /**
+ * The number of seconds that the translations status entry should be considered.
+ */
+const LOCALE_TRANSLATION_STATUS_TTL = 600;
+
+/**
  * Implements hook_help().
  */
 function locale_help($path, $arg) {
@@ -179,8 +184,21 @@ function locale_menu() {
   $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' => 'locale_translation_update_status',
+    'access arguments' => array('translate interface'),
+    'file' => 'locale.pages.inc',
+  );
+  $items['admin/reports/translations/list'] = array(
+    'title' => 'List',
     'access arguments' => array('translate interface'),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/reports/translations/settings'] = array(
+    'title' => 'Settings',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('locale_translation_admin_settings'),
+    'access arguments' => array('administer site configuration'),
+    'type' => MENU_LOCAL_TASK,
     'file' => 'locale.pages.inc',
   );
   $items['admin/reports/translations/check'] = array(
@@ -215,6 +233,14 @@ function locale_theme() {
       'render element' => 'form',
       'file' => 'locale.pages.inc',
     ),
+    'locale_translation_update_status' => array(
+      'variables' => array('languages' => NULL, 'projects' => NULL, 'updates' => NULL, 'last' => NULL),
+      'file' => 'locale.pages.inc',
+    ),
+    'locale_translation_language_updates' => array(
+      'variables' => array('updates' => NULL),
+      'file' => 'locale.pages.inc',
+    ),
   );
 }
 
@@ -745,6 +771,83 @@ function locale_preprocess_node(&$variables) {
 }
 
 /**
+ * Get current translation status from the {locale_file} table.
+ *
+ * @return array
+ *   Array of translation file objects.
+ */
+// @todo Is this the right place for this function?
+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 = 'current';
+      $history[$file->project][$file->langcode] = $file;
+    }
+  }
+  return $history;
+}
+
+/**
+ * Update the {locale_file} table.
+ *
+ * @param $file
+ *   Object representing the file just imported.
+ *
+ * @return integer
+ *   FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
+ *
+ * @see drupal_write_record()
+ */
+// @todo Is this the right place for this function?
+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);
+}
+
+/**
+ * Delete the history of downloaded translations.
+ *
+ * @param string $langcode
+ *   Language code of the file history to be deleted.
+ */
+function locale_translation_file_history_delete($langcode) {
+  db_delete('locale_file')
+    ->condition('language', $langcode)
+    ->execute();
+}
+
+/**
+ * Save 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) {
+  $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_status_last_update', REQUEST_TIME);
+}
+
+/**
  * Clear the translation status cache.
  */
 function locale_translation_clear_status() {
diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc
index 8ea8780..3e18e1c 100644
--- a/core/modules/locale/locale.pages.inc
+++ b/core/modules/locale/locale.pages.inc
@@ -471,15 +471,192 @@ function locale_translation_manual_status() {
  *
  * @see locale_menu()
  */
-function locale_translation_status() {
+function locale_translation_update_status() {
+  module_load_include('compare.inc', 'locale');
+  //locale_translation_flush_projects();
+  $projects = locale_translation_get_projects();
   $languages = locale_translatable_language_list();
+
   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');
+    $build['add_language']['#markup'] = t('No translatable languages available. <a href="@add_lanuage">Add language</a> first.', array(
+      '@add_lanuage' => url('admin/config/regional/language')
+    ));
+  }
+  else {
+    // === Start of temporay code. For development only ===
+    $last_update = state()->get('locale_translation_status_last_update');
+    $markup = '<p class="locale checked">';
+    $markup .= $last_update ? t('Last checked: @time ago', array('@time' => format_interval(REQUEST_TIME - $last_update))) : t('Last checked: never');
+    $markup .= ' <span class="check-manually">(' . l(t('Check manually'), 'admin/reports/translations/check', array('query' => drupal_get_destination())) . ')</span>';
+    $markup .= '</p>';
+    $build['last_checked'] = array(
+        '#markup' => $markup,
+    );
+
+    $status = _locale_translation_update_status();
+    $build['status']['#markup'] = theme('locale_translation_update_status', array(
+      'languages' => $languages,
+      'projects' => $projects,
+      'updates' => $status['updates'],
+      'last' => $status['last'],
+    ));
+    if ($status['updates']) {
+      $build['update'] = drupal_get_form('locale_translation_status_form');
+    }
+  }
+
+  return $build;
+}
+
+/**
+ * Compute available updates per language.
+ */
+function _locale_translation_update_status() {
+  $updates = $last = $sources = array();
+  $status = state()->get('locale_translation_status') ?: array();
+  foreach ($status as $name  => $project_status) {
+    foreach ($project_status as $langcode => $source) {
+      if (isset($last[$langcode])) {
+        $last_updated = $last[$langcode]['updated'];
+        $last_available = $last[$langcode]['available'];
+      }
+      else {
+        $last_updated = $last_available = 0;
+      }
+      $available = NULL;
+      if (isset($source->type)) {
+        if ($source->type == 'current') {
+          $last_updated = max($last_updated, $source->timestamp);
+        }
+        elseif ($source->type == 'remote') {
+          $available = $source->files['remote'];
+          $updates[$langcode]['remote'][$name] = $available;
+        }
+        elseif ($source->type == 'local') {
+          $available = $source->files['local'];
+          $updates[$langcode]['local'][$name] = $available;
+        }
+      }
+      if ($available) {
+        $last_available = max($last_available, $available->timestamp);
+        $sources[] = $source;
+      }
+      $last[$langcode]['updated'] = $last_updated;
+      $last[$langcode]['available'] = $last_available;
+    }
+  }
+
+  return array('updates' => $updates, 'last' => $last, 'sources' => $sources);
+}
+
+
+
+
+/**
+ * Page callback: Display the current translation status.
+ *
+ * @see locale_menu()
+ */
+function locale_translation_status_form($form, &$form_state) {
+  $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) {
+  // If the status was updated recently we can immediately start updating. If
+  // the status is not expired we will get the status first.
+  $last_checked = state()->get('locale_translation_status_last_update');
+  if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) {
+    locale_translation_clear_status();
+    // @todo Batch with check status + download + import.
+  }
+  else {
+    module_load_include('fetch.inc', 'locale');
+    $status = _locale_translation_update_status();
+    $batch = locale_translation_batch_fetch_build($status['sources']);
+    batch_set($batch);
+  }
+}
+
+/**
+ * Settings form for locale updates.
+ */
+function locale_translation_admin_settings($form, &$form_state) {
+  $config = config('locale.settings');
+  $form['options'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Update options'),
+    '#tree' => TRUE,
+  );
+  $form['options']['use_source'] = array(
+    '#title' => t('Update source'),
+    '#type' => 'select',
+    '#options' => array(
+      'remote_and_local' => t('Remote and local'),
+    ),
+    '#default_value' => $config->get('translation.use_source'),
+  );
+  $form['options']['default_server_pattern'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Server pattern'),
+    '#default_value' => $config->get('translation.default_server_pattern'),
+  );
+  $form['options']['overwrite_customized'] = array(
+  );
+  $form['options']['overwrite_not_customized'] = array(
+  );
+  $form['options']['check_disabled_modules'] = array(
+    '#title' => t('Update disabled modules'),
+    '#type' => 'checkbox',
+    '#default_value' => $config->get('translation.check_disabled_modules'),
+  );
+  // Select languages and projects.
+  module_load_include('compare.inc', 'locale');
+  $projects = locale_translation_get_projects();
+  $languages = locale_translatable_language_list();
+  $options = array();
+  foreach ($languages as $langcode => $language) {
+    $options[$langcode] = $language->name;
+  }
+  $form['langcodes'] = array(
+      '#title' => t('Languages'),
+      '#type' => 'checkboxes',
+      '#options' => $options,
+      '#default_value' => drupal_map_assoc(array_keys($options)),
+      '#required' => TRUE,
+  );
+  $options = array();
+  foreach ($projects as $name => $project) {
+    $options[$name] = $project->name;
   }
+  $form['projects'] = array(
+      '#title' => t('Projects'),
+      '#type' => 'checkboxes',
+      '#options' => $options,
+      '#default_value' => drupal_map_assoc(array_keys($options)),
+      '#required' => TRUE,
+  );
+  return system_settings_form($form);
+}
 
-  // @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';
+/**
+ * Submit settings for locale_updates.
+ */
+function locale_translation_admin_settings_submit($form, &$form_state) {
+  $config = config('locale.settings');
+  foreach ($from_state['values']['options'] as $name => $value) {
+    $config->set('translation.' . $name, $value);
+  }
+  $config->save();
 }
 
 /**
@@ -516,3 +693,84 @@ function theme_locale_translate_edit_form_strings($variables) {
   $output .= theme('pager');
   return $output;
 }
+
+/**
+ * Translation update status.
+ */
+function theme_locale_translation_update_status($variables) {
+  $languages = $variables['languages'];
+  $projects = $variables['projects'];
+  $updates = $variables['updates'];
+  $last = $variables['last'];
+  $total_strings = locale_storage()->countStrings();
+  $translations = locale_storage()->countTranslations();
+  $rows = array();
+  $local = $remote = 0;
+  foreach ($languages as $langcode => $language) {
+    $last_updated = isset($last[$langcode]) ? $last[$langcode]['updated'] : 0;
+
+    // Generate row content for language.
+    if ($last_updated && empty($updates[$langcode])) {
+      $class = 'ok';
+      $legend = t('Up to date');
+      $icon = theme('image', array('uri' => 'core/misc/watchdog-ok.png', 'width' => 18, 'height' => 18, 'alt' => t('ok'), 'title' => t('ok')));
+    }
+    elseif (!empty($updates[$langcode])) {
+      $class = 'warning';
+      $legend = t('Update available');
+      $icon = theme('image', array('uri' => 'core/misc/watchdog-warning.png', 'width' => 18, 'height' => 18, 'alt' => t('warning'), 'title' => t('warning')));
+    }
+    else {
+      $class = 'unknown';
+      $legend = t('Unknown');
+      $icon = theme('image', array('uri' => 'core/misc/watchdog-warning.png', 'width' => 18, 'height' => 18, 'alt' => t('warning'), 'title' => t('warning')));
+    }
+
+
+    $row['language'] = '<h3>' . check_plain($language->name) . '</h3>';
+    $row['language'] .= '<div class="last-updated">';
+    $row['language'] .= t('Last updated: @updated', array('@updated' => $last_updated ? format_date($last_updated, 'custom', 'j M - G:i:s') : t('Unknown')));
+    $row['language'] .= '</div>';
+    if ($total_strings) {
+      $translated = isset($translations[$langcode]) ? $translations[$langcode] : 0;
+      $row['language'] .= '<div class="translation-stats">';
+      $row['language'] .= t('Translated: @translated/@total (@ratio%)', array(
+        '@translated' => $translated,
+        '@total' => $total_strings,
+        '@ratio' => round($translated / $total_strings * 100, 2),
+      ));
+      $row['language'] .= '</div>';
+    }
+    if (isset($updates[$langcode])) {
+      $row['updates'] = theme('locale_translation_language_updates', array('updates' => $updates[$langcode]));
+    }
+    else {
+      $row['updates'] = '';
+    }
+    $row['status'] = '<div class="version-status">';
+    $row['status'] .= '<span>' . $legend . '</span>';
+    $row['status'] .= '<span class="icon">' . $icon . '</span>';
+    $row['status'] .= "</div>\n";
+    $rows[] = array(
+      'class' => array($class),
+      'data' => $row,
+    );
+  }
+  drupal_add_css(drupal_get_path('module', 'update') . '/update.css');
+  return theme('table', array('rows' => $rows, 'attributes' => array('class' => array('update'))));
+}
+
+/**
+ * Format language updates.
+ */
+function theme_locale_translation_language_updates($variables) {
+  $updates = $variables['updates'];
+  $items = array();
+  if (isset($updates['local'])) {
+    $items[] = t('Local updates: !projects', array('!projects' => implode(', ', array_keys($updates['local']))));
+  }
+  if (isset($updates['remote'])) {
+    $items[] = t('Remote updates: !projects', array('!projects' => implode(', ', array_keys($updates['remote']))));
+  }
+  return theme('item_list', array('items' => $items));
+}
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..e084915 100644
--- a/core/modules/locale/tests/modules/locale_test/locale_test.module
+++ b/core/modules/locale/tests/modules/locale_test/locale_test.module
@@ -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',
