diff --git a/core/includes/gettext.inc b/core/includes/gettext.inc
index 18c27ea..a8498dc 100644
--- a/core/includes/gettext.inc
+++ b/core/includes/gettext.inc
@@ -23,11 +23,16 @@
  *   Drupal file object corresponding to the PO file to import.
  * @param $langcode
  *   Language code.
- * @param $mode
- *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
- *   LOCALE_IMPORT_OVERWRITE.
+ * @param $overwrite_options
+ *   An associative array indicating what data should be overwritten, if any.
+ *   - not_customized: strings marked not customized should be overwritten.
+ *   - customized: strings marked customized should be overwritten.
+ * @param $customized
+ *   Whether the strings being imported should be saved as customized.
+ *   Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. All strings in the file
+ *   will be saved with this customization flag.
  */
-function _locale_import_po($file, $langcode, $mode) {
+function _locale_import_po($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) {
   // Try to allocate enough time to parse and import the data.
   drupal_set_time_limit(240);
 
@@ -38,7 +43,7 @@ function _locale_import_po($file, $langcode, $mode) {
   }
 
   // Get strings from file (returns on failure after a partial import, or on success)
-  $status = _locale_import_read_po('db-store', $file, $mode, $langcode);
+  $status = _locale_import_read_po('db-store', $file, $overwrite_options, $langcode, $customized);
   if ($status === FALSE) {
     // Error messages are set in _locale_import_read_po().
     return FALSE;
@@ -80,13 +85,17 @@ function _locale_import_po($file, $langcode, $mode) {
  *   Storage operation type: db-store or mem-store.
  * @param $file
  *   Drupal file object corresponding to the PO file to import.
- * @param $mode
- *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
- *   LOCALE_IMPORT_OVERWRITE.
+ * @param $overwrite_options
+ *   An associative array indicating what data should be overwritten, if any.
+ *   - not_customized: not customized strings should be overwritten.
+ *   - customized: customized strings should be overwritten.
  * @param $lang
  *   Language code.
+ * @param $customized
+ *   Whether the strings being imported should be saved as customized.
+ *   Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
  */
-function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
+function _locale_import_read_po($op, $file, $overwrite_options = NULL, $lang = NULL, $customized = LOCALE_NOT_CUSTOMIZED) {
 
   // The file will get closed by PHP on returning from this function.
   $fd = fopen($file->uri, 'rb');
@@ -138,7 +147,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
       }
       elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
         // We are currently in string token, close it out.
-        _locale_import_one_string($op, $current, $mode, $lang, $file);
+        _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
 
         // Start a new entry for the comment.
         $current         = array();
@@ -182,7 +191,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
 
       if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
         // We are currently in a message string, close it out.
-        _locale_import_one_string($op, $current, $mode, $lang, $file);
+        _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
 
         // Start a new context for the id.
         $current = array();
@@ -212,7 +221,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
 
       if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
         // We are currently in a message, start a new one.
-        _locale_import_one_string($op, $current, $mode, $lang, $file);
+        _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
         $current = array();
       }
       elseif (!empty($current['msgctxt'])) {
@@ -326,7 +335,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
 
   // End of PO file, closed out the last entry.
   if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
-    _locale_import_one_string($op, $current, $mode, $lang, $file);
+    _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
   }
   elseif ($context != 'COMMENT') {
     _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
@@ -360,16 +369,20 @@ function _locale_import_message($message, $file, $lineno = NULL) {
  *   Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'.
  * @param $value
  *   Details of the string stored.
- * @param $mode
- *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
- *   LOCALE_IMPORT_OVERWRITE.
+ * @param $overwrite_options
+ *   An associative array indicating what data should be overwritten, if any.
+ *   - not_customized: not customized strings should be overwritten.
+ *   - customized: customized strings should be overwritten.
  * @param $lang
  *   Language to store the string in.
  * @param $file
  *   Object representation of file being imported, only required when op is
  *   'db-store'.
+ * @param $customized
+ *   Whether the strings being imported should be saved as customized.
+ *   Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
  */
-function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL) {
+function _locale_import_one_string($op, $value = NULL, $overwrite_options = NULL, $lang = NULL, $file = NULL, $customized = LOCALE_NOT_CUSTOMIZED) {
   $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
   $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
   $strings = &drupal_static(__FUNCTION__ . ':strings', array());
@@ -395,7 +408,7 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
         // If 'msgid' is empty, it means we got values for the header of the
         // file as per the structure of the Gettext format.
         $locale_plurals = variable_get('locale_translation_plurals', array());
-        if (($mode != LOCALE_IMPORT_KEEP) || empty($locale_plurals[$lang]['plurals'])) {
+        if (array_sum($overwrite_options) || empty($locale_plurals[$lang]['plurals'])) {
           // Since we only need to parse the header if we ought to update the
           // plural formula, only run this if we don't need to keep existing
           // data untouched or if we don't have an existing plural formula.
@@ -432,7 +445,8 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
           $value['msgid'],
           $value['msgstr'],
           $comments,
-          $mode
+          $overwrite_options,
+          $customized
         );
       }
   } // end of db-store operation
@@ -454,57 +468,76 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
  *   Translation to language specified in $langcode.
  * @param $location
  *   Location value to save with source string.
- * @param $mode
- *   Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
+ * @param $overwrite_options
+ *   An associative array indicating what data should be overwritten, if any.
+ *   - not_customized: not customized strings should be overwritten.
+ *   - customized: customized strings should be overwritten.
+ * @param $customized
+ *   (optional) Whether the strings being imported should be saved as customized.
+ *   Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
  *
  * @return
  *   The string ID of the existing string modified or the new string added.
  */
-function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $mode) {
-  $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $source, ':context' => $context))->fetchField();
+function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) {
+
+  // Initialize overwrite options if not set.
+  $overwrite_options += array(
+    'not_customized' => FALSE,
+    'customized' => FALSE,
+  );
+
+  // Look up the source string and any existing translation.
+  $string = db_query("SELECT s.lid, t.customized FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context", array(
+    ':source' => $source,
+    ':context' => $context,
+    ':language' => $langcode,
+    ))
+    ->fetchObject();
 
   if (!empty($translation)) {
     // Skip this string unless it passes a check for dangerous code.
     if (!locale_string_is_safe($translation)) {
       watchdog('locale', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation), WATCHDOG_ERROR);
       $report['skips']++;
-      $lid = 0;
+      return 0;
     }
-    elseif ($lid) {
+    elseif (isset($string->lid)) {
       // We have this source string saved already.
       db_update('locales_source')
         ->fields(array(
           'location' => $location,
         ))
-        ->condition('lid', $lid)
+        ->condition('lid', $string->lid)
         ->execute();
 
-      $exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField();
-
-      if (!$exists) {
+      if (!isset($string->customized)) {
         // No translation in this language.
         db_insert('locales_target')
           ->fields(array(
-            'lid' => $lid,
+            'lid' => $string->lid,
             'language' => $langcode,
             'translation' => $translation,
+            'customized' => $customized,
           ))
           ->execute();
 
         $report['additions']++;
       }
-      elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
+      elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
         // Translation exists, only overwrite if instructed.
         db_update('locales_target')
           ->fields(array(
             'translation' => $translation,
+            'customized' => $customized,
           ))
           ->condition('language', $langcode)
-          ->condition('lid', $lid)
+          ->condition('lid', $string->lid)
           ->execute();
 
         $report['updates']++;
       }
+      return $string->lid;
     }
     else {
       // No such source string in the database yet.
@@ -521,23 +554,24 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
            'lid' => $lid,
            'language' => $langcode,
            'translation' => $translation,
+           'customized' => $customized,
         ))
         ->execute();
 
       $report['additions']++;
+      return $lid;
     }
   }
-  elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
+  elseif (isset($string->lid) && isset($string->customized) && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
     // Empty translation, remove existing if instructed.
     db_delete('locales_target')
       ->condition('language', $langcode)
-      ->condition('lid', $lid)
+      ->condition('lid', $string->lid)
       ->execute();
 
     $report['deletes']++;
+    return $string->lid;
   }
-
-  return $lid;
 }
 
 /**
@@ -828,17 +862,67 @@ function _locale_import_parse_quoted($string) {
  * @param $language
  *   Language object to generate the output for, or NULL if generating
  *   translation template.
+ * @param $options
+ *   (optional) An associative array specifying what to include in the output:
+ *   - customized: include customized strings (if TRUE)
+ *   - uncustomized: include non-customized string (if TRUE)
+ *   - untranslated: include untranslated source strings (if TRUE)
+ *   Ignored if $language is NULL.
  *
  * @return
  *   An array of translated strings that can be used to generate an export.
  */
