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..4020fd1 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); + } +} + +/** + * Delete 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, ''); +} + +/** + * 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..f916664 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( @@ -26,10 +28,7 @@ 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'; - + function testLanguamergeFromBrowser() { $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.'); + } }