diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index e53e895..5f33c28 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1310,6 +1310,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..913714e
--- /dev/null
+++ b/core/modules/language/config/language.mappings.yml
@@ -0,0 +1,7 @@
+zh-tw: zh-hant-tw
+zh-hk: zh-hant-hk
+zh-mo: zh-hant-mo
+zh-cht: zh-hant
+zh-cn: zh-hans-cn
+zh-sg: zh-hans-sg
+zh-chs: zh-hans
diff --git a/core/modules/language/language.admin.inc b/core/modules/language/language.admin.inc
index b388986..b0abcf4 100644
--- a/core/modules/language/language.admin.inc
+++ b/core/modules/language/language.admin.inc
@@ -807,3 +807,192 @@ 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();
+
+  $form['info'] = array(
+    '#type' => 'markup',
+    '#markup' => t('You can add custom mappings to map browser languages to Drupal languages.'),
+  );
+
+  $form['mappings'] = array(
+    '#type' => 'container',
+    '#tree' => TRUE,
+    '#theme' => 'language_negotiation_configure_browser_form_table',
+  );
+
+  $mappings = language_get_mappings();
+  foreach ($mappings as $browser_langcode => $drupal_langcode) {
+    $form['mappings'][$browser_langcode] = array(
+      'browser_langcode' => array(
+        '#type' => 'textfield',
+        '#default_value' => $browser_langcode,
+        '#size' => 20,
+      ),
+      'drupal_langcode' => array(
+        '#type' => 'textfield',
+        '#default_value' => $drupal_langcode,
+        '#size' => 20,
+      ),
+    );
+  }
+
+  // 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_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_mappings();
+  if (array_key_exists($browser_langcode, $mappings)) {
+    unset($mappings[$browser_langcode]);
+    language_set_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 f90ef93..837ca51 100644
--- a/core/modules/language/language.module
+++ b/core/modules/language/language.module
@@ -116,6 +116,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 +169,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',
+    ),
   );
 }
 
@@ -391,6 +410,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(
@@ -596,3 +616,24 @@ function language_url_outbound_alter(&$path, &$options, $original_path) {
     }
   }
 }
+
+/**
+ * Returns language mappings.
+ */
+function language_get_mappings() {
+  $config = config('language.mappings');
+  if ($config->isNew()) {
+    config_install_default_config('module', 'language');
+    $config = config('language.mappings');
+  }
+  return $config->get();
+}
+
+/**
+ * Stores language mappings.
+ */
+function language_set_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 48a8083..a363217 100644
--- a/core/modules/language/language.negotiation.inc
+++ b/core/modules/language/language.negotiation.inc
@@ -81,7 +81,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_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.
@@ -100,9 +111,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..e42a319 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(
@@ -27,9 +29,6 @@ class LanguageBrowserDetectionUnitTest extends UnitTestBase {
    * Unit tests for the language_from_browser() function.
    */
   function testLanguageFromBrowser() {
-    // Load the required functions.
-    require_once DRUPAL_ROOT . '/core/modules/language/language.negotiation.inc';
-
     $languages = array(
       // In our test case, 'en' has priority over 'en-US'.
       'en' => new Language(array(
@@ -58,6 +57,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 +75,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 +129,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 +152,80 @@ 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')));
     }
   }
+
+  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->assertText($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]' => 'yy',
+    );
+    $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', 'yy', '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]' => 'yyyy',
+    );
+    $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]' => 'yy',
+    );
+    $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-sg.
+    $edit = array(
+      'mappings[xx][browser_langcode]' => 'xx',
+      'mappings[xx][drupal_langcode]' => 'zh-hans-sg',
+    );
+    $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-sg', 'Drupal language code found.');
+  }
 }