-function _locale_export_get_strings($language = NULL) {
-  if (isset($language)) {
-    $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language", array(':language' => $language->langcode));
+function _locale_export_get_strings($language = NULL, $options = array()) {
+
+  // Assume FALSE for all options if not provided by the API.
+  $options += array(
+    'customized' => FALSE,
+    'not_customized' => FALSE,
+    'not_translated' => FALSE,
+  );
+  if (array_sum($options) == 0) {
+    // If user asked to not include anything in the translation files,
+    // that would not make sense, so just fall back on providing a template.
+    $language = NULL;
+  }
+
+  // Build and execute query to collect source strings and translations.
+  $query = db_select('locales_source', 's');
+  if (!empty($language)) {
+    if ($options['not_translated']) {
+      // Left join to keep untranslated strings in.
+      $query->leftJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode));
+    }
+    else {
+      // Inner join to filter for only translations.
+      $query->innerJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode));
+    }
+    if ($options['customized']) {
+      if (!$options['not_customized']) {
+        // Filter for customized strings only.
+        $query->condition('t.customized', LOCALE_CUSTOMIZED);
+      }
+      // Else no filtering needed in this case.
+    }
+    else {
+      if ($options['not_customized']) {
+        // Filter for non-customized strings only.
+        $query->condition('t.customized', LOCALE_NOT_CUSTOMIZED);
+      }
+      else {
+        // Filter for strings without translation.
+        $query->isNull('t.translation');
+      }
+    }
+    $query->fields('t', array('translation'));
   }
   else {
-    $result = db_query("SELECT s.lid, s.source, s.context, s.location FROM {locales_source} s");
+    $query->leftJoin('locales_target', 't', 's.lid = t.lid');
   }
+  $query->fields('s', array('lid', 'source', 'context', 'location'));
+  $result = $query->execute();
+
+  // Structure results in an array with metainformation on the strings.
   $strings = array();
   foreach ($result as $child) {
     $strings[$child->lid] = array(
diff --git a/core/includes/locale.inc b/core/includes/locale.inc
index 1f9567b..45e4a95 100644
--- a/core/includes/locale.inc
+++ b/core/includes/locale.inc
@@ -73,26 +73,28 @@ define('LOCALE_JS_OBJECT_CONTEXT', '
 ');
 
 /**
- * Translation import mode overwriting all existing translations
- * if new translated version available.
+ * Flag for locally not customized interface translation.
+ *
+ * Such translations are imported from .po files downloaded from
+ * localize.drupal.org for example.
  */
-const LOCALE_IMPORT_OVERWRITE = 0;
+const LOCALE_NOT_CUSTOMIZED = 0;
 
 /**
- * Translation import mode keeping existing translations and only
- * inserting new strings.
+ * Flag for locally customized interface translation.
+ *
+ * Such translations are edited from their imported originals on the user
+ * interface or are imported as customized.
  */
-const LOCALE_IMPORT_KEEP = 1;
+const LOCALE_CUSTOMIZED = 1;
 
 /**
- * URL language negotiation: use the path prefix as URL language
- * indicator.
+ * URL language negotiation: use the path prefix as URL language indicator.
  */
 const LANGUAGE_NEGOTIATION_URL_PREFIX = 0;
 
 /**
- * URL language negotiation: use the domain as URL language
- * indicator.
+ * URL language negotiation: use the domain as URL language indicator.
  */
 const LANGUAGE_NEGOTIATION_URL_DOMAIN = 1;
 
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index a4982e1..f733ce7 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -28,68 +28,104 @@ function locale_translate_import_form($form, &$form_state) {
   // the list of existing and then predefined languages.
   form_load_include($form_state, 'inc', 'language', 'language.admin');
   if (empty($existing_languages)) {
-    $language_options = language_admin_predefined_list();
-    $default = key($language_options);
+    $language_options = array_merge(
+      array('' => t('- Select a language -')),
+      language_admin_predefined_list());
+    $default = '';
+    $required = TRUE;
+    $desc = t('The language will be added automatically.');
   }
   else {
     $default = key($existing_languages);
     $language_options = array(
-      t('Already added languages') => $existing_languages,
+      t('Existing languages') => $existing_languages,
       t('Languages not yet added') => language_admin_predefined_list()
     );
+    // Don't set 'required' mark if it doensn't matter anyway.
+    $required = FALSE;
+    $desc = '';
   }
 
-  $form['import'] = array('#type' => 'fieldset',
-    '#title' => t('Import translation'),
-  );
-  $form['import']['file'] = array('#type' => 'file',
-    '#title' => t('Language file'),
+  // Check for existing translations; the overwrite_optinos are unnecessary
+  // if they don't exist. This is unlikely (and the check is imperfect since
+  // we do not check for specific languages) but may still help ease confusion
+  // on new sites.
+  $existing_c = (bool) db_query_range('SELECT 1 FROM {locales_target} WHERE customized = 1', 0, 1)->fetchField();
+  $existing_nc = (bool) db_query_range('SELECT 1 FROM {locales_target} WHERE customized = 0', 0, 1)->fetchField();
+
+  $form['file'] = array(
+    '#type' => 'file',
+    '#title' => t('Translation file'),
     '#size' => 50,
     '#description' => t('A Gettext Portable Object (<em>.po</em>) file.'),
   );
-  $form['import']['langcode'] = array('#type' => 'select',
-    '#title' => t('Import into'),
+  $form['langcode'] = array(
+    '#type' => 'select',
+    '#title' => t('Language'),
     '#options' => $language_options,
     '#default_value' => $default,
-    '#description' => t('Choose the language you want to add strings into. If you choose a language which is not yet set up, it will be added.'),
+    '#required' => $required,
+    '#description' => $desc,
+  );
+
+  $form['customized'] = array(
+    '#title' => t('Treat imported strings as customized translations'),
+    '#type' => 'checkbox',
   );
-  $form['import']['mode'] = array('#type' => 'radios',
-    '#title' => t('Mode'),
-    '#default_value' => LOCALE_IMPORT_KEEP,
-    '#options' => array(
-      LOCALE_IMPORT_OVERWRITE => t('Strings in the uploaded file replace existing ones, new ones are added. The plural format is updated.'),
-      LOCALE_IMPORT_KEEP => t('Existing strings and the plural format are kept, only new strings are added.')
+  $form['overwrite_options'] = array(
+    '#type' => 'container',
+    '#tree' => TRUE,
+  );
+  $form['overwrite_options']['not_customized'] = array(
+    '#title' => t('Overwrite existing non-customized translations'),
+    '#type' => 'checkbox',
+    '#states' => array(
+      'checked' => array(
+         ':input[name="customized"]' => array('checked' => TRUE),
+      ),
     ),
+    '#access' => $existing_nc,
+  );
+  $form['overwrite_options']['customized'] = array(
+    '#title' => t('Overwrite existing customized translations'),
+    '#type' => 'checkbox',
+    '#access' => $existing_c,
   );
-  $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import'));
 
+  $form['actions'] = array(
+    '#type' => 'actions'
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Import')
+  );
   return $form;
 }
 
 /**
- * Process the locale import form submission.
+ * Processes the locale import form submission.
  */
 function locale_translate_import_form_submit($form, &$form_state) {
   $validators = array('file_validate_extensions' => array('po'));
-  // Ensure we have the file uploaded
+
+  // Ensure we have the file uploaded.
   if ($file = file_save_upload('file', $validators)) {
 
-    // Add language, if not yet supported
-    drupal_static_reset('language_list');
-    $languages = language_list();
-    $langcode = $form_state['values']['langcode'];
-    if (!isset($languages[$langcode])) {
+    // Add language, if not yet supported.
+    $language = language_load($form_state['values']['langcode']);
+    if (empty($language)) {
       include_once DRUPAL_ROOT . '/core/includes/standard.inc';
       $predefined = standard_language_list();
       $language = (object) array(
-        'langcode' => $langcode,
+        'langcode' => $form_state['values']['langcode'],
       );
-      language_save($language);
-      drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0]))));
+      $language = language_save($language);
+      drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name))));
     }
+    $customized = $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED;
 
     // Now import strings into the language
-    if ($return = _locale_import_po($file, $langcode, $form_state['values']['mode']) == FALSE) {
+    if ($return = _locale_import_po($file, $language->langcode, $form_state['values']['overwrite_options'], $customized) == FALSE) {
       $variables = array('%filename' => $file->filename);
       drupal_set_message(t('The translation import of %filename failed.', $variables), 'error');
       watchdog('locale', 'The translation import of %filename failed.', $variables, WATCHDOG_ERROR);
@@ -106,11 +142,9 @@ function locale_translate_import_form_submit($form, &$form_state) {
 }
 
 /**
- * User interface for the translation export screen.
+ * Builds form to export Gettext translation files.
  */
-function locale_translate_export_screen() {
-  // Get all enabled languages, except English, if we should not translate that.
-  drupal_static_reset('language_list');
+function locale_translate_export_form($form, &$form_state) {
   $languages = language_list(TRUE);
   $language_options = array();
   foreach ($languages as $langcode => $language) {
@@ -118,65 +152,80 @@ function locale_translate_export_screen() {
       $language_options[$langcode] = $language->name;
     }
   }
+  $language_default = language_default();
 
-  $output = '';
-  // Offer translation export if any language is set up.
-  if (!empty($language_options)) {
-    $elements = drupal_get_form('locale_translate_export_po_form', $language_options);
-    $output = drupal_render($elements);
+  if (empty($language_options)) {
+    $form['langcode'] = array(
+      '#type' => 'value',
+      '#value' => LANGUAGE_SYSTEM,
+    );
+    $form['langcode_text'] = array(
+      '#type' => 'item',
+      '#title' => t('Language'),
+      '#markup' => t('No language available. The export will only contain source strings.'),
+    );
+  }
+  else {
+    $form['langcode'] = array(
+      '#type' => 'select',
+      '#title' => t('Language'),
+      '#options' => $language_options,
+      '#default_value' => $language_default->langcode,
+      '#empty_option' => t('Source text only, no translations'),
+      '#empty_value' => LANGUAGE_SYSTEM,
+    );
+    $form['content_options'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Export options'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#tree' => TRUE,
+      '#states' => array(
+        'invisible' => array(
+           ':input[name="langcode"]' => array('value' => LANGUAGE_SYSTEM),
+        ),
+      ),
+    );
+    $form['content_options']['not_customized'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Include non-customized translations'),
+      '#default_value' => TRUE,
+    );
+    $form['content_options']['customized'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Include customized translations'),
+      '#default_value' => TRUE,
+    );
+    $form['content_options']['not_translated'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Include untranslated text'),
+      '#default_value' => TRUE,
+    );
   }
-  $elements = drupal_get_form('locale_translate_export_pot_form');
-  $output .= drupal_render($elements);
-  return $output;
-}
 
-/**
- * Form to export PO files for the languages provided.
- *
- * @param $names
- *   An associate array with localized language names
- */
-function locale_translate_export_po_form($form, &$form_state, $names) {
-  $form['export_title'] = array('#type' => 'item',
-    '#title' => t('Export translation'),
+  $form['actions'] = array(
+    '#type' => 'actions'
   );
-  $form['langcode'] = array('#type' => 'select',
-    '#title' => t('Language name'),
-    '#options' => $names,
-    '#description' => t('Select the language to export in Gettext Portable Object (<em>.po</em>) format.'),
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Export')
   );
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
   return $form;
 }
 
 /**
- * Translation template export form.
+ * Processes a translation (or template) export form submission.
  */
-function locale_translate_export_pot_form() {
-  // Complete template export of the strings
-  $form['export_title'] = array('#type' => 'item',
-    '#title' => t('Export template'),
-    '#description' => t('Generate a Gettext Portable Object Template (<em>.pot</em>) file with all strings from the Drupal locale database.'),
-  );
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
-  // Reuse PO export submission callback.
-  $form['#submit'][] = 'locale_translate_export_po_form_submit';
-  return $form;
-}
-
-/**
- * Process a translation (or template) export form submission.
- */
-function locale_translate_export_po_form_submit($form, &$form_state) {
+function locale_translate_export_form_submit($form, &$form_state) {
   // If template is required, language code is not given.
-  $language = NULL;
-  if (isset($form_state['values']['langcode'])) {
-    $languages = language_list();
-    $language = $languages[$form_state['values']['langcode']];
+  if ($form_state['values']['langcode'] != LANGUAGE_SYSTEM) {
+    $language = language_load($form_state['values']['langcode']);
+  }
+  else {
+    $language = NULL;
   }
-  _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language)));
+  $content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
+  _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $content_options)));
 }
 
 /**
@@ -267,7 +316,7 @@ function locale_translate_batch_import($filepath, &$context) {
   // we can extract the language code to use for the import from the end.
   if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) {
     $file = (object) array('filename' => drupal_basename($filepath), 'uri' => $filepath);
-    _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]);
+    _locale_import_read_po('db-store', $file, array(), $langcode[2]);
     $context['results'][] = $filepath;
   }
 }
diff --git a/core/modules/locale/locale.css b/core/modules/locale/locale.css
index 66de82b..96f08b9 100644
--- a/core/modules/locale/locale.css
+++ b/core/modules/locale/locale.css
@@ -6,7 +6,7 @@
 
 #locale-translation-filter-form .form-item-language,
 #locale-translation-filter-form .form-item-translation,
-#locale-translation-filter-form .form-item-group {
+#locale-translation-filter-form .form-item-customized {
   float: left; /* LTR */
   padding-right: .8em; /* LTR */
   margin: 0.1em;
diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install
index acf3a80..294fa4d 100644
--- a/core/modules/locale/locale.install
+++ b/core/modules/locale/locale.install
@@ -176,6 +176,12 @@ function locale_schema() {
         'default' => '',
         'description' => 'Language code. References {language}.langcode.',
       ),
+      'customized' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0, // LOCALE_NOT_CUSTOMIZED
+        'description' => 'Boolean indicating whether the translation is custom to this site.',
+      ),
     ),
     'primary key' => array('language', 'lid'),
     'foreign keys' => array(
@@ -485,6 +491,19 @@ function locale_update_8005() {
 }
 
 /**
+ * Add column to track customized string status to locales_target.
+ */
+function locale_update_8006() {
+  $spec = array(
+    'type' => 'int',
+    'not null' => TRUE,
+    'default' => 0, // LOCALE_NOT_CUSTOMIZED
+    'description' => 'Boolean indicating whether the translation is custom to this site.',
+  );
+  db_add_field('locales_target', 'customized', $spec);
+}
+
+/**
  * @} 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 6d3ded0..4b00e6c 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -124,7 +124,8 @@ function locale_menu() {
   );
   $items['admin/config/regional/translate/export'] = array(
     'title' => 'Export',
-    'page callback' => 'locale_translate_export_screen',  // possibly multiple forms concatenated
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('locale_translate_export_form'),
     'access arguments' => array('translate interface'),
     'weight' => 30,
     'type' => MENU_LOCAL_TASK,
diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc
index 3df2832..d7e9f78 100644
--- a/core/modules/locale/locale.pages.inc
+++ b/core/modules/locale/locale.pages.inc
@@ -29,6 +29,7 @@ function _locale_translate_seek() {
     $query = array(
       'translation' => 'all',
       'language' => 'all',
+      'customized' => 'all',
       'string' => '',
     );
   }
@@ -36,13 +37,16 @@ function _locale_translate_seek() {
   $sql_query = db_select('locales_source', 's');
   $sql_query->leftJoin('locales_target', 't', 't.lid = s.lid');
   $sql_query->fields('s', array('source', 'location', 'context', 'lid'));
-  $sql_query->fields('t', array('translation', 'language'));
+  $sql_query->fields('t', array('translation', 'language', 'customized'));
 
   // Compute LIKE section.
   switch ($query['translation']) {
     case 'translated':
       $sql_query->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE');
       $sql_query->orderBy('t.translation', 'DESC');
+      if ($query['customized'] != 'all') {
+        $sql_query->condition('t.customized', $query['customized']);
+      }
       break;
     case 'untranslated':
       $sql_query->condition(db_and()
@@ -134,7 +138,7 @@ function _locale_translate_seek_query() {
   $query = &drupal_static(__FUNCTION__);
   if (!isset($query)) {
     $query = array();
-    $fields = array('string', 'language', 'translation');
+    $fields = array('string', 'language', 'translation', 'customized');
     foreach ($fields as $field) {
       if (isset($_SESSION['locale_translation_filter'][$field])) {
         $query[$field] = $_SESSION['locale_translation_filter'][$field];
@@ -172,7 +176,25 @@ function locale_translation_filters() {
 
   $filters['translation'] = array(
     'title' => t('Search in'),
-    'options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
+    'options' => array(
+      'all' => t('Both translated and untranslated strings'),
+      'translated' => t('Only translated strings'),
+      'untranslated' => t('Untranslated strings')
+    ),
+  );
+
+  $filters['customized'] = array(
+    'title' => t('Translation type'),
+    'options' => array(
+      'all' => t('All'),
+      LOCALE_NOT_CUSTOMIZED => t('Base translation'),
+      LOCALE_CUSTOMIZED => t('Customized translation'),
+    ),
+    'states' => array(
+      'visible' => array(
+        ':input[name=translation]' => array('value' => 'translated'),
+      )
+    ),
   );
 
   return $filters;
@@ -210,6 +232,9 @@ function locale_translation_filter_form() {
         '#size' => 0,
         '#options' => $filter['options'],
       );
+      if (isset($filter['states'])) {
+        $form['filters']['status'][$key]['#states'] = $filter['states'];
+      }
     }
     if (!empty($_SESSION['locale_translation_filter'][$key])) {
       $form['filters']['status'][$key]['#default_value'] = $_SESSION['locale_translation_filter'][$key];
@@ -435,21 +460,23 @@ function locale_translate_edit_form_submit($form, &$form_state) {
     }
     if ($has_translation) {
       // Only update or insert if we have a value to use.
-      if (!empty($translation)) {
+      if (!empty($translation) && $translation != $value) {
         db_update('locales_target')
           ->fields(array(
             'translation' => $value,
+            'customized' => LOCALE_CUSTOMIZED,
           ))
           ->condition('lid', $lid)
           ->condition('language', $langcode)
           ->execute();
       }
-      else {
+      if (empty($translation)) {
         db_insert('locales_target')
           ->fields(array(
             'lid' => $lid,
             'translation' => $value,
             'language' => $langcode,
+            'customized' => LOCALE_CUSTOMIZED,
           ))
           ->execute();
       }
diff --git a/core/modules/locale/locale.test b/core/modules/locale/locale.test
index f2395ad..f3c9969 100644
--- a/core/modules/locale/locale.test
+++ b/core/modules/locale/locale.test
@@ -621,9 +621,11 @@ class LocalePluralFormatTest extends DrupalWebTestCase {
     // will not overwrite the proper plural formula imported above.
     $this->importPoFile($this->getPoFileWithMissingPlural(), array(
       'langcode' => 'fr',
+      'overwrite_options[not_customized]' => TRUE,
     ));
     $this->importPoFile($this->getPoFileWithBrokenPlural(), array(
       'langcode' => 'hr',
+      'overwrite_options[not_customized]' => TRUE,
     ));
 
     // Reset static caches from locale_get_plural() to ensure we get fresh data.
@@ -809,7 +811,6 @@ class LocalePluralFormatTest extends DrupalWebTestCase {
     $name = tempnam('temporary://', "po_") . '.po';
     file_put_contents($name, $contents);
     $options['files[file]'] = $name;
-    $options['mode'] = LOCALE_IMPORT_OVERWRITE;
     $this->drupalPost('admin/config/regional/translate/import', $options, t('Import'));
     drupal_unlink($name);
   }
@@ -821,7 +822,7 @@ class LocalePluralFormatTest extends DrupalWebTestCase {
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -844,7 +845,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -868,7 +869,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -885,7 +886,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -972,7 +973,6 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase {
     // strings are kept.
     $this->importPoFile($this->getOverwritePoFile(), array(
       'langcode' => 'fr',
-      'mode' => 1, // Existing strings are kept, only new strings are added.
     ));
 
     // The import should have created 1 string.
@@ -994,7 +994,7 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase {
     // strings are overwritten.
     $this->importPoFile($this->getOverwritePoFile(), array(
       'langcode' => 'fr',
-      'mode' => 0, // Strings in the uploaded file replace existing ones, new ones are added.
+      'overwrite_options[not_customized]' => TRUE,
     ));
 
     // The import should have updated 2 strings.
@@ -1010,6 +1010,59 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase {
     // This import should have changed number of plural forms.
     $locale_plurals = variable_get('locale_translation_plurals', array());
     $this->assert($locale_plurals['fr']['plurals'] == 3, t('Plural numbers changed.'));
+
+    // Importing a .po file and mark its strings as customized strings.
+    $this->importPoFile($this->getCustomPoFile(), array(
+      'langcode' => 'fr',
+      'customized' => TRUE,
+    ));
+
+    // 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.'));
+
+    // The database should now contain 6 customized strings (two imported
+    // strings are not translated).
+    $count = db_query('SELECT lid FROM {locales_target} WHERE customized = :custom', array(':custom' => 1))->rowCount();
+    $this->assertEqual($count, 6, t('Customized translations succesfully imported.'));
+
+    // Try importing a .po file with overriding strings, and ensure existing
+    // customized strings are kept.
+    $this->importPoFile($this->getCustomOverwritePoFile(), array(
+      'langcode' => 'fr',
+      'overwrite_options[not_customized]' => TRUE,
+      'overwrite_options[customized]' => FALSE,
+    ));
+
+    // 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.'));
+    // Ensure string wasn't overwritten.
+    $search = array(
+      'string' => 'januari',
+      'language' => 'fr',
+      'translation' => 'translated',
+    );
+    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+    $this->assertText(t('No strings available.'), t('Customized string not overwritten by imported string.'));
+
+    // Try importing a .po file with overriding strings, and ensure existing
+    // customized strings are overwritten.
+    $this->importPoFile($this->getCustomOverwritePoFile(), array(
+      'langcode' => 'fr',
+      'overwrite_options[not_customized]' => FALSE,
+      'overwrite_options[customized]' => TRUE,
+    ));
+
+    // 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.'));
+    // Ensure string was overwritten.
+    $search = array(
+      'string' => 'januari',
+      'language' => 'fr',
+      'translation' => 'translated',
+    );
+    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
+    $this->assertNoText(t('No strings available.'), t('Customized string overwritten by imported string.'));
+
   }
 
   /**
@@ -1074,7 +1127,7 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase {
     // Try importing a .po file.
     $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array(
       'langcode' => $langcode,
-      'mode' => 0,
+      '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 is the language indicator on the translation search screen for
@@ -1116,7 +1169,7 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase {
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -1157,7 +1210,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -1182,7 +1235,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -1197,6 +1250,66 @@ EOF;
   }
 
   /**
+   * Helper function that returns a .po file which strings will be marked
+   * as customized.
+   */
+  function getCustomPoFile() {
+    return <<< 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"
+
+msgid "One dog"
+msgid_plural "@count dogs"
+msgstr[0] "un chien"
+msgstr[1] "@count chiens"
+
+msgid "January"
+msgstr "janvier"
+
+msgid "February"
+msgstr "février"
+
+msgid "March"
+msgstr "mars"
+
+msgid "April"
+msgstr "avril"
+
+msgid "June"
+msgstr "juin"
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file for testing customized strings.
+   */
+  function getCustomOverwritePoFile() {
+    return <<< 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"
+
+msgid "January"
+msgstr "januari"
+
+msgid "February"
+msgstr "februari"
+
+msgid "July"
+msgstr "juillet"
+EOF;
+  }
+
+  /**
    * Helper function that returns a .po file with context.
    */
   function getPoFileWithContext() {
@@ -1205,7 +1318,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -1227,7 +1340,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -1245,7 +1358,7 @@ EOF;
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 7\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -1309,6 +1422,50 @@ class LocaleExportFunctionalTest extends DrupalWebTestCase {
     $this->assertRaw('# French translation of Drupal', t('Exported French translation file.'));
     // Ensure our imported translations exist in the file.
     $this->assertRaw('msgstr "lundi"', t('French translations present in exported file.'));
+
+    // Import some more French translations which will be marked as customized.
+    $name = tempnam('temporary://', "po2_") . '.po';
+    file_put_contents($name, $this->getCustomPoFile());
+    $this->drupalPost('admin/config/regional/translate/import', array(
+      'langcode' => 'fr',
+      'files[file]' => $name,
+      'customized' => 1,
+    ), t('Import'));
+    drupal_unlink($name);
+
+    // We can't import a string with an empty translation, but calling
+    // locale() for an new string creates an entry in the locales_source table.
+    locale('February', NULL, 'fr');
+
+    // Export only customized French translations.
+    $this->drupalPost('admin/config/regional/translate/export', array(
+      'langcode' => 'fr',
+      'content_options[not_customized]' => FALSE,
+      'content_options[customized]' => TRUE,
+      'content_options[not_translated]' => FALSE,
+    ), t('Export'));
+
+    // Ensure we have a translation file.
+    $this->assertRaw('# French translation of Drupal', t('Exported French translation file with only customized strings.'));
+    // Ensure the customized translations exist in the file.
+    $this->assertRaw('msgstr "janvier"', t('French custom translation present in exported file.'));
+    // Ensure no untranslated strings exist in the file.
+    $this->assertNoRaw('msgid "February"', t('Untranslated string not present in exported file.'));
+
+    // Export only untranslated French translations.
+    $this->drupalPost('admin/config/regional/translate/export', array(
+      'langcode' => 'fr',
+      'content_options[not_customized]' => FALSE,
+      'content_options[customized]' => FALSE,
+      'content_options[not_translated]' => TRUE,
+    ), t('Export'));
+
+    // Ensure we have a translation file.
+    $this->assertRaw('# French translation of Drupal', t('Exported French translation file with only untranslated strings.'));
+    // Ensure no customized translations exist in the file.
+    $this->assertNoRaw('msgstr "janvier"', t('French custom translation not present in exported file.'));
+    // Ensure the untranslated strings exist in the file.
+    $this->assertRaw('msgid "February"', t('Untranslated string present in exported file.'));
   }
 
   /**
@@ -1316,9 +1473,6 @@ class LocaleExportFunctionalTest extends DrupalWebTestCase {
    */
   function testExportTranslationTemplateFile() {
     // Get the translation template file.
-    // There are two 'Export' buttons on this page, but it somehow works.  It'd
-    // be better if we could use the submit button id like documented but that
-    // doesn't work.
     $this->drupalPost('admin/config/regional/translate/export', array(), t('Export'));
     // Ensure we have a translation file.
     $this->assertRaw('# LANGUAGE translation of PROJECT', t('Exported translation template file.'));
@@ -1331,7 +1485,7 @@ class LocaleExportFunctionalTest extends DrupalWebTestCase {
     return <<< EOF
 msgid ""
 msgstr ""
-"Project-Id-Version: Drupal 6\\n"
+"Project-Id-Version: Drupal 8\\n"
 "MIME-Version: 1.0\\n"
 "Content-Type: text/plain; charset=UTF-8\\n"
 "Content-Transfer-Encoding: 8bit\\n"
@@ -1342,6 +1496,25 @@ msgstr "lundi"
 EOF;
   }
 
+  /**
+   * Helper function that returns a .po file which strings will be marked
+   * as customized.
+   */
+  function getCustomPoFile() {
+    return <<< 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"
+
+msgid "January"
+msgstr "janvier"
+EOF;
+  }
+
 }
 
 /**
