diff --git a/core/CHANGELOG.txt b/core/CHANGELOG.txt
index e48669b553064c3a02b641c89375c8d5bd7448e0..c5a8b5da0864c597a3611eac9d0d470d09e0cc75 100644
--- a/core/CHANGELOG.txt
+++ b/core/CHANGELOG.txt
@@ -31,6 +31,7 @@ Drupal 8.0, xxxx-xx-xx (development version)
       * Made interface translation directly accessible from language list.
       * Centralized interface translation import to one directory.
       * Drupal can now be translated to English and English can be deleted.
+      * Added support for singular/plural discovery and translation.
     * Improved content language support:
       * Freely orderable language selector in forms.
       * Made it possible to assign language to taxonomy terms, vocabularies
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 240f644a154ebca2b0ef5bc3e37ac7bfdaa26040..327ffbd27a53c4dff3cfffd775beb50923a57c6e 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -162,6 +162,15 @@
  */
 const DRUPAL_CACHE_GLOBAL = 0x0008;
 
+/**
+ * The delimiter used to split plural strings.
+ *
+ * This is the ETX (End of text) character and is used as a minimal means to
+ * separate singular and plural variants in source and translation text. It
+ * was found to be the most compatible delimiter for the supported databases.
+ */
+const LOCALE_PLURAL_DELIMITER = "\03";
+
 /**
  * Adds content to a specified region.
  *
@@ -1719,27 +1728,34 @@ function format_xml_elements($array) {
  */
 function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
   $args['@count'] = $count;
+  // Join both forms to search a translation.
+  $tranlatable_string = implode(LOCALE_PLURAL_DELIMITER, array($singular, $plural));
+  // Translate as usual.
+  $translated_strings = t($tranlatable_string, $args, $options);
+  // Split joined translation strings into array.
+  $translated_array = explode(LOCALE_PLURAL_DELIMITER, $translated_strings);
+
   if ($count == 1) {
-    return t($singular, $args, $options);
+    return $translated_array[0];
   }
 
   // Get the plural index through the gettext formula.
+  // @todo implement static variable to minimize function_exists() usage.
   $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
-  // If the index cannot be computed, use the plural as a fallback (which
-  // allows for most flexiblity with the replaceable @count value).
-  if ($index < 0) {
-    return t($plural, $args, $options);
+  if ($index == 0) {
+    // Singular form.
+    return $translated_array[0];
   }
   else {
-    switch ($index) {
-      case "0":
-        return t($singular, $args, $options);
-      case "1":
-        return t($plural, $args, $options);
-      default:
-        unset($args['@count']);
-        $args['@count[' . $index . ']'] = $count;
-        return t(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options);
+    if (isset($translated_array[$index])) {
+      // N-th plural form.
+      return $translated_array[$index];
+    }
+    else {
+      // If the index cannot be computed or there's no translation, use
+      // the second plural form as a fallback (which allows for most flexiblity
+      // with the replaceable @count value).
+      return $translated_array[1];
     }
   }
 }
diff --git a/core/includes/gettext.inc b/core/includes/gettext.inc
index 95e84cf15275ba17c268e6f50f66e4b4708b9c94..18c27ea426eb4bfceb85582405ed9b17e9a111c1 100644
--- a/core/includes/gettext.inc
+++ b/core/includes/gettext.inc
@@ -173,7 +173,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
       }
 
       // Append the plural form to the current entry.
-      $current['msgid'] .= "\0" . $quoted;
+      $current['msgid'] .= LOCALE_PLURAL_DELIMITER . $quoted;
 
       $context = 'MSGID_PLURAL';
     }
@@ -390,8 +390,10 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
 
     // Store the string we got in the database.
     case 'db-store':
-      // We got header information.
+
       if ($value['msgid'] == '') {
+        // 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'])) {
           // Since we only need to parse the header if we ought to update the
@@ -413,32 +415,25 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
       }
 
       else {
-        // Some real string to import.
+        // Found a string to store, clean up and prepare the data.
         $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
 
-        if (strpos($value['msgid'], "\0")) {
-          // This string has plural versions.
-          $english = explode("\0", $value['msgid'], 2);
-          $entries = array_keys($value['msgstr']);
-          for ($i = 3; $i <= count($entries); $i++) {
-            $english[] = $english[1];
-          }
-          $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
-          $english = array_map('_locale_import_append_plural', $english, $entries);
-          foreach ($translation as $key => $trans) {
-            if ($key == 0) {
-              $plid = 0;
-            }
-            $plid = _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $comments, $mode, $plid, $key);
-          }
+        if (is_array($value['msgstr'])) {
+          // Sort plural variants by their form index.
+          ksort($value['msgstr']);
+          // Serialize plural variants in one string by LOCALE_PLURAL_DELIMITER.
+          $value['msgstr'] = implode(LOCALE_PLURAL_DELIMITER, $value['msgstr']);
         }
 
-        else {
-          // A simple string to import.
-          $english = $value['msgid'];
-          $translation = $value['msgstr'];
-          _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $comments, $mode);
-        }
+        _locale_import_one_string_db(
+          $report,
+          $lang,
+          isset($value['msgctxt']) ? $value['msgctxt'] : '',
+          $value['msgid'],
+          $value['msgstr'],
+          $comments,
+          $mode
+        );
       }
   } // end of db-store operation
 }
@@ -461,15 +456,11 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
  *   Location value to save with source string.
  * @param $mode
  *   Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
- * @param $plid
- *   Optional plural ID to use.
- * @param $plural
- *   Optional plural value to use.
  *
  * @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, $plid = 0, $plural = 0) {
+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();
 
   if (!empty($translation)) {
@@ -497,8 +488,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
             'lid' => $lid,
             'language' => $langcode,
             'translation' => $translation,
-            'plid' => $plid,
-            'plural' => $plural,
           ))
           ->execute();
 
@@ -509,8 +498,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
         db_update('locales_target')
           ->fields(array(
             'translation' => $translation,
-            'plid' => $plid,
-            'plural' => $plural,
           ))
           ->condition('language', $langcode)
           ->condition('lid', $lid)
@@ -534,8 +521,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
            'lid' => $lid,
            'language' => $langcode,
            'translation' => $translation,
-           'plid' => $plid,
-           'plural' => $plural
         ))
         ->execute();
 
@@ -547,8 +532,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
     db_delete('locales_target')
       ->condition('language', $langcode)
       ->condition('lid', $lid)
-      ->condition('plid', $plid)
-      ->condition('plural', $plural)
       ->execute();
 
     $report['deletes']++;
@@ -790,27 +773,6 @@ function _locale_import_tokenize_formula($formula) {
   return $tokens;
 }
 
-/**
- * Adds count indices to a string.
- *
- * Callback for array_map() within _locale_import_one_string().
- *
- * @param $entry
- *   An array element.
- * @param $key
- *   Index of the array element.
- */
-function _locale_import_append_plural($entry, $key) {
-  // No modifications for 0, 1
-  if ($key == 0 || $key == 1) {
-    return $entry;
-  }
-
-  // First remove any possibly false indices, then add new ones
-  $entry = preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
-  return preg_replace('/(@count)/', "\\1[$key]", $entry);
-}
-
 /**
  * Generates a short, one-string version of the passed comment array.
  *
@@ -872,28 +834,19 @@ function _locale_import_parse_quoted($string) {
  */
 function _locale_export_get_strings($language = NULL) {
   if (isset($language)) {
-    $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language ORDER BY t.plid, t.plural", array(':language' => $language->langcode));
+    $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));
   }
   else {
-    $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid ORDER BY t.plid, t.plural");
+    $result = db_query("SELECT s.lid, s.source, s.context, s.location FROM {locales_source} s");
   }
   $strings = array();
   foreach ($result as $child) {
-    $string = array(
+    $strings[$child->lid] = array(
       'comment'     => $child->location,
       'source'      => $child->source,
       'context'     => $child->context,
       'translation' => isset($child->translation) ? $child->translation : '',
     );
-    if ($child->plid) {
-      // Has a parent lid. Since we process in the order of plids,
-      // we already have the parent in the array, so we can add the
-      // lid to the next plural version to it. This builds a linked
-      // list of plurals.
-      $string['child'] = TRUE;
-      $strings[$child->plid]['plural'] = $child->lid;
-    }
-    $strings[$child->lid] = $string;
   }
   return $strings;
 }
