Commit 369d68af authored by webchick's avatar webchick

Issue #365615 by attiks, smokris, penyaskito, Gábor Hojtsy, Bojhan, WormFood,...

Issue #365615 by attiks, smokris, penyaskito, Gábor Hojtsy, Bojhan, WormFood, yang_yi_cn: Fixed Language detection not working correctly for most Chinese readers.
parent a7c17d91
......@@ -1368,6 +1368,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();
......
# 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
......@@ -813,3 +813,200 @@ 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.
$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(
'#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 langauge'),
'#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/detection/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/detection';
}
/**
* 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';
}
......@@ -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 <a href="@configure-languages">languages used by Drupal</a>.', array('@configure-languages' => url('admin/config/regional/language'))) . '</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,31 @@ 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()) {
return array();
}
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();
}
......@@ -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,23 @@ 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, 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;
}
}
......
......@@ -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 @@ public static function getInfo() {
/**
* 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 @@ function testLanguageFromBrowser() {
'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 @@ function testLanguageFromBrowser() {
'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 @@ function testLanguageFromBrowser() {
'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,70 @@ function testLanguageFromBrowser() {
$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);
// 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(
'new_mapping[browser_langcode]' => 'xx',
'new_mapping[drupal_langcode]' => 'en',
);
$this->drupalPost('admin/config/regional/language/detection/browser', $edit, t('Save configuration'));
$this->drupalGet('admin/config/regional/language/detection/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/detection/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/detection/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/detection/browser', $edit, t('Save configuration'));
$this->drupalGet('admin/config/regional/language/detection/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.');
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment