diff --git a/core/includes/language.inc b/core/includes/language.inc index 896cf69..32d1384 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -15,6 +15,11 @@ const LANGUAGE_NEGOTIATION_SELECTED = 'language-selected'; /** + * The language is determined using the current interface language. + */ +const LANGUAGE_NEGOTIATION_INTERFACE = 'language-interface'; + +/** * @defgroup language_negotiation Language Negotiation API functionality * @{ * Functions to customize the language types and the negotiation process. @@ -52,6 +57,10 @@ * } * @endcode * + * The locked configuration property prevents one language type from being + * switched from customized to not customized, and vice versa. + * @see language_types_set() + * * Every language type can have a different set of language negotiation methods * assigned to it. Different language types often share the same language * negotiation settings, but they can have independent settings if needed. If @@ -167,35 +176,11 @@ function language_types_info() { * whose language negotiation methods are module-defined and not altered through * the user interface. * - * @param $stored - * (optional) By default, retrieves values from the 'language_types' variable - * to avoid unnecessary hook invocations. If set to FALSE, retrieves values - * from the actual language type definitions. This allows reaction to - * alterations performed on the definitions by modules installed after the - * 'language_types' variable is set. - * * @return * An array of language type names. */ -function language_types_get_configurable($stored = TRUE) { - $configurable = &drupal_static(__FUNCTION__); - - if ($stored && !isset($configurable)) { - $types = variable_get('language_types', language_types_get_default()); - $configurable = array_keys(array_filter($types)); - } - - if (!$stored) { - $result = array(); - foreach (language_types_info() as $type => $info) { - if (!isset($info['fixed'])) { - $result[] = $type; - } - } - return $result; - } - - return $configurable; +function language_types_get_configurable() { + return Drupal::config('system.language.types')->get('configurable'); } /** @@ -216,37 +201,62 @@ function language_types_disable($types) { /** * Updates the language type configuration. + * + * @param array $configurables + * The configuration object containing the user defined preferences for + * language type customizability. + * @todo We are using variable_set('language_types', $language_types) and + * config('system.language.types')->set('configurable', $configurable)->save() + * at the same time. This is redundant. We need to change variable_set to + * configuration in http://drupal.org/node/2010542. */ -function language_types_set() { +function language_types_set(array $configurables) { // Ensure that we are getting the defined language negotiation information. An // invocation of module_enable() or module_disable() could outdate the cached // information. drupal_static_reset('language_types_info'); drupal_static_reset('language_negotiation_info'); + $configurable = array(); - // Determine which language types are configurable and which not by checking - // whether the 'fixed' key is defined. Non-configurable (fixed) language types - // have their language negotiation settings stored there. $language_types = array(); $negotiation_info = language_negotiation_info(); foreach (language_types_info() as $type => $info) { - if (isset($info['fixed'])) { - $language_types[$type] = FALSE; - $method_weights = array(); - foreach ($info['fixed'] as $weight => $method_id) { - if (isset($negotiation_info[$method_id])) { - $method_weights[$method_id] = $weight; + $configurable[$type] = in_array($type, $configurables); + // Check if the language is locked. The configuration of a locked language + // type cannot be changed using the UI. + if ($locked = !empty($info['locked'])) { + // Locked languages with a default configuration (using the 'fixed' + // property) are always non-configurable and viceversa. + $configurable[$type] = empty($info['fixed']); + $language_types[$type] = !$locked; + if (!$configurable[$type]) { + $method_weights = array(); + foreach ($info['fixed'] as $weight => $method_id) { + if (isset($negotiation_info[$method_id])) { + $method_weights[$method_id] = $weight; + } } + language_negotiation_set($type, $method_weights); } - language_negotiation_set($type, $method_weights); } else { - $language_types[$type] = TRUE; + // The configuration of an unlocked language will be based on the + // $configurable array (stored in configuration). These values may be + // altered using the UI. + $language_types[$type] = !empty($configurable[$type]); + if (!$language_types[$type] && empty($info['fixed'])) { + // If the language is not configurable and there is no default language + // negotiation setting then provide one. + $method_weights = array(LANGUAGE_NEGOTIATION_INTERFACE); + $method_weights = array_flip($method_weights); + language_negotiation_set($type, $method_weights); + } } } // Save enabled language types. variable_set('language_types', $language_types); + config('system.language.types')->set('configurable', array_keys(array_filter($configurable)))->save(); // Ensure that subsequent calls of language_types_get_configurable() return // the updated language type information. @@ -367,7 +377,7 @@ function language_negotiation_set($type, $method_weights) { $negotiation = array(); $negotiation_info = language_negotiation_info(); - $default_types = language_types_get_configurable(FALSE); + $default_types = language_types_get_configurable(); // Order the language negotiation method list by weight. asort($method_weights); diff --git a/core/modules/language/language.admin.inc b/core/modules/language/language.admin.inc index 33eaa6b..d01b4d9 100644 --- a/core/modules/language/language.admin.inc +++ b/core/modules/language/language.admin.inc @@ -357,11 +357,17 @@ function language_negotiation_configure_form() { $form = array( '#submit' => array('language_negotiation_configure_form_submit'), '#theme' => 'language_negotiation_configure_form', - '#language_types' => language_types_get_configurable(FALSE), '#language_types_info' => language_types_info(), '#language_negotiation_info' => language_negotiation_info(), ); - + $form['#language_types'] = array(); + $configurable = config('system.language.types')->get('configurable'); + foreach ($form['#language_types_info'] as $type => $info) { + // Show locked language types only if they are configurable. + if (empty($info['locked']) || in_array($type, $configurable)) { + $form['#language_types'][] = $type; + } + } foreach ($form['#language_types'] as $type) { language_negotiation_configure_form_table($form, $type); } @@ -389,6 +395,21 @@ function language_negotiation_configure_form_table(&$form, $type) { '#show_operations' => FALSE, 'weight' => array('#tree' => TRUE), ); + // Only show configurability checkbox for the unlocked language types. + if (empty($info['locked'])) { + $configurable = config('system.language.types')->get('configurable'); + $table_form['configurable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize %language_name language detection to differ from User interface text language detection settings.', array('%language_name' => $info['name'])), + '#default_value' => in_array($type, $configurable), + '#attributes' => array('class' => array('language-customization-checkbox')), + '#attached' => array( + 'library' => array( + array('language', 'language.admin') + ), + ), + ); + } $negotiation_info = $form['#language_negotiation_info']; $enabled_methods = variable_get("language_negotiation_$type", array()); @@ -519,12 +540,13 @@ function theme_language_negotiation_configure_form($variables) { 'rows' => $rows, 'attributes' => array('id' => "language-negotiation-methods-$type"), ); - $table = theme('table', $variables); + $table = drupal_render($form[$type]['configurable']); + $table .= theme('table', $variables); $table .= drupal_render_children($form[$type]); drupal_add_tabledrag("language-negotiation-methods-$type", 'order', 'sibling', "language-method-weight-$type"); - $output .= '
' . $title . $description . $table . '
'; + $output .= '
' . $title . $description . $table . '
'; } $output .= drupal_render_children($form); @@ -537,11 +559,19 @@ function theme_language_negotiation_configure_form($variables) { function language_negotiation_configure_form_submit($form, &$form_state) { $configurable_types = $form['#language_types']; + $configurables = config('system.language.types')->get('configurable'); + $configurable = array(); + $method_weights_type = array(); + foreach ($configurable_types as $type) { + $configurable[$type] = in_array($type, $configurables); $method_weights = array(); $enabled_methods = $form_state['values'][$type]['enabled']; $enabled_methods[LANGUAGE_NEGOTIATION_SELECTED] = TRUE; $method_weights_input = $form_state['values'][$type]['weight']; + if (isset($form_state['values'][$type]['configurable'])) { + $configurable[$type] = !empty($form_state['values'][$type]['configurable']); + } foreach ($method_weights_input as $method_id => $weight) { if ($enabled_methods[$method_id]) { @@ -549,13 +579,28 @@ function language_negotiation_configure_form_submit($form, &$form_state) { } } - language_negotiation_set($type, $method_weights); + $method_weights_type[$type] = $method_weights; variable_set("language_negotiation_methods_weight_$type", $method_weights_input); } // Update non-configurable language types and the related language negotiation // configuration. - language_types_set(); + language_types_set(array_keys(array_filter($configurable))); + + // Update the language negotiations after setting the configurability. + foreach ($method_weights_type as $type => $method_weights) { + language_negotiation_set($type, $method_weights); + } + + // Clear block definitions cache since the available blocks and their names + // may have been changed based on the configurable types. + if (module_exists('block')) { + // If there is an active language switcher for a language type that has been + // made not configurable, deactivate it first. + $non_configurable = array_keys(array_diff($configurable, array_filter($configurable))); + _language_disable_language_switcher($non_configurable); + Drupal::service('plugin.manager.block')->clearCachedDefinitions(); + } $form_state['redirect'] = 'admin/config/regional/language/detection'; drupal_set_message(t('Language negotiation configuration saved.')); @@ -933,3 +978,21 @@ function language_content_settings_form_submit(array $form, array &$form_state) } drupal_set_message(t('Settings successfully updated.')); } + +/** + * Helper function to disable the language switcher blocks. + * + * @param array $language_types + * Array containing all language types whose language switchers need to be + * disabled. + */ +function _language_disable_language_switcher(array $language_types) { + $blocks = _block_rehash(); + foreach ($language_types as $language_type) { + foreach ($blocks as $block) { + if (strpos($block->id, 'language_switcher_' . substr($language_type, 9)) !== FALSE) { + $block->delete(); + } + } + } +} diff --git a/core/modules/language/language.admin.js b/core/modules/language/language.admin.js new file mode 100644 index 0000000..7e974b0 --- /dev/null +++ b/core/modules/language/language.admin.js @@ -0,0 +1,32 @@ +(function ($, Drupal) { + +"use strict"; + +/** + * Makes language negotiation inherit user interface negotiation. + */ +Drupal.behaviors.negotiationLanguage = { + attach: function () { + var $configForm = $('#language-negotiation-configure-form'); + var inputSelector = 'input[name$="[configurable]"]'; + // Given a customization checkbox derive the language type being changed. + function toggleTable (checkbox) { + var $checkbox = $(checkbox); + // Get the language detection type such as User interface text language + // detection or Content language detection. + $checkbox.closest('.table-language-group') + .find('table, .tabledrag-toggle-weight') + .toggle($checkbox.prop('checked')); + } + // Bind hide/show and rearrange customization checkboxes. + $configForm.once('negotiation-language-admin-bind').on('change', inputSelector, function (event) { + toggleTable(event.target); + }); + // Initially, hide language detection types that are not customized. + $configForm.find(inputSelector + ':not(:checked)').each(function (index, element) { + toggleTable(element); + }); + } +}; + +})(jQuery, Drupal); diff --git a/core/modules/language/language.install b/core/modules/language/language.install index b16c53b..78d3a36 100644 --- a/core/modules/language/language.install +++ b/core/modules/language/language.install @@ -22,7 +22,7 @@ function language_install() { // Enable URL language detection for each configurable language type. require_once DRUPAL_ROOT . '/core/includes/language.inc'; - foreach (language_types_get_configurable(FALSE) as $type) { + foreach (language_types_get_configurable() as $type) { language_negotiation_set($type, array(LANGUAGE_NEGOTIATION_URL => 0)); } } diff --git a/core/modules/language/language.module b/core/modules/language/language.module index a6ab9d2..44ae1ea 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -562,6 +562,26 @@ function language_delete($langcode) { } /** + * Implements hook_library_info(). + */ +function language_library_info() { + $libraries['language.admin'] = array( + 'title' => 'Language detection admin', + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'language') . '/language.admin.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'jquery.once'), + ), + ); + + return $libraries; +} + +/** * Implements hook_css_alter(). * * This function checks all CSS files currently added via drupal_add_css() and @@ -609,14 +629,17 @@ function language_language_types_info() { Language::TYPE_INTERFACE => array( 'name' => t('User interface text'), 'description' => t('Order of language detection methods for user interface text. If a translation of user interface text is available in the detected language, it will be displayed.'), + 'locked' => TRUE, ), Language::TYPE_CONTENT => array( 'name' => t('Content'), 'description' => t('Order of language detection methods for content. If a version of content is available in the detected language, it will be displayed.'), 'fixed' => array(LANGUAGE_NEGOTIATION_INTERFACE), + 'locked' => TRUE, ), Language::TYPE_URL => array( 'fixed' => array(LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_URL_FALLBACK), + 'locked' => TRUE, ), ); } @@ -716,8 +739,10 @@ function language_negotiation_include() { * Implements hook_modules_enabled(). */ function language_modules_enabled($modules) { - language_negotiation_include(); - language_types_set(); + include_once DRUPAL_ROOT . '/core/includes/language.inc'; + // Load configurability options from configuration. + $configurable = config('system.language.types')->get('configurable'); + language_types_set($configurable); language_negotiation_purge(); } diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc index d205d76..cb07f9a 100644 --- a/core/modules/language/language.negotiation.inc +++ b/core/modules/language/language.negotiation.inc @@ -20,11 +20,6 @@ const LANGUAGE_NEGOTIATION_BROWSER = 'language-browser'; /** - * The language is determined using the current interface language. - */ -const LANGUAGE_NEGOTIATION_INTERFACE = 'language-interface'; - -/** * If no URL language, language is determined using an already detected one. */ const LANGUAGE_NEGOTIATION_URL_FALLBACK = 'language-url-fallback'; diff --git a/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php b/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php index d451b27..f5260f9 100644 --- a/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php +++ b/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php @@ -38,11 +38,17 @@ public function getDerivativeDefinition($derivative_id, array $base_plugin_defin public function getDerivativeDefinitions(array $base_plugin_definition) { include_once DRUPAL_ROOT . '/core/includes/language.inc'; $info = language_types_info(); - foreach (language_types_get_configurable(FALSE) as $type) { + $configurable_types = language_types_get_configurable(); + foreach ($configurable_types as $type) { $this->derivatives[$type] = $base_plugin_definition; $this->derivatives[$type]['admin_label'] = t('Language switcher (!type)', array('!type' => $info[$type]['name'])); $this->derivatives[$type]['cache'] = DRUPAL_NO_CACHE; } + // If there is just one configurable type then change the title of the + // block. + if (count($configurable_types) == 1) { + $this->derivatives[reset($configurable_types)]['admin_label'] = t('Language switcher'); + } return $this->derivatives; } diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php index baef5ca..4e2ab6e 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php @@ -56,8 +56,8 @@ function testInfoAlterations() { state()->set('language_test.content_language_type', TRUE); $this->languageNegotiationUpdate(); $type = Language::TYPE_CONTENT; - $language_types = variable_get('language_types', language_types_get_default()); - $this->assertTrue($language_types[$type], 'Content language type is configurable.'); + $language_types = language_types_get_configurable(); + $this->assertTrue(in_array($type, $language_types), 'Content language type is configurable.'); // Enable some core and custom language negotiation methods. The test // language type is supposed to be configurable. @@ -69,6 +69,7 @@ function testInfoAlterations() { $form_field => TRUE, $type . '[enabled][' . $test_method_id . ']' => TRUE, $test_type . '[enabled][' . $test_method_id . ']' => TRUE, + $test_type . '[configurable]' => TRUE, ); $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings')); @@ -158,8 +159,9 @@ protected function languageNegotiationUpdate($op = 'enable') { */ protected function checkFixedLanguageTypes() { drupal_static_reset('language_types_info'); + $configurable = language_types_get_configurable(); foreach (language_types_info() as $type => $info) { - if (isset($info['fixed'])) { + if (!in_array($type, $configurable) && isset($info['fixed'])) { $negotiation = variable_get("language_negotiation_$type", array()); $equal = count($info['fixed']) == count($negotiation); while ($equal && list($id) = each($negotiation)) { diff --git a/core/modules/language/tests/language_test/language_test.module b/core/modules/language/tests/language_test/language_test.module index b83373a..f5d29f0 100644 --- a/core/modules/language/tests/language_test/language_test.module +++ b/core/modules/language/tests/language_test/language_test.module @@ -32,6 +32,7 @@ function language_test_language_types_info() { ), 'fixed_test_language_type' => array( 'fixed' => array('test_language_negotiation_method'), + 'locked' => TRUE, ), ); } @@ -42,7 +43,15 @@ function language_test_language_types_info() { */ function language_test_language_types_info_alter(array &$language_types) { if (state()->get('language_test.content_language_type')) { + $language_types[Language::TYPE_CONTENT]['locked'] = FALSE; unset($language_types[Language::TYPE_CONTENT]['fixed']); + // By default languages are not configurable. Make Language::TYPE_CONTENT + // configurable. + $configurable = config('system.language.types')->get('configurable'); + if (!in_array(Language::TYPE_CONTENT, $configurable)) { + $configurable[] = Language::TYPE_CONTENT; + config('system.language.types')->set('configurable', $configurable)->save(); + } } } diff --git a/core/modules/system/config/system.language.types.yml b/core/modules/system/config/system.language.types.yml new file mode 100644 index 0000000..64bdf43 --- /dev/null +++ b/core/modules/system/config/system.language.types.yml @@ -0,0 +1,2 @@ +configurable: + - language_interface diff --git a/core/modules/system/language.api.php b/core/modules/system/language.api.php index fb165a4..7e7d6b9 100644 --- a/core/modules/system/language.api.php +++ b/core/modules/system/language.api.php @@ -44,10 +44,13 @@ function hook_language_switch_links_alter(array &$links, $type, $path) { * may contain the following elements: * - name: The human-readable language type identifier. * - description: A description of the language type. + * - locked: A boolean indicating if the user can choose wether to configure + * the language type or not using the UI. * - fixed: A fixed array of language negotiation method identifiers to use to - * initialize this language. Defining this key makes the language type - * non-configurable, so it will always use the specified methods in the - * given priority order. Omit to make the language type configurable. + * initialize this language. If locked is set to TRUE and fixed is set, it + * will always use the specified methods in the given priority order. If not + * present and locked is TRUE then LANGUAGE_NEGOTIATION_INTERFACE will be + * used. * * @see hook_language_types_info_alter() * @ingroup language_negotiation @@ -57,8 +60,10 @@ function hook_language_types_info() { 'custom_language_type' => array( 'name' => t('Custom language'), 'description' => t('A custom language type.'), + 'locked' => FALSE, ), 'fixed_custom_language_type' => array( + 'locked' => TRUE, 'fixed' => array('custom_language_negotiation_method'), ), ); diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php index 4244c13..65167f8 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php @@ -543,7 +543,7 @@ function testGetLibrary() { $this->assertTrue(isset($libraries['jquery.farbtastic']), 'Retrieved all module libraries.'); // Retrieve all libraries for a module not implementing hook_library_info(). // Note: This test installs language module. - $libraries = drupal_get_library('language'); + $libraries = drupal_get_library('dblog'); $this->assertEqual($libraries, array(), 'Retrieving libraries from a module not implementing hook_library_info() returns an emtpy array.'); // Retrieve a specific library by module and name. diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 3824136..f57fba8 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -2194,6 +2194,19 @@ function system_update_8056() { } /** + * Move variable language_types to config. + * + * @ingroup config_upgrade + */ +function system_update_8057() { + update_variables_to_config('system.language.types', array( + 'language_interface' => 'configurable.language_interface', + 'language_content' => 'configurable.language_content', + 'language_url' => 'configurable.language_content', + )); +} + +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module index f51c2cb..0aa5981 100644 --- a/core/modules/translation_entity/translation_entity.module +++ b/core/modules/translation_entity/translation_entity.module @@ -63,8 +63,9 @@ function translation_entity_module_implements_alter(&$implementations, $hook) { * Implements hook_language_type_info_alter(). */ function translation_entity_language_types_info_alter(array &$language_types) { - // Make content language negotiation configurable by removing its predefined - // configuration. + // Make content language negotiation configurable by removing the 'locked' + // flag. + $language_types[Language::TYPE_CONTENT]['locked'] = FALSE; unset($language_types[Language::TYPE_CONTENT]['fixed']); }