diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 2278112..f76486f 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1307,6 +1307,7 @@ function install_select_language(&$install_state) {
  */
 function install_select_language_form($form, &$form_state, $files) {
   include_once DRUPAL_ROOT . '/core/includes/standard.inc';
+  include_once DRUPAL_ROOT . '/core/modules/language/language.module';
   include_once DRUPAL_ROOT . '/core/modules/language/language.negotiation.inc';
 
   $standard_languages = standard_language_list();
diff --git a/core/modules/language/config/language.mappings.yml b/core/modules/language/config/language.mappings.yml
new file mode 100644
index 0000000..03a6569
--- /dev/null
+++ b/core/modules/language/config/language.mappings.yml
@@ -0,0 +1,9 @@
+# Browsers use different language codes to refer to the same languages,
+# these defaults handles the most common cases.
+zh-tw: zh-hant # Taiwan Chinese in traditional script
+zh-hk: zh-hant # Hong Kong Chinese in traditional script
+zh-mo: zh-hant # Macao Chinese in traditional script
+zh-cht: zh-hant # traditional Chinese
+zh-cn: zh-hans # PRC Mainland Chinese in simplified script
+zh-sg: zh-hans # Singapore Chinese in simplified script
+zh-chs: zh-hans # simplified Chinese
diff --git a/core/modules/language/language.admin.inc b/core/modules/language/language.admin.inc
index c1154c5..01b591b 100644
--- a/core/modules/language/language.admin.inc
+++ b/core/modules/language/language.admin.inc
@@ -813,3 +813,213 @@ function language_negotiation_configure_session_form($form, &$form_state) {
 
   return system_settings_form($form);
 }
+
+/**
+ * Builds the browser language negotiation method configuration form.
+ */
+function language_negotiation_configure_browser_form($form, &$form_state) {
+  $form = array();
+
+  // Initialize a language list to the ones available, including English.
+  drupal_static_reset('language_list');
+  $languages = language_list();
+
+  $existing_languages = array();
+  foreach ($languages as $langcode => $language) {
+    $existing_languages[$langcode] = $language->name;
+  }
+
+  // If we have no languages available, present the list of predefined languages
+  // only. If we do have already added languages, set up two option groups with
+  // the list of existing and then predefined languages.
+  if (empty($existing_languages)) {
+    $language_options = language_admin_predefined_list();
+    $default = key($language_options);
+  }
+  else {
+    $default = key($existing_languages);
+    $language_options = array(
+      t('Existing languages') => $existing_languages,
+      t('Languages not yet added') => language_admin_predefined_list()
+    );
+  }
+
+  $form['mappings'] = array(
+    '#type' => 'container',
+    '#tree' => TRUE,
+    '#theme' => 'language_negotiation_configure_browser_form_table',
+  );
+
+  $mappings = language_get_browser_drupal_langcode_mappings();
+  foreach ($mappings as $browser_langcode => $drupal_langcode) {
+    $form['mappings'][$browser_langcode] = array(
+      'browser_langcode' => array(
+        '#type' => 'textfield',
+        '#default_value' => $browser_langcode,
+        '#size' => 20,
+        '#required' => TRUE,
+      ),
+      'drupal_langcode' => array(
+        '#type' => 'select',
+        '#options' => $language_options,
+        '#default_value' => $drupal_langcode,
+        '#required' => TRUE,
+      ),
+    );
+  }
+
+  // Add empty row.
+  $form['mappings']['_new'] = array(
+    'browser_langcode' => array(
+      '#type' => 'textfield',
+      '#default_value' => '',
+      '#size' => 20,
+    ),
+    'drupal_langcode' => array(
+      '#type' => 'textfield',
+      '#default_value' => '',
+      '#size' => 20,
+    ),
+  );
+
+  $form_state['redirect'] = 'admin/config/regional/language/detection';
+
+  $form['actions']['#type'] = 'actions';
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save configuration'),
+  );
+
+  return $form;
+}
+
+/**
+ * Theme browser configuration form as table.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_language_negotiation_configure_browser_form_table($variables) {
+  $form = $variables['form'];
+  $rows = array();
+  $link_attributes = array(
+    'attributes' => array(
+      'class' => array('image-style-link'),
+    ),
+  );
+  foreach (element_children($form, TRUE) as $key) {
+    $row = array();
+    $row[] = drupal_render($form[$key]['browser_langcode']);
+    $row[] = drupal_render($form[$key]['drupal_langcode']);
+
+    if ($key != '_new') {
+      $row[] = l(t('Delete'), 'admin/config/regional/language/detection/browser/delete/' . $key, $link_attributes);
+    }
+    else {
+      $row[] = '';
+    }
+    $rows[] = array(
+      'data' => $row,
+    );
+  }
+
+  $header = array(
+    t('Browser language code'),
+    t('Drupal language code'),
+    t('Operations'),
+  );
+
+  $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'lang-neg-browser')));
+
+  return $output;
+}
+
+/**
+ * Browser language negotiation form validation.
+ */
+function language_negotiation_configure_browser_form_validate($form, &$form_state) {
+  $mappings = $form_state['values']['mappings'];
+  $unique_values = array();
+  foreach ($mappings as $key => $data) {
+    if ($key == '_new') {
+      if (!empty($data['browser_langcode']) && !empty($data['drupal_langcode'])) {
+        // Make sure browser_langcode is unique.
+        if (array_key_exists($data['browser_langcode'], $unique_values)) {
+          form_set_error('mappings][' . $key . '][browser_langcode', 'Browser language codes must be unique.');
+        }
+        elseif (preg_match('/[^a-z\-]/', $data['browser_langcode'])) {
+          form_set_error('mappings][' . $key . '][browser_langcode', 'Browser language codes can only contain lowercase letters and a hyphen(-).');
+        }
+        elseif (preg_match('/[^a-z\-]/', $data['drupal_langcode'])) {
+          form_set_error('mappings][' . $key . '][drupal_langcode', 'Drupal language codes can only contain lowercase letters and a hyphen(-).');
+        }
+        $unique_values[$data['browser_langcode']] = $data['drupal_langcode'];
+      }
+      elseif (empty($data['browser_langcode']) xor empty($data['drupal_langcode'])) {
+        form_set_error('mappings][' . $key . '][drupal_langcode', 'Both browser and Drupal language codes must be provided.');
+      }
+    }
+    else {
+      if (!empty($data['browser_langcode']) && !empty($data['drupal_langcode'])) {
+        // Make sure browser_langcode is unique.
+        if (array_key_exists($data['browser_langcode'], $unique_values)) {
+          form_set_error('mappings][' . $key . '][browser_langcode', 'Browser language codes must be unique.');
+        }
+        elseif (preg_match('/[^a-z\-]/', $data['browser_langcode'])) {
+          form_set_error('mappings][' . $key . '][browser_langcode', 'Browser language codes can only contain lowercase letters and a hyphen(-).');
+        }
+        elseif (preg_match('/[^a-z\-]/', $data['drupal_langcode'])) {
+          form_set_error('mappings][' . $key . '][drupal_langcode', 'Drupal language codes can only contain lowercase letters and a hyphen(-).');
+        }
+        $unique_values[$data['browser_langcode']] = $data['drupal_langcode'];
+      }
+      else {
+        if (empty($data['browser_langcode'])) {
+          form_set_error('mappings][' . $key . '][browser_langcode', 'Browser language code cannot be blank.');
+        }
+        if (empty($data['drupal_langcode'])) {
+          form_set_error('mappings][' . $key . '][drupal_langcode', 'Drupal language code cannot be blank.');
+        }
+      }
+    }
+  }
+  $form_state['mappings'] = $unique_values;
+}
+
+/**
+ * Browser language negotiation form submit.
+ */
+function language_negotiation_configure_browser_form_submit($form, &$form_state) {
+  $mappings = $form_state['mappings'];
+  if (!empty($mappings)) {
+    language_set_browser_drupal_langcode_mappings($mappings);
+  }
+}
+
+/**
+ * Form for deleting a browser language negotiation mapping.
+ */
+function language_negotiation_configure_browser_delete_form($form, &$form_state, $browser_langcode) {
+  $form_state['browser_langcode'] = $browser_langcode;
+  $question = t('Are you sure you want to delete %browser_langcode?', array(
+    '%browser_langcode' => $browser_langcode,
+  ));
+  $path = 'admin/config/regional/language/detection/browser';
+  return confirm_form($form, $question, $path, '');
+}
+
+/**
+ * Form submit handler to delete a browser language negotiation mapping.
+ */
+function language_negotiation_configure_browser_delete_form_submit($form, &$form_state) {
+  $browser_langcode = $form_state['browser_langcode'];
+  $mappings = language_get_browser_drupal_langcode_mappings();
+  if (array_key_exists($browser_langcode, $mappings)) {
+    unset($mappings[$browser_langcode]);
+    language_set_browser_drupal_langcode_mappings($mappings);
+  }
+  $form_state['redirect'] = 'admin/config/regional/language/detection/browser';
+}
diff --git a/core/modules/language/language.module b/core/modules/language/language.module
index 67c0d6f..00220c0 100644
--- a/core/modules/language/language.module
+++ b/core/modules/language/language.module
@@ -37,6 +37,10 @@ function language_help($path, $arg) {
       $output = '<p>' . t('Determine the language from a request/session parameter. Example: "http://example.com?language=de" sets language to German based on the use of "de" within the "language" parameter.') . '</p>';
       return $output;
 
+    case 'admin/config/regional/language/detection/browser':
+      $output = '<p>' . t('Browsers use different language codes to refer to the same languages. You can add and edit mappings from browser language codes to the language codes used by Drupal.') . '</p>';
+      return $output;
+
     case 'admin/structure/block/manage/%/%':
       if ($arg[4] == 'language' && $arg[5] == 'language_interface') {
         return '<p>' . t('With multiple languages enabled, registered users can select their preferred language and authors can assign a specific language to content.') . '</p>';
@@ -116,6 +120,21 @@ function language_menu() {
     'file' => 'language.admin.inc',
     'type' => MENU_VISIBLE_IN_BREADCRUMB,
   );
+  $items['admin/config/regional/language/detection/browser'] = array(
+    'title' => 'Browser language detection configuration',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('language_negotiation_configure_browser_form'),
+    'access arguments' => array('administer languages'),
+    'file' => 'language.admin.inc',
+    'type' => MENU_VISIBLE_IN_BREADCRUMB,
+  );
+  $items['admin/config/regional/language/detection/browser/delete/%'] = array(
+    'title' => 'Delete language mapping',
+    'page arguments' => array('language_negotiation_configure_browser_delete_form', 7),
+    'type' => MENU_CALLBACK,
+    'access arguments' => array('administer languages'),
+    'file' => 'language.admin.inc',
+  );
 
   return $items;
 }
@@ -154,6 +173,10 @@ function language_theme() {
     'language_negotiation_configure_form' => array(
       'render element' => 'form',
     ),
+    'language_negotiation_configure_browser_form_table' => array(
+      'render element' => 'form',
+      'file' => 'language.admin.inc',
+    ),
   );
 }
 
@@ -395,6 +418,7 @@ function language_language_negotiation_info() {
     'cache' => 0,
     'name' => t('Browser'),
     'description' => t("Language from the browser's language settings."),
+    'config' => 'admin/config/regional/language/detection/browser',
   );
 
   $negotiation_info[LANGUAGE_NEGOTIATION_INTERFACE] = array(
@@ -581,3 +605,32 @@ function language_url_outbound_alter(&$path, &$options, $original_path) {
     }
   }
 }
+
+/**
+ * Returns language mappings between browser and Drupal language codes.
+ *
+ * @return array
+ *   An array containing browser language codes as keys with corresponding
+ *   Drupal language codes as values.
+ */
+function language_get_browser_drupal_langcode_mappings() {
+  $config = config('language.mappings');
+  if ($config->isNew()) {
+    config_install_default_config('module', 'language');
+    $config = config('language.mappings');
+  }
+  return $config->get();
+}
+
+/**
+ * Stores language mappings between browser and Drupal language codes.
+ *
+ * @param array $mappings
+ *   An array containing browser language codes as keys with corresponding
+ *   Drupal language codes as values.
+ */
+function language_set_browser_drupal_langcode_mappings($mappings) {
+  $config = config('language.mappings');
+  $config->setData($mappings);
+  $config->save();
+}
diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc
index 8f3dc12..92b4b66 100644
--- a/core/modules/language/language.negotiation.inc
+++ b/core/modules/language/language.negotiation.inc
@@ -63,6 +63,15 @@ function language_from_interface() {
 /**
  * Identify language from the Accept-language HTTP header we got.
  *
+ * The algorithm works as follows:
+ * - map browser language codes to Drupal language codes.
+ * - order all browser language codes by qvalue from high to low.
+ * - add generic browser language codes if they aren't already specified
+ *   but with a slightly lower qvalue.
+ * - find the most specific Drupal language code with the highest qvalue.
+ * - if 2 or more languages are having the same qvalue, respect the order of
+ *   them inside the $languages array.
+ *
  * We perform browser accept-language parsing only if page cache is disabled,
  * otherwise we would cache a user-specific preference.
  *
@@ -86,7 +95,18 @@ function language_from_browser($languages) {
   // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
   $browser_langcodes = array();
   if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
+    // Load custom mappings to support browsers that are sending non standard
+    // language codes.
+    $mappings = language_get_browser_drupal_langcode_mappings();
     foreach ($matches as $match) {
+      if ($mappings) {
+        $langcode = strtolower($match[1]);
+        foreach ($mappings as $browser_langcode => $drupal_langcode) {
+          if ($langcode == $browser_langcode) {
+            $match[1] = $drupal_langcode;
+          }
+        }
+      }
       // We can safely use strtolower() here, tags are ASCII.
       // RFC2616 mandates that the decimal part is no more than three digits,
       // so we multiply the qvalue by 1000 to avoid floating point comparisons.
@@ -105,9 +125,20 @@ function language_from_browser($languages) {
   // http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
   asort($browser_langcodes);
   foreach ($browser_langcodes as $langcode => $qvalue) {
-    $generic_tag = strtok($langcode, '-');
-    if (!isset($browser_langcodes[$generic_tag])) {
-      $browser_langcodes[$generic_tag] = $qvalue;
+    // For Chinese languages the generic tag is either zh-hans or zh-hant, so we
+    // need to handle this separately.
+    $generic_tag = '';
+    if (strlen($langcode) > 7 && (substr($langcode, 0, 7) == 'zh-hant' || substr($langcode, 0, 7) == 'zh-hans')) {
+      $generic_tag = substr($langcode, 0, 7);
+    }
+    else {
+      $generic_tag = strtok($langcode, '-');
+    }
+    if (!empty($generic_tag) && !isset($browser_langcodes[$generic_tag])) {
+      // Add the generic langcode, but make sure it has a lower qvalue as the
+      // more specific one, so the more specific one gets selected if it's
+      // defined by both the browser and Drupal.
+      $browser_langcodes[$generic_tag] = $qvalue - 0.1;
     }
   }
 
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageBrowserDetectionUnitTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageBrowserDetectionUnitTest.php
index 3586ba8..afa5666 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageBrowserDetectionUnitTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageBrowserDetectionUnitTest.php
@@ -7,13 +7,15 @@
 
 namespace Drupal\language\Tests;
 
-use Drupal\simpletest\UnitTestBase;
+use Drupal\simpletest\WebTestBase;
 use Drupal\Core\Language\Language;
 
 /**
  * Test browser language detection.
  */
-class LanguageBrowserDetectionUnitTest extends UnitTestBase {
+class LanguageBrowserDetectionUnitTest extends WebTestBase {
+
+  public static $modules = array('language');
 
   public static function getInfo() {
     return array(
@@ -25,10 +27,14 @@ class LanguageBrowserDetectionUnitTest extends UnitTestBase {
 
   /**
    * Unit tests for the language_from_browser() function.
+   *
+   * @see language_from_browser().
    */
   function testLanguageFromBrowser() {
-    // Load the required functions.
-    require_once DRUPAL_ROOT . '/core/modules/language/language.negotiation.inc';
+    // The order of the languages is only important if the browser language
+    // codes are having the same qvalue, otherwise the one with the highest
+    // qvalue is prefered. The automatically generated generic tags are always
+    // having a lower qvalue.
 
     $languages = array(
       // In our test case, 'en' has priority over 'en-US'.
@@ -58,6 +64,16 @@ class LanguageBrowserDetectionUnitTest extends UnitTestBase {
       'eh-oh-laa-laa' => new Language(array(
         'langcode' => 'eh-oh-laa-laa',
       )),
+      // Chinese languages.
+      'zh-hans' => new Language(array(
+        'langcode' => 'zh-hans',
+      )),
+      'zh-hant' => new Language(array(
+        'langcode' => 'zh-hant',
+      )),
+      'zh-hant-tw' => new Language(array(
+        'langcode' => 'zh-hant',
+      )),
     );
 
     $test_cases = array(
@@ -66,8 +82,8 @@ class LanguageBrowserDetectionUnitTest extends UnitTestBase {
       'en-US,en,fr-CA,fr,es-MX' => 'en',
       'fr,en' => 'en',
       'en,fr' => 'en',
-      'en-US,fr' => 'en',
-      'fr,en-US' => 'en',
+      'en-US,fr' => 'en-US',
+      'fr,en-US' => 'en-US',
       'fr,fr-CA' => 'fr-CA',
       'fr-CA,fr' => 'fr-CA',
       'fr' => 'fr-CA',
@@ -120,6 +136,21 @@ class LanguageBrowserDetectionUnitTest extends UnitTestBase {
       'de,pl' => FALSE,
       'iecRswK4eh' => FALSE,
       $this->randomName(10) => FALSE,
+
+      // Chinese langcodes.
+      'zh-cn, en-us;q=0.90, en;q=0.80, zh;q=0.70' => 'zh-hans',
+      'zh-tw, en-us;q=0.90, en;q=0.80, zh;q=0.70' => 'zh-hant',
+      'zh-hant, en-us;q=0.90, en;q=0.80, zh;q=0.70' => 'zh-hant',
+      'zh-hans, en-us;q=0.90, en;q=0.80, zh;q=0.70' => 'zh-hans',
+      'zh-cn' => 'zh-hans',
+      'zh-sg' => 'zh-hans',
+      'zh-tw' => 'zh-hant',
+      'zh-hk' => 'zh-hant',
+      'zh-mo' => 'zh-hant',
+      'zh-hans' => 'zh-hans',
+      'zh-hant' => 'zh-hant',
+      'zh-chs' => 'zh-hans',
+      'zh-cht' => 'zh-hant',
     );
 
     foreach ($test_cases as $accept_language => $expected_result) {
@@ -128,4 +159,84 @@ class LanguageBrowserDetectionUnitTest extends UnitTestBase {
       $this->assertIdentical($result, $expected_result, t("Language selection '@accept-language' selects '@result', result = '@actual'", array('@accept-language' => $accept_language, '@result' => $expected_result, '@actual' => isset($result) ? $result : 'none')));
     }
   }
+
+  /**
+   * Tests for adding, editing and deleting mappings between browser language
+   * codes and Drupal language codes.
+   */
+  function testUIBrowserLanguageMappings() {
+    // User to manage languages.
+    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
+    $this->drupalLogin($admin_user);
+
+    // Check that the configure link exists.
+    $this->drupalGet('admin/config/regional/language/detection');
+    $this->assertLinkByHref('admin/config/regional/language/detection/browser');
+
+    // Check that defaults are loaded from language.mappings.yml.
+    $this->drupalGet('admin/config/regional/language/detection/browser');
+    $this->assertField('edit-mappings-zh-cn-browser-langcode', 'zh-cn', 'Chinese browser language code found.');
+    $this->assertField('edit-mappings-zh-cn-drupal-langcode', 'zh-hans-cn', 'Chinese Drupal language code found.');
+
+    // Delete zh-cn language code.
+    $browser_langcode = 'zh-cn';
+    $this->drupalGet('admin/config/regional/language/detection/browser/delete/' . $browser_langcode);
+    $message = t('Are you sure you want to delete @browser_langcode?', array(
+      '@browser_langcode' => $browser_langcode,
+    ));
+    $this->assertRaw($message, 'Question found.');
+
+    // Confirm the delete.
+    $edit = array();
+    $this->drupalPost('admin/config/regional/language/detection/browser/delete/' . $browser_langcode, $edit, t('Confirm'));
+
+    // Check that ch-zn no longer exists.
+    $this->assertNoField('edit-mappings-zh-cn-browser-langcode', 'Chinese browser language code no longer exists.');
+
+    // Add a new custom mapping.
+    $edit = array(
+      'mappings[_new][browser_langcode]' => 'xx',
+      'mappings[_new][drupal_langcode]' => 'en',
+    );
+    $this->drupalPost('admin/config/regional/language/detection/browser', $edit, t('Save configuration'));
+    $this->assertField('edit-mappings-xx-browser-langcode', 'xx', 'Browser language code found.');
+    $this->assertField('edit-mappings-xx-drupal-langcode', 'en', 'Drupal language code found.');
+
+    // Add the same custom mapping again.
+    $this->drupalPost('admin/config/regional/language/detection/browser', $edit, t('Save configuration'));
+    $this->assertText('Browser language codes must be unique.', 'Error message displayed.');
+
+    // Add a new custom mapping with only a browser language code.
+    $edit = array(
+      'mappings[_new][browser_langcode]' => 'xxxx',
+      'mappings[_new][drupal_langcode]' => '',
+    );
+    $this->drupalPost('admin/config/regional/language/detection/browser', $edit, t('Save configuration'));
+    $this->assertText(t('Both browser and Drupal language codes must be provided.'), 'Error message displayed.');
+
+    // Add a new custom mapping with only a Drupal language code.
+    $edit = array(
+      'mappings[_new][browser_langcode]' => '',
+      'mappings[_new][drupal_langcode]' => 'en',
+    );
+    $this->drupalPost('admin/config/regional/language/detection/browser', $edit, t('Save configuration'));
+    $this->assertText(t('Both browser and Drupal language codes must be provided.'), 'Error message displayed.');
+
+    // Change browser language code of our custom mapping to zh-sg.
+    $edit = array(
+      'mappings[xx][browser_langcode]' => 'zh-sg',
+      'mappings[xx][drupal_langcode]' => 'en',
+    );
+    $this->drupalPost('admin/config/regional/language/detection/browser', $edit, t('Save configuration'));
+    $this->assertText(t('Browser language codes must be unique.'), 'Error message displayed.');
+
+    // Change Drupal language code of our custom mapping to zh-hans.
+    $edit = array(
+      'mappings[xx][browser_langcode]' => 'xx',
+      'mappings[xx][drupal_langcode]' => 'zh-hans',
+    );
+    $this->drupalPost('admin/config/regional/language/detection/browser', $edit, t('Save configuration'));
+    $this->assertField('edit-mappings-xx-browser-langcode', 'xx', 'Browser language code found.');
+    $this->assertField('edit-mappings-xx-drupal-langcode', 'zh-hans', 'Drupal language code found.');
+  }
 }