@@ -933,6 +886,12 @@ function _locale_export_po_generate($language = NULL, $strings = array(), $heade
       $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
       if (!empty($locale_plurals[$language->langcode]['formula'])) {
         $header .= "\"Plural-Forms: nplurals=" . $locale_plurals[$language->langcode]['plurals'] . "; plural=" . strtr($locale_plurals[$language->langcode]['formula'], array('$' => '')) . ";\\n\"\n";
+        // Remember number of plural variants to optimize the export.
+        $nplurals = $locale_plurals[$language->langcode]['plurals'];
+      }
+      else {
+        // Remember we did not have a plural number for the export.
+        $nplurals = 0;
       }
     }
     else {
@@ -956,41 +915,38 @@ function _locale_export_po_generate($language = NULL, $strings = array(), $heade
   $output = $header . "\n";
 
   foreach ($strings as $lid => $string) {
-    // Only process non-children, children are output below their parent.
-    if (!isset($string['child'])) {
-      if ($string['comment']) {
-        $output .= '#: ' . $string['comment'] . "\n";
-      }
-      if (!empty($string['context'])) {
-        $output .= 'msgctxt ' . _locale_export_string($string['context']);
-      }
-      $output .= 'msgid ' . _locale_export_string($string['source']);
-      if (!empty($string['plural'])) {
-        $plural = $string['plural'];
-        $output .= 'msgid_plural ' . _locale_export_string($strings[$plural]['source']);
-        if (isset($language)) {
-          $translation = $string['translation'];
-          for ($i = 0; $i < $locale_plurals[$language->langcode]['plurals']; $i++) {
-            $output .= 'msgstr[' . $i . '] ' . _locale_export_string($translation);
-            if ($plural) {
-              $translation = _locale_export_remove_plural($strings[$plural]['translation']);
-              $plural = isset($strings[$plural]['plural']) ? $strings[$plural]['plural'] : 0;
-            }
-            else {
-              $translation = '';
-            }
+    if ($string['comment']) {
+      $output .= '#: ' . $string['comment'] . "\n";
+    }
+    if (!empty($string['context'])) {
+      $output .= 'msgctxt ' . _locale_export_string($string['context']);
+    }
+    if (strpos($string['source'], LOCALE_PLURAL_DELIMITER) !== FALSE) {
+      // Export plural string.
+      $export_array = explode(LOCALE_PLURAL_DELIMITER, $string['source']);
+      $output .= 'msgid ' . _locale_export_string($export_array[0]);
+      $output .= 'msgid_plural ' . _locale_export_string($export_array[1]);
+      if (isset($language)) {
+        $export_array = explode(LOCALE_PLURAL_DELIMITER, $string['translation']);
+        for ($i = 0; $i < $nplurals; $i++) {
+          if (isset($export_array[$i])) {
+            $output .= 'msgstr[' . $i . '] ' . _locale_export_string($export_array[$i]);
+          }
+          else {
+            $output .= 'msgstr[' . $i . '] ""' . "\n";
           }
-        }
-        else {
-          $output .= 'msgstr[0] ""' . "\n";
-          $output .= 'msgstr[1] ""' . "\n";
         }
       }
       else {
-        $output .= 'msgstr ' . _locale_export_string($string['translation']);
+        $output .= 'msgstr[0] ""' . "\n";
+        $output .= 'msgstr[1] ""' . "\n";
       }
-      $output .= "\n";
     }
+    else {
+      $output .= 'msgid ' . _locale_export_string($string['source']);
+      $output .= 'msgstr ' . _locale_export_string($string['translation']);
+    }
+    $output .= "\n";
   }
   return $output;
 }
@@ -1086,13 +1042,6 @@ function _locale_export_wrap($str, $len) {
   return implode("\n", $return);
 }
 
-/**
- * Removes plural index information from a string.
- */
-function _locale_export_remove_plural($entry) {
-  return preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
-}
-
 /**
  * @} End of "locale-api-import-export"
  */
diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install
index d05d24ba08568a2036b550c68af9c9f9e65e04b7..11d1873455a0e8e9b6467951f15dcf85ed1fa17f 100644
--- a/core/modules/locale/locale.install
+++ b/core/modules/locale/locale.install
@@ -176,20 +176,8 @@ function locale_schema() {
         'default' => '',
         'description' => 'Language code. References {language}.langcode.',
       ),
-      'plid' => array(
-        'type' => 'int',
-        'not null' => TRUE, // This should be NULL for no referenced string, not zero.
-        'default' => 0,
-        'description' => 'Parent lid (lid of the previous string in the plural chain) in case of plural strings. References {locales_source}.lid.',
-      ),
-      'plural' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'Plural index number in case of plural strings.',
-      ),
     ),
-    'primary key' => array('language', 'lid', 'plural'),
+    'primary key' => array('language', 'lid'),
     'foreign keys' => array(
       'locales_source' => array(
         'table' => 'locales_source',
@@ -198,8 +186,6 @@ function locale_schema() {
     ),
     'indexes' => array(
       'lid'      => array('lid'),
-      'plid'     => array('plid'),
-      'plural'   => array('plural'),
     ),
   );
 
@@ -379,6 +365,123 @@ function locale_update_8004() {
   }
 }
 
+/**
+ * Update plural interface translations to new format.
+ *
+ * See http://drupal.org/node/532512#comment-5679184 for the details of the
+ * structures handled in this update.
+ */
+function locale_update_8005() {
+  // Collect all LIDs that are sources to plural variants.
+  $results = db_query("SELECT lid, plid FROM {locales_target} WHERE plural <> 0");
+  $plural_lids = array();
+  foreach ($results as $row) {
+    // Need to collect both LID and PLID. The LID for the first (singular)
+    // string can only be retrieved from the first plural's PLID given no
+    // other indication. The last plural variant is never referenced, so we
+    // need to store the LID directly for that. We never know whether we are
+    // on the last plural though, so we always remember LID too.
+    $plural_lids[] = $row->lid;
+    $plural_lids[] = $row->plid;
+  }
+  $plural_lids = array_unique($plural_lids);
+
+  // Look up all translations for these source strings. Ordering by language
+  // will group the strings by language, the 'plid' order will get the
+  // strings in singular/plural order and 'plural' will get them in precise
+  // sequential order needed.
+  $results = db_query("SELECT s.lid, s.source, t.translation, t.plid, t.plural, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.lid IN (:lids) ORDER BY t.language, t.plid, t.plural", array(':lids' => $plural_lids));
+
+  // Collect the strings into an array and combine values as we go.
+  $strings = array();
+  $parents_to_sources = array();
+  $remove_lids = array();
+  foreach ($results as $child) {
+    $strings[$child->language][$child->lid] = array(
+      'source' => array($child->source),
+      'translation' => array($child->translation),
+    );
+
+    if (empty($child->plid)) {
+      // Non-children strings point to themselves as parents. This makes it
+      // easy to look up the utmost parents for any plurals.
+      $parents_to_sources[$child->lid] = $child->lid;
+    }
+    else {
+      // Children strings point to their utmost parents. Because we get data
+      // in PLID order, we can ensure that all previous parents have data now,
+      // so we can just copy the parent's data about their parent, etc.
+      $parents_to_sources[$child->lid] = $parents_to_sources[$child->plid];
+
+      // Append translation to the utmost parent's translation string.
+      $utmost_parent = &$strings[$child->language][$parents_to_sources[$child->plid]];
+      // Drop the Drupal-specific numbering scheme from the end of plural
+      // formulas.
+      $utmost_parent['translation'][] = str_replace('@count[' . $child->plural .']', '@count', $child->translation);
+      if (count($utmost_parent['source']) < 2) {
+        // Append source to the utmost parent's source string only if it is the
+        // plural variant. Further Drupal specific plural variants are not to be
+        // retained for source strings.
+        $utmost_parent['source'][] = $child->source;
+      }
+
+      // All plural variant LIDs are to be removed with their translations.
+      // Only the singular LIDs will be kept.
+      $remove_lids[] = $child->lid;
+    }
+  }
+
+  // Do updates for all source strings and all translations.
+  $updated_sources = array();
+  foreach ($strings as $langcode => $translations) {
+    foreach($translations as $lid => $translation) {
+      if (!in_array($lid, $updated_sources)) {
+        // Only update source string if not yet updated. We merged these within
+        // the translation lookups because plural information was only avilable
+        // with the translation, but we don't need to save it again for every
+        // language.
+        db_update('locales_source')
+          ->fields(array(
+            'source' => implode(LOCALE_PLURAL_DELIMITER, $translation['source']),
+          ))
+          ->condition('lid', $lid)
+          ->execute();
+        $updated_sources[] = $lid;
+      }
+      db_update('locales_target')
+        ->fields(array(
+          'translation' => implode(LOCALE_PLURAL_DELIMITER, $translation['translation']),
+        ))
+        ->condition('lid', $lid)
+        ->condition('language', $langcode)
+        ->execute();
+    }
+  }
+
+  // Remove all plural LIDs from source and target. only keep those which were
+  // originally used for the singular strings (now updated to contain the
+  // serialized version of plurals).
+  $remove_lids = array_unique($remove_lids);
+  db_delete('locales_source')
+    ->condition('lid', $remove_lids, 'IN')
+    ->execute();
+  db_delete('locales_target')
+    ->condition('lid', $remove_lids, 'IN')
+    ->execute();
+
+  // Drop the primary key because it contains 'plural'.
+  db_drop_primary_key('locales_target');
+
+  // Remove the 'plid' and 'plural' columns and indexes.
+  db_drop_index('locales_target', 'plid');
+  db_drop_field('locales_target', 'plid');
+  db_drop_index('locales_target', 'plural');
+  db_drop_field('locales_target', 'plural');
+
+  // Add back a primary key without 'plural'.
+  db_add_primary_key('locales_target', array('language', 'lid'));
+}
+
 /**
  * @} 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.pages.inc b/core/modules/locale/locale.pages.inc
index e41bae59d4bdddaf84c9434998ea0601175b175d..0d59f421f09ac68f98f84f46c0b19dbf2636f2bc 100644
--- a/core/modules/locale/locale.pages.inc
+++ b/core/modules/locale/locale.pages.inc
@@ -92,7 +92,7 @@ function _locale_translate_seek() {
   $rows = array();
   foreach ($strings as $lid => $string) {
     $rows[] = array(
-      array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
+      array('data' => check_plain(truncate_utf8(str_replace(LOCALE_PLURAL_DELIMITER, ', ', $string['source']), 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
       $string['context'],
       array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'),
       array('data' => l(t('edit'), "admin/config/regional/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')),
@@ -278,13 +278,37 @@ function locale_translate_edit_form($form, &$form_state, $lid) {
     drupal_set_message(t('String not found.'), 'error');
     drupal_goto('admin/config/regional/translate/translate');
   }
-
-  // Add original text to the top and some values for form altering.
-  $form['original'] = array(
-    '#type'  => 'item',
-    '#title' => t('Original text'),
-    '#markup' => check_plain(wordwrap($source->source, 0)),
-  );
+  // Split source to work with plural values.
+  $source_array = explode(LOCALE_PLURAL_DELIMITER, $source->source);
+  if (count($source_array) == 1) {
+    // Add original text value and mark as non-plural.
+    $form['plural'] = array(
+      '#type' => 'value',
+      '#value' => 0
+    );
+    $form['original'] = array(
+      '#type'  => 'item',
+      '#title' => t('Original text'),
+      '#markup' => check_plain($source_array[0]),
+    );
+  }
+  else {
+    // Add original text value and mark as plural.
+    $form['plural'] = array(
+      '#type' => 'value',
+      '#value' => 1
+    );
+    $form['original_singular'] = array(
+      '#type'  => 'item',
+      '#title' => t('Original singular form'),
+      '#markup' => check_plain($source_array[0]),
+    );
+    $form['original_plural'] = array(
+      '#type'  => 'item',
+      '#title' => t('Original plural form'),
+      '#markup' => check_plain($source_array[1]),
+    );
+  }
   if (!empty($source->context)) {
     $form['context'] = array(
       '#type' => 'item',
@@ -307,22 +331,68 @@ function locale_translate_edit_form($form, &$form_state, $lid) {
   if (!locale_translate_english()) {
     unset($languages['en']);
   }
-  $form['translations'] = array('#tree' => TRUE);
+  // Store languages to iterate for validation and submission of the form.
+  $form_state['langcodes'] = array_keys($languages);
+  $plural_formulas = variable_get('locale_translation_plurals', array());
+
+  $form['translations'] = array(
+    '#type' => 'vertical_tabs',
+    '#tree' => TRUE
+  );
+
   // Approximate the number of rows to use in the default textarea.
-  $rows = min(ceil(str_word_count($source->source) / 12), 10);
+  $rows = min(ceil(str_word_count($source_array[0]) / 12), 10);
   foreach ($languages as $langcode => $language) {
     $form['translations'][$langcode] = array(
-      '#type' => 'textarea',
+      '#type' => 'fieldset',
       '#title' => $language->name,
-      '#rows' => $rows,
-      '#default_value' => '',
     );
+    if (empty($form['plural']['#value'])) {
+      $form['translations'][$langcode][0] = array(
+        '#type' => 'textarea',
+        '#title' => $language->name,
+        '#rows' => $rows,
+        '#default_value' => '',
+      );
+    }
+    else {
+      // Dealing with plural strings.
+      if (isset($plural_formulas[$langcode]['plurals']) && $plural_formulas[$langcode]['plurals'] > 1) {
+        // Add a textarea for each plural variant.
+        for ($i = 0; $i < $plural_formulas[$langcode]['plurals']; $i++) {
+          $form['translations'][$langcode][$i] = array(
+            '#type' => 'textarea',
+            '#title' => ($i == 0 ? t('Singular form') : format_plural($i, 'First plural form', '@count. plural form')),
+            '#rows' => $rows,
+            '#default_value' => '',
+          );
+        }
+      }
+      else {
+        // Fallback for unknown number of plurals.
+        $form['translations'][$langcode][0] = array(
+          '#type' => 'textarea',
+          '#title' => t('Sigular form'),
+          '#rows' => $rows,
+          '#default_value' => '',
+        );
+        $form['translations'][$langcode][1] = array(
+          '#type' => 'textarea',
+          '#title' => t('Plural form'),
+          '#rows' => $rows,
+          '#default_value' => '',
+        );
+      }
+    }
   }
 
   // Fetch translations and fill in default values in the form.
   $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid", array(':lid' => $lid));
   foreach ($result as $translation) {
-    $form['translations'][$translation->language]['#default_value'] = $translation->translation;
+    $translation_array = explode(LOCALE_PLURAL_DELIMITER, $translation->translation);
+    for ($i = 0; $i < count($translation_array); $i++) {
+      $form['translations'][$translation->language][$i]['#default_value'] = $translation_array[$i];
+    }
   }
 
   $form['actions'] = array('#type' => 'actions');
@@ -334,10 +404,12 @@ function locale_translate_edit_form($form, &$form_state, $lid) {
  * Validate string editing form submissions.
  */
 function locale_translate_edit_form_validate($form, &$form_state) {
-  foreach ($form_state['values']['translations'] as $key => $value) {
-    if (!locale_string_is_safe($value)) {
-      form_set_error('translations', t('The submitted string contains disallowed HTML: %string', array('%string' => $value)));
-      watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), WATCHDOG_WARNING);
+  foreach ($form_state['langcodes'] as $langcode) {
+    foreach ($form_state['values']['translations'][$langcode] as $key => $value) {
+      if (!locale_string_is_safe($value)) {
+        form_set_error("translations][$langcode][$key", t('The submitted string contains disallowed HTML: %string', array('%string' => $value)));
+        watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), WATCHDOG_WARNING);
+      }
     }
   }
 }
@@ -349,9 +421,19 @@ function locale_translate_edit_form_validate($form, &$form_state) {
  */
 function locale_translate_edit_form_submit($form, &$form_state) {
   $lid = $form_state['values']['lid'];
-  foreach ($form_state['values']['translations'] as $key => $value) {
-    $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField();
-    if (!empty($value)) {
+  foreach ($form_state['langcodes'] as $langcode) {
+    // Serialize plural variants in one string by LOCALE_PLURAL_DELIMITER.
+    $value = implode(LOCALE_PLURAL_DELIMITER, $form_state['values']['translations'][$langcode]);
+    $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField();
+    // No translation when all strings are empty.
+    $has_translation = FALSE;
+    foreach ($form_state['values']['translations'][$langcode] as $string) {
+      if (!empty($string)) {
+        $has_translation = TRUE;
+        break;
+      }
+    }
+    if ($has_translation) {
       // Only update or insert if we have a value to use.
       if (!empty($translation)) {
         db_update('locales_target')
@@ -359,7 +441,7 @@ function locale_translate_edit_form_submit($form, &$form_state) {
             'translation' => $value,
           ))
           ->condition('lid', $lid)
-          ->condition('language', $key)
+          ->condition('language', $langcode)
           ->execute();
       }
       else {
@@ -367,7 +449,7 @@ function locale_translate_edit_form_submit($form, &$form_state) {
           ->fields(array(
             'lid' => $lid,
             'translation' => $value,
-            'language' => $key,
+            'language' => $langcode,
           ))
           ->execute();
       }
@@ -376,12 +458,12 @@ function locale_translate_edit_form_submit($form, &$form_state) {
       // Empty translation entered: remove existing entry from database.
       db_delete('locales_target')
         ->condition('lid', $lid)
-        ->condition('language', $key)
+        ->condition('language', $langcode)
         ->execute();
     }
 
     // Force JavaScript translation file recreation for this language.
-    _locale_invalidate_js($key);
+    _locale_invalidate_js($langcode);
   }
 
   drupal_set_message(t('The string has been saved.'));
diff --git a/core/modules/locale/locale.test b/core/modules/locale/locale.test
index 4914593c404c9487b1da21032511e2a76f57de14..c6598a1b40f369c3cee6854dd450006fc7ddf9a5 100644
--- a/core/modules/locale/locale.test
+++ b/core/modules/locale/locale.test
@@ -267,8 +267,8 @@ class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
     $this->clickLink(t('edit'));
     $string_edit_url = $this->getUrl();
     $edit = array(
-      "translations[$langcode]" => $translation,
-      'translations[en]' => $translation_to_en,
+      "translations[$langcode][0]" => $translation,
+      'translations[en][0]' => $translation_to_en,
     );
     $this->drupalPost(NULL, $edit, t('Save translations'));
     $this->assertText(t('The string has been saved.'), t('The string has been saved.'));
@@ -367,7 +367,7 @@ class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
     $query->addExpression('min(l.lid)', 'lid');
     $result = $query->condition('l.location', '%.js%', 'LIKE')->execute();
     $url = 'admin/config/regional/translate/edit/' . $result->fetchObject()->lid;
-    $edit = array('translations['. $langcode .']' => $this->randomName());
+    $edit = array('translations['. $langcode .'][0]' => $this->randomName());
     $this->drupalPost($url, $edit, t('Save translations'));
 
     // Trigger JavaScript translation parsing and building.
@@ -434,7 +434,7 @@ class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
     $path = $matches[0];
     foreach ($bad_translations as $key => $translation) {
       $edit = array(
-        "translations[$langcode]" => $translation,
+        "translations[$langcode][0]" => $translation,
       );
       $this->drupalPost($path, $edit, t('Save translations'));
       // Check for a form error on the textarea.
@@ -521,7 +521,7 @@ class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
     preg_match('!admin/config/regional/translate/edit/(\d)+!', $this->getUrl(), $matches);
     $lid = $matches[1];
     $edit = array(
-      "translations[$langcode]" => $translation,
+      "translations[$langcode][0]" => $translation,
     );
     $this->drupalPost(NULL, $edit, t('Save translations'));
 
@@ -586,26 +586,26 @@ class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
 }
 
 /**
- * Tests plural index computation functionality.
+ * Tests plural format handling functionality.
  */
 class LocalePluralFormatTest extends DrupalWebTestCase {
   public static function getInfo() {
     return array(
-      'name' => 'Plural formula evaluation',
-      'description' => 'Tests plural formula evaluation for various languages.',
+      'name' => 'Plural handling',
+      'description' => 'Tests plural handling for various languages.',
       'group' => 'Locale',
     );
   }
 
   function setUp() {
-    parent::setUp('locale', 'locale_test');
+    parent::setUp('locale');
 
     $admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
     $this->drupalLogin($admin_user);
   }
 
   /**
-   * Tests locale_get_plural() functionality.
+   * Tests locale_get_plural() and format_plural() functionality.
    */
   function testGetPluralFormat() {
     // Import some .po files with formulas to set up the environment.
@@ -629,30 +629,174 @@ class LocalePluralFormatTest extends DrupalWebTestCase {
     // Reset static caches from locale_get_plural() to ensure we get fresh data.
     drupal_static_reset('locale_get_plural');
     drupal_static_reset('locale_get_plural:plurals');
+    drupal_static_reset('locale');
+
+    // Expected plural translation strings for each plural index.
+    $plural_strings = array(
+      // English is not imported in this case, so we assume built-in text
+      // and formulas.
+      'en' => array(
+        0 => '1 hour',
+        1 => '@count hours',
+      ),
+      'fr' => array(
+        0 => '1 heure',
+        1 => '@count heures',
+      ),
+      'hr' => array(
+        0 => '@count sat',
+        1 => '@count sata',
+        2 => '@count sati',
+      ),
+      // Hungarian is not imported, so it should assume the same text as
+      // English, but it will always pick the plural form as per the built-in
+      // logic, so only index -1 is relevant with the plural value.
+      'hu' => array(
+        0 => '1 hour',
+        -1 => '@count hours',
+      ),
+    );
 
-    // Test locale_get_plural() for English (no formula presnt).
-    $this->assertIdentical(locale_get_plural(1, 'en'), 0, t("Computed plural index for 'en' with count 1 is 0."));
-    $this->assertIdentical(locale_get_plural(0, 'en'), 1, t("Computed plural index for 'en' with count 0 is 1."));
-    $this->assertIdentical(locale_get_plural(5, 'en'), 1, t("Computed plural index for 'en' with count 5 is 1."));
+    // Expected plural indexes precomputed base on the plural formulas with
+    // given $count value.
+    $plural_tests = array(
+      'en' => array(
+        1 => 0,
+        0 => 1,
+        5 => 1,
+      ),
+      'fr' => array(
+        1 => 0,
+        0 => 0,
+        5 => 1,
+      ),
+      'hr' => array(
+        1 => 0,
+        21 => 0,
+        0 => 2,
+        2 => 1,
+        8 => 2,
+      ),
+      'hu' => array(
+        1 => -1,
+        21 => -1,
+        0 => -1,
+      ),
+    );
 
-    // Test locale_get_plural() for French (simpler formula).
-    $this->assertIdentical(locale_get_plural(1, 'fr'), 0, t("Computed plural index for 'fr' with count 1 is 0."));
-    $this->assertIdentical(locale_get_plural(0, 'fr'), 0, t("Computed plural index for 'fr' with count 0 is 0."));
-    $this->assertIdentical(locale_get_plural(5, 'fr'), 1, t("Computed plural index for 'fr' with count 5 is 1."));
+    foreach ($plural_tests as $langcode => $tests) {
+      foreach ($tests as $count => $expected_plural_index) {
+        // Assert that the we get the right plural index.
+        $this->assertIdentical(locale_get_plural($count, $langcode), $expected_plural_index, 'Computed plural index for ' . $langcode . ' for count ' . $count . ' is ' . $expected_plural_index);
+        // Assert that the we get the right translation for that. Change the
+        // expected index as per the logic for translation lookups.
+        $expected_plural_index = ($count == 1) ? 0 : $expected_plural_index;
+        $expected_plural_string = str_replace('@count', $count, $plural_strings[$langcode][$expected_plural_index]);
+        $this->assertIdentical(format_plural($count, '1 hour', '@count hours', array(), array('langcode' => $langcode)), $expected_plural_string, 'Plural translation of 1 hours / @count hours for count ' . $count . ' in ' . $langcode . ' is ' . $expected_plural_string);
+      }
+    }
+  }
 
-    // Test locale_get_plural() for Croatian (more complex formula).
-    $this->assertIdentical(locale_get_plural( 1, 'hr'), 0, t("Computed plural index for 'hr' with count 1 is 0."));
-    $this->assertIdentical(locale_get_plural(21, 'hr'), 0, t("Computed plural index for 'hr' with count 21 is 0."));
-    $this->assertIdentical(locale_get_plural( 0, 'hr'), 2, t("Computed plural index for 'hr' with count 0 is 2."));
-    $this->assertIdentical(locale_get_plural( 2, 'hr'), 1, t("Computed plural index for 'hr' with count 2 is 1."));
-    $this->assertIdentical(locale_get_plural( 8, 'hr'), 2, t("Computed plural index for 'hr' with count 8 is 2."));
+  /**
+   * Tests plural editing and export functionality.
+   */
+  function testPluralEditExport() {
+    // Import some .po files with formulas to set up the environment.
+    // These will also add the languages to the system and enable them.
+    $this->importPoFile($this->getPoFileWithSimplePlural(), array(
+      'langcode' => 'fr',
+    ));
+    $this->importPoFile($this->getPoFileWithComplexPlural(), array(
+      'langcode' => 'hr',
+    ));
 
-    // Test locale_get_plural() for Hungarian (nonexistent language).
-    $this->assertIdentical(locale_get_plural( 1, 'hu'), -1, t("Computed plural index for 'hu' with count 1 is -1."));
-    $this->assertIdentical(locale_get_plural(21, 'hu'), -1, t("Computed plural index for 'hu' with count 21 is -1."));
-    $this->assertIdentical(locale_get_plural( 0, 'hu'), -1, t("Computed plural index for 'hu' with count 0 is -1."));
+    // Get the French translations.
+    $this->drupalPost('admin/config/regional/translate/export', array(
+      'langcode' => 'fr',
+    ), t('Export'));
+    // Ensure we have a translation file.
+    $this->assertRaw('# French translation of Drupal', t('Exported French translation file.'));
+    // Ensure our imported translations exist in the file.
+    $this->assertRaw("msgid \"Monday\"\nmsgstr \"lundi\"", t('French translations present in exported file.'));
+    // Check for plural export specifically.
+    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"1 heure\"\nmsgstr[1] \"@count heures\"", t('Plural translations exported properly.'));
+
+    // Get the Croatian translations.
+    $this->drupalPost('admin/config/regional/translate/export', array(
+      'langcode' => 'hr',
+    ), t('Export'));
+    // Ensure we have a translation file.
+    $this->assertRaw('# Croatian translation of Drupal', t('Exported Croatian translation file.'));
+    // Ensure our imported translations exist in the file.
+    $this->assertRaw("msgid \"Monday\"\nmsgstr \"Ponedjeljak\"", t('Croatian translations present in exported file.'));
+    // Check for plural export specifically.
+    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata\"\nmsgstr[2] \"@count sati\"", t('Plural translations exported properly.'));
+
+    // Check if the source appears on the translation page.
+    $this->drupalGet('admin/config/regional/translate');
+    $this->assertText("1 hour, @count hours");
+
+    // Look up editing page for this plural string and check fields.
+    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 hour" . LOCALE_PLURAL_DELIMITER . "@count hours"))->fetchField();
+    $path = 'admin/config/regional/translate/edit/' . $lid;
+    $this->drupalGet($path);
+    // Labels for plural editing elements.
+    $this->assertText('Singular form');
+    $this->assertText('First plural form');
+    $this->assertText('2. plural form');
+    // Plural values for both languages.
+    $this->assertFieldById('edit-translations-hr-0', '@count sat');
+    $this->assertFieldById('edit-translations-hr-1', '@count sata');
+    $this->assertFieldById('edit-translations-hr-2', '@count sati');
+    $this->assertNoFieldById('edit-translations-hr-3');
+    $this->assertFieldById('edit-translations-fr-0', '1 heure');
+    $this->assertFieldById('edit-translations-fr-1', '@count heures');
+    $this->assertNoFieldById('edit-translations-fr-2');
+
+    // Edit some translations and see if that took effect.
+    $edit = array(
+      'translations[fr][0]' => '1 heure edited',
+      'translations[hr][1]' => '@count sata edited',
+    );
+    $this->drupalPost($path, $edit, t('Save translations'));
+
+    // Inject a plural source string to the database. We need to use a specific
+    // langcode here because the language will be English by default and will
+    // not save our source string for performance optimization if we do not ask
+    // specifically for a language.
+    format_plural(1, '1 day', '@count days', array(), array('langcode' => 'fr'));
+    // Look up editing page for this plural string and check fields.
+    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 day" . LOCALE_PLURAL_DELIMITER . "@count days"))->fetchField();
+    $path = 'admin/config/regional/translate/edit/' . $lid;
+
+    // Save complete translations for the string in both languages.
+    $edit = array(
+      'translations[fr][0]' => '1 jour',
+      'translations[fr][1]' => '@count jours',
+      'translations[hr][0]' => '@count dan',
+      'translations[hr][1]' => '@count dana',
+      'translations[hr][2]' => '@count dana',
+    );
+    $this->drupalPost($path, $edit, t('Save translations'));
+
+    // Get the French translations.
+    $this->drupalPost('admin/config/regional/translate/export', array(
+      'langcode' => 'fr',
+    ), t('Export'));
+    // Check for plural export specifically.
+    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"1 heure edited\"\nmsgstr[1] \"@count heures\"", t('Edited French plural translations for hours exported properly.'));
+    $this->assertRaw("msgid \"1 day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"1 jour\"\nmsgstr[1] \"@count jours\"", t('Added French plural translations for days exported properly.'));
+
+    // Get the Croatian translations.
+    $this->drupalPost('admin/config/regional/translate/export', array(
+      'langcode' => 'hr',
+    ), t('Export'));
+    // Check for plural export specifically.
+    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata edited\"\nmsgstr[2] \"@count sati\"", t('Edited Croatian plural translations exported properly.'));
+    $this->assertRaw("msgid \"1 day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"@count dan\"\nmsgstr[1] \"@count dana\"\nmsgstr[2] \"@count dana\"", t('Added Croatian plural translations exported properly.'));
   }
 
+
   /**
    * Imports a standalone .po file in a given language.
    *
@@ -792,8 +936,8 @@ class LocaleImportFunctionalTest extends DrupalWebTestCase {
     // The import should automatically create the corresponding language.
     $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.'));
 
-    // The import should have created 7 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' => 9, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
+    // The import should have created 8 strings.
+    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
 
     // This import should have saved plural forms to have 2 variants.
     $locale_plurals = variable_get('locale_translation_plurals', array());
@@ -1284,7 +1428,7 @@ class LocaleUninstallFunctionalTest extends DrupalWebTestCase {
     $string = db_query('SELECT min(lid) AS lid FROM {locales_source} WHERE location LIKE :location', array(
       ':location' => '%.js%',
     ))->fetchObject();
-    $edit = array('translations[fr]' => 'french translation');
+    $edit = array('translations[fr][0]' => 'french translation');
     $this->drupalPost('admin/config/regional/translate/edit/' . $string->lid, $edit, t('Save translations'));
     _locale_rebuild_js('fr');
     $locale_javascripts = variable_get('locale_translation_javascript', array());
@@ -2214,8 +2358,8 @@ class LocaleUILanguageNegotiationTest extends DrupalWebTestCase {
     // Should find the string and now click edit to post translated string.
     $this->clickLink('edit');
     $edit = array(
-      "translations[$langcode_browser_fallback]" => $language_browser_fallback_string,
-      "translations[$langcode]" => $language_string,
+      "translations[$langcode_browser_fallback][0]" => $language_browser_fallback_string,
+      "translations[$langcode][0]" => $language_string,
     );
     $this->drupalPost(NULL, $edit, t('Save translations'));
 
diff --git a/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php b/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php
index dfe37fa38e4ad03896145513ac70f9583a29d5e7..99dc318fd0c5509db603fa886b65db22993f260f 100644
--- a/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php
+++ b/core/modules/simpletest/tests/upgrade/drupal-7.language.database.php
@@ -183,6 +183,19 @@
   'weight' => '0',
   'javascript' => '',
 ))
+->values(array(
+  'language' => 'hr',
+  'name' => 'Croatian',
+  'native' => 'Hrvatski',
+  'direction' => '0',
+  'enabled' => '1',
+  'plurals' => '3',
+  'formula' => '((($n%10)==1)&&(($n%100)!=11))?(0):((((($n%10)>=2)&&(($n%10)<=4))&&((($n%100)<10)||(($n%100)>=20)))?(1):2));',
+  'domain' => '',
+  'prefix' => '',
+  'weight' => '0',
+  'javascript' => '',
+))
 ->execute();
 
 // Add locales_source table from locale.install schema and fill with some
@@ -413,6 +426,30 @@
   'context' => '',
   'version' => 'none',
 ))
+->values(array(
+  'lid' => '22',
+  'location' => '',
+  'textgroup' => 'default',
+  'source' => '1 byte',
+  'context' => '',
+  'version' => 'none',
+))
+->values(array(
+  'lid' => '23',
+  'location' => '',
+  'textgroup' => 'default',
+  'source' => '@count bytes',
+  'context' => '',
+  'version' => 'none',
+))
+->values(array(
+  'lid' => '24',
+  'location' => '',
+  'textgroup' => 'default',
+  'source' => '@count[2] bytes',
+  'context' => '',
+  'version' => 'none',
+))
 ->execute();
 
 // Add locales_target table from locale.install schema.
@@ -472,6 +509,49 @@
   'module' => 'locale',
   'name' => 'locales_target',
 ));
+db_insert('locales_target')->fields(array(
+  'lid',
+  'translation',
+  'language',
+  'plid',
+  'plural',
+))
+->values(array(
+  'lid' => 22,
+  'translation' => '1 byte',
+  'language' => 'ca',
+  'plid' => 0,
+  'plural' => 0,
+))
+->values(array(
+  'lid' => 23,
+  'translation' => '@count bytes',
+  'language' => 'ca',
+  'plid' => 22,
+  'plural' => 1,
+))
+->values(array(
+  'lid' => 22,
+  'translation' => '@count bajt',
+  'language' => 'hr',
+  'plid' => 0,
+  'plural' => 0,
+))
+->values(array(
+  'lid' => 23,
+  'translation' => '@count bajta',
+  'language' => 'hr',
+  'plid' => 22,
+  'plural' => 1,
+))
+->values(array(
+  'lid' => 24,
+  'translation' => '@count[2] bajtova',
+  'language' => 'hr',
+  'plid' => 23,
+  'plural' => 2,
+))
+->execute();
 
 // Set up variables needed for language support.
 db_insert('variable')->fields(array(
diff --git a/core/modules/simpletest/tests/upgrade/upgrade.language.test b/core/modules/simpletest/tests/upgrade/upgrade.language.test
index feae8f821486911a6f51800f31c217c06502ef8e..ecbbd12229a9c264b9e222dc39c963940b4dd276 100644
--- a/core/modules/simpletest/tests/upgrade/upgrade.language.test
+++ b/core/modules/simpletest/tests/upgrade/upgrade.language.test
@@ -104,6 +104,23 @@ class LanguageUpgradePathTestCase extends UpgradePathTestCase {
     // renamed.
     $current_weights = variable_get('locale_language_negotiation_methods_weight_language_interface', array());
     $this->assertTrue(serialize($expected_weights) == serialize($current_weights), t('Language negotiation method weights upgraded.'));
+
+    // Look up migrated plural string.
+    $source_string = db_query('SELECT * FROM {locales_source} WHERE lid = 22')->fetchObject();
+    $this->assertEqual($source_string->source, implode(LOCALE_PLURAL_DELIMITER, array('1 byte', '@count bytes')));
+
+    $translation_string = db_query("SELECT * FROM {locales_target} WHERE lid = 22 AND language = 'hr'")->fetchObject();
+    $this->assertEqual($translation_string->translation, implode(LOCALE_PLURAL_DELIMITER, array('@count bajt', '@count bajta', '@count bajtova')));
+    $this->assertTrue(!isset($translation_string->plural), 'Chained plural indicator removed.');
+    $this->assertTrue(!isset($translation_string->plid), 'Chained plural indicator removed.');
+
+    $source_string = db_query('SELECT * FROM {locales_source} WHERE lid IN (23, 24)')->fetchObject();
+    $this->assertTrue(empty($source_string), 'Individual plural variant source removed');
+    $translation_string = db_query("SELECT * FROM {locales_target} WHERE lid IN (23, 24)")->fetchObject();
+    $this->assertTrue(empty($translation_string), 'Individual plural variant translation removed');
+
+    $translation_string = db_query("SELECT * FROM {locales_target} WHERE lid = 22 AND language = 'ca'")->fetchObject();
+    $this->assertEqual($translation_string->translation, implode(LOCALE_PLURAL_DELIMITER, array('1 byte', '@count bytes')));
   }
 
   /**