diff --git a/includes/locale.inc b/includes/locale.inc
index c168da0..0f98009 100644
--- a/includes/locale.inc
+++ b/includes/locale.inc
@@ -118,6 +118,14 @@ function locale_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.
@@ -142,7 +150,18 @@ function locale_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.
@@ -161,9 +180,23 @@ function locale_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, we can not split $langcode on the
+    // first occurence of '-' otherwise we get a non-existing language zh.
+    // All other languages use a langcode without a '-', so we can safely split
+    // on the first occurence of it.
+    $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/modules/locale/locale.admin.inc b/modules/locale/locale.admin.inc
index 2535357..7b69fb2 100644
--- a/modules/locale/locale.admin.inc
+++ b/modules/locale/locale.admin.inc
@@ -1436,3 +1436,201 @@ function locale_date_format_reset_form_submit($form, &$form_state) {
     ->execute();
   $form_state['redirect'] = 'admin/config/regional/date-time/locale';
 }
+
+
+/**
+ * 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.
+  $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_list();
+    $default = key($language_options);
+  }
+  else {
+    $default = key($existing_languages);
+    $language_options = array(
+      t('Existing languages') => $existing_languages,
+      t('Languages not yet added') => _locale_prepare_predefined_list(),
+    );
+  }
+
+  $form['mappings'] = array(
+    '#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['new_mapping'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Add a new mapping'),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+    '#tree' => TRUE,
+  );
+  $form['new_mapping']['browser_langcode'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Browser language code'),
+    '#description' => t('Use language codes as <a href="@w3ctags">defined by the W3C</a> for interoperability. <em>Examples: "en", "en-gb" and "zh-hant".</em>', array('@w3ctags' => 'http://www.w3.org/International/articles/language-tags/')),
+    '#default_value' => '',
+    '#size' => 20,
+  );
+  $form['new_mapping']['drupal_langcode'] = array(
+    '#type' => 'select',
+    '#title' => t('Drupal language'),
+    '#options' => $language_options,
+    '#default_value' => '',
+  );
+
+  $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']);
+    $row[] = l(t('Delete'), 'admin/config/regional/language/configure/browser/delete/' . $key, $link_attributes);
+
+    $rows[] = array(
+      'data' => $row,
+    );
+  }
+
+  $header = array(
+    t('Browser language code'),
+    t('Drupal language'),
+    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) {
+  // Array to check if all browser language codes are unique.
+  $unique_values = array();
+
+  // Check all mappings.
+  $mappings = array();
+  if (isset($form_state['values']['mappings'])) {
+    $mappings = $form_state['values']['mappings'];
+    foreach ($mappings as $key => $data) {
+      // Make sure browser_langcode is unique.
+      if (array_key_exists($data['browser_langcode'], $unique_values)) {
+        form_set_error('mappings][' . $key . '][browser_langcode', t('Browser language codes must be unique.'));
+      }
+      elseif (preg_match('/[^a-z\-]/', $data['browser_langcode'])) {
+        form_set_error('mappings][' . $key . '][browser_langcode', t('Browser language codes can only contain lowercase letters and a hyphen(-).'));
+      }
+      $unique_values[$data['browser_langcode']] = $data['drupal_langcode'];
+    }
+  }
+
+  // Check new mapping.
+  $data = $form_state['values']['new_mapping'];
+  if (!empty($data['browser_langcode'])) {
+    // Make sure browser_langcode is unique.
+    if (array_key_exists($data['browser_langcode'], $unique_values)) {
+      form_set_error('mappings][' . $key . '][browser_langcode', t('Browser language codes must be unique.'));
+    }
+    elseif (preg_match('/[^a-z\-]/', $data['browser_langcode'])) {
+      form_set_error('mappings][' . $key . '][browser_langcode', t('Browser language codes can only contain lowercase letters and a hyphen(-).'));
+    }
+    $unique_values[$data['browser_langcode']] = $data['drupal_langcode'];
+  }
+
+  $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_state['redirect'] = 'admin/config/regional/language/configure';
+}
+
+/**
+ * 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/configure/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/configure/browser';
+}
\ No newline at end of file
diff --git a/modules/locale/locale.install b/modules/locale/locale.install
index b4db757..c4493a8 100644
--- a/modules/locale/locale.install
+++ b/modules/locale/locale.install
@@ -24,6 +24,17 @@ function locale_install() {
       'javascript' => '',
     ))
     ->execute();
+
+  // Set default mappings for Chinese languages.
+  language_set_browser_drupal_langcode_mappings(array(
+    '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/modules/locale/locale.module b/modules/locale/locale.module
index 94e7cd1..25a78f2 100644
--- a/modules/locale/locale.module
+++ b/modules/locale/locale.module
@@ -47,6 +47,9 @@ function locale_help($path, $arg) {
     case 'admin/config/regional/language/configure/session':
       $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/configure/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 <a href="@configure-languages">languages used by Drupal</a>.', array('@configure-languages' => url('admin/config/regional/language'))) . '</p>';
+      return $output;
     case 'admin/config/regional/translate':
       $output = '<p>' . t('This page provides an overview of available translatable strings. Drupal displays translatable strings in text groups; modules may define additional text groups containing other translatable strings. Because text groups provide a method of grouping related strings, they are often used to focus translation efforts on specific areas of the Drupal interface.') . '</p>';
       $output .= '<p>' . t('See the <a href="@languages">Languages page</a> for more information on adding support for additional languages.', array('@languages' => url('admin/config/regional/language'))) . '</p>';
@@ -214,7 +217,21 @@ function locale_menu() {
     'access arguments' => array('administer site configuration'),
     'file' => 'locale.admin.inc',
   );
-
+  $items['admin/config/regional/language/configure/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' => 'locale.admin.inc',
+    'type' => MENU_VISIBLE_IN_BREADCRUMB,
+  );
+  $items['admin/config/regional/language/configure/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' => 'locale.admin.inc',
+  );
   return $items;
 }
 
@@ -428,6 +445,10 @@ function locale_theme() {
     'locale_date_format_form' => array(
       'render element' => 'form',
     ),
+    'language_negotiation_configure_browser_form_table' => array(
+      'render element' => 'form',
+      'file' => 'locale.admin.inc',
+    ),
   );
 }
 
@@ -573,6 +594,7 @@ function locale_language_negotiation_info() {
     'cache' => 0,
     'name' => t('Browser'),
     'description' => t("Determine the language from the browser's language settings."),
+    'config' => 'admin/config/regional/language/configure/browser',
   );
 
   $providers[LOCALE_LANGUAGE_NEGOTIATION_INTERFACE] = array(
@@ -1072,3 +1094,26 @@ function locale_form_comment_form_alter(&$form, &$form_state, $form_id) {
     $form['language']['#value'] = $language_content->language;
   }
 }
+
+
+/**
+ * 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() {
+  return variable_get('locale_language_mappings', array());
+}
+
+/**
+ * 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) {
+  variable_set('locale_language_mappings', $mappings);
+}
diff --git a/modules/locale/locale.test b/modules/locale/locale.test
index 632506e..e0ff37e 100644
--- a/modules/locale/locale.test
+++ b/modules/locale/locale.test
@@ -1637,7 +1637,7 @@ class LocaleLanguageSwitchingFunctionalTest extends DrupalWebTestCase {
 /**
  * Test browser language detection.
  */
-class LocaleBrowserDetectionTest extends DrupalUnitTestCase {
+class LocaleBrowserDetectionTest extends DrupalWebTestCase {
 
   public static function getInfo() {
     return array(
@@ -1651,8 +1651,10 @@ class LocaleBrowserDetectionTest extends DrupalUnitTestCase {
    * Unit tests for the locale_language_from_browser() function.
    */
   function testLanguageFromBrowser() {
-    // Load the required functions.
-    require_once DRUPAL_ROOT . '/includes/locale.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'.
@@ -1682,6 +1684,16 @@ class LocaleBrowserDetectionTest extends DrupalUnitTestCase {
       'eh-oh-laa-laa' => (object) array(
         'language' => 'eh-oh-laa-laa',
       ),
+      // Chinese languages.
+      'zh-hans' => (object) array(
+        'language' => 'zh-hans',
+      ),
+      'zh-hant' => (object) array(
+        'language' => 'zh-hant',
+      ),
+      'zh-hant-tw' => (object) array(
+        'language' => 'zh-hant',
+      ),
     );
 
     $test_cases = array(
@@ -1690,8 +1702,8 @@ class LocaleBrowserDetectionTest extends DrupalUnitTestCase {
       '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',
@@ -1744,6 +1756,21 @@ class LocaleBrowserDetectionTest extends DrupalUnitTestCase {
       '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) {
@@ -1752,6 +1779,72 @@ class LocaleBrowserDetectionTest extends DrupalUnitTestCase {
       $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/configure');
+    $this->assertLinkByHref('admin/config/regional/language/configure/browser');
+
+    // Check that defaults are loaded from language.mappings.yml.
+    $this->drupalGet('admin/config/regional/language/configure/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/configure/browser/delete/' . $browser_langcode);
+    $message = t('Are you sure you want to delete @browser_langcode?', array(
+      '@browser_langcode' => $browser_langcode,
+    ));
+    $this->assertRaw($message);
+
+    // Confirm the delete.
+    $edit = array();
+    $this->drupalPost('admin/config/regional/language/configure/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(
+      'new_mapping[browser_langcode]' => 'xx',
+      'new_mapping[drupal_langcode]' => 'en',
+    );
+    $this->drupalPost('admin/config/regional/language/configure/browser', $edit, t('Save configuration'));
+    $this->drupalGet('admin/config/regional/language/configure/browser');
+    $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/configure/browser', $edit, t('Save configuration'));
+    $this->assertText('Browser language codes must be unique.');
+
+    // 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/configure/browser', $edit, t('Save configuration'));
+    $this->assertText(t('Browser language codes must be unique.'));
+
+    // 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/configure/browser', $edit, t('Save configuration'));
+    $this->drupalGet('admin/config/regional/language/configure/browser');
+    $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.');
+  }
 }
 
 /**
