Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.303 diff -u -p -r1.303 bootstrap.inc --- includes/bootstrap.inc 5 Sep 2009 13:05:30 -0000 1.303 +++ includes/bootstrap.inc 11 Sep 2009 14:47:58 -0000 @@ -159,28 +159,19 @@ define('DRUPAL_AUTHENTICATED_RID', 2); define('DRUPAL_KILOBYTE', 1024); /** - * No language negotiation. The default language is used. + * The type of language used to define the content language. */ -define('LANGUAGE_NEGOTIATION_NONE', 0); +define('LANGUAGE_TYPE_CONTENT', 'language'); /** - * Path based negotiation with fallback to default language - * if no defined path prefix identified. + * The type of language used to select the user interface. */ -define('LANGUAGE_NEGOTIATION_PATH_DEFAULT', 1); +define('LANGUAGE_TYPE_INTERFACE', 'language_interface'); /** - * Path based negotiation with fallback to user preferences - * and browser language detection if no defined path prefix - * identified. + * The type of language used for URLs. */ -define('LANGUAGE_NEGOTIATION_PATH', 2); - -/** - * Domain based negotiation with fallback to default language - * if no language identified by domain. - */ -define('LANGUAGE_NEGOTIATION_DOMAIN', 3); +define('LANGUAGE_TYPE_URL', 'language_url'); /** * Language written left to right. Possible value of $language->direction. @@ -1644,23 +1635,35 @@ function get_t() { } /** - * Choose a language for the current page, based on site and user preferences. + * Initialize all the defined language types. */ function drupal_language_initialize() { - global $language, $user; + $types = variable_get('language_types', drupal_language_types()); // Ensure the language is correctly returned, even without multilanguage support. // Useful for eg. XML/HTML 'lang' attributes. if (variable_get('language_count', 1) == 1) { - $language = language_default(); + $default = language_default(); + foreach ($types as $type) { + $GLOBALS[$type] = $default; + } } else { include_once DRUPAL_ROOT . '/includes/language.inc'; - $language = language_initialize(); + foreach ($types as $type) { + $GLOBALS[$type] = language_initialize($type); + } } } /** + * The built-in language types. + */ +function drupal_language_types() { + return array(LANGUAGE_TYPE_CONTENT, LANGUAGE_TYPE_INTERFACE, LANGUAGE_TYPE_URL); +} + +/** * Get a list of languages set up indexed by the specified key * * @param $field The field to index the list with. Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.987 diff -u -p -r1.987 common.inc --- includes/common.inc 10 Sep 2009 22:10:10 -0000 1.987 +++ includes/common.inc 11 Sep 2009 14:47:58 -0000 @@ -354,6 +354,26 @@ function drupal_query_string_encode($que } /** + * Split an URL-encoded query string into an array. + * + * @param $query + * The query string to split. + * + * @return + * An array of url decoded couples $param_name => $value. + */ +function drupal_query_string_decode($query) { + $result = array(); + if (!empty($query)) { + foreach (explode('&', $query) as $param) { + $param = explode('=', $param); + $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : ''; + } + } + return $result; +} + +/** * Prepare a destination query string for use in combination with drupal_goto(). * * Used to direct the user back to the referring page after completing a form. @@ -1260,12 +1280,12 @@ function fix_gpc_magic() { * The translated string. */ function t($string, array $args = array(), array $options = array()) { - global $language; + global $language_interface; static $custom_strings; // Merge in default. if (empty($options['langcode'])) { - $options['langcode'] = isset($language->language) ? $language->language : 'en'; + $options['langcode'] = isset($language_interface->language) ? $language_interface->language : 'en'; } if (empty($options['context'])) { $options['context'] = ''; @@ -2252,7 +2272,8 @@ function url($path = NULL, array $option $path = ''; } elseif (!empty($path) && !$options['alias']) { - $path = drupal_get_path_alias($path, isset($options['language']) ? $options['language']->language : ''); + $language = isset($options['language']) && isset($options['language']->language) ? $options['language']->language : ''; + $path = drupal_get_path_alias($path, $language); } if (function_exists('custom_url_rewrite_outbound')) { @@ -2357,7 +2378,7 @@ function drupal_attributes(array $attrib * an HTML string containing a link to the given path. */ function l($text, $path, array $options = array()) { - global $language; + global $language_url; // Merge in defaults. $options += array( @@ -2367,7 +2388,7 @@ function l($text, $path, array $options // Append active class. if (($path == $_GET['q'] || ($path == '' && drupal_is_front_page())) && - (empty($options['language']) || $options['language']->language == $language->language)) { + (empty($options['language']) || $options['language']->language == $language_url->language)) { $options['attributes']['class'][] = 'active'; } Index: includes/language.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/language.inc,v retrieving revision 1.19 diff -u -p -r1.19 language.inc --- includes/language.inc 1 Feb 2009 16:45:53 -0000 1.19 +++ includes/language.inc 11 Sep 2009 15:59:46 -0000 @@ -7,138 +7,315 @@ */ /** - * Choose a language for the page, based on language negotiation settings. + * No language negotiation. The default language is used. */ -function language_initialize() { - global $user; +define('LANGUAGE_NEGOTIATION_DEFAULT', 'language-default'); - // Configured presentation language mode. - $mode = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE); - // Get a list of enabled languages. - $languages = language_list('enabled'); - $languages = $languages[1]; - - switch ($mode) { - case LANGUAGE_NEGOTIATION_NONE: - return language_default(); - - case LANGUAGE_NEGOTIATION_DOMAIN: - foreach ($languages as $language) { - $host = parse_url($language->domain, PHP_URL_HOST); - if ($host && ($_SERVER['HTTP_HOST'] == $host)) { - return $language; - } - } - return language_default(); +/** + * Return all the defined language types. + * + * @return + * An array of language type names. The name will be used as the global + * variable name the language value will be stored in. + */ +function language_types_info() { + $language_types = &drupal_static(__FUNCTION__); - case LANGUAGE_NEGOTIATION_PATH_DEFAULT: - case LANGUAGE_NEGOTIATION_PATH: - // $_GET['q'] might not be available at this time, because - // path initialization runs after the language bootstrap phase. - $args = isset($_GET['q']) ? explode('/', $_GET['q']) : array(); - $prefix = array_shift($args); - // Search prefix within enabled languages. - foreach ($languages as $language) { - if (!empty($language->prefix) && $language->prefix == $prefix) { - // Rebuild $GET['q'] with the language removed. - $_GET['q'] = implode('/', $args); - return $language; - } - } - if ($mode == LANGUAGE_NEGOTIATION_PATH_DEFAULT) { - // If we did not found the language by prefix, choose the default. - return language_default(); - } - break; + if (!isset($language_types)) { + $language_types = module_invoke_all('language_types_info'); + // Let other modules alter the list of language types. + drupal_alter('language_types_info', $language_providers); } - // User language. - if ($user->uid && isset($languages[$user->language])) { - return $languages[$user->language]; + return $language_types; +} + +/** + * Return only the configurable language types. + * + * A language type maybe configurable or fixed. A fixed language type is a type + * whose negotiation values are unchangable and defined while defining the + * language type itself. + * + * @return + * An array of language type names. + */ +function language_types_configurable() { + $result = array(); + foreach (language_types_info() as $type => $info) { + if (!isset($info['fixed'])) { + $result[$type] = $info; + } } + return $result; +} - // Browser accept-language parsing. - if ($language = language_from_browser()) { - return $language; +/** + * Uninstall the given language types. + * + * @param $types + * An array of language types. + */ +function language_types_uninstall($types) { + $installed_types = variable_get('language_types', drupal_language_types()); + variable_set('language_types', array_diff($installed_types, $types)); +} + +/** + * Check if a language provider is enabled. + * + * This has two possible behaviors: + * - If $provider_id is given return its ID if enabled, FALSE otherwise. + * - If no ID is passed the first enabled language provider is returned. + * + * @param $type + * The language negotiation type. + * @param $provider_id + * The language provider ID. + * + * @return + * The provider ID if it is enabled, FALSE otherwise. + */ +function language_negotiation_get($type, $provider_id = NULL) { + $negotiation = variable_get("language_negotiation_$type", array()); + if (empty($negotiation)) { + return empty($provider_id) ? LANGUAGE_NEGOTIATION_DEFAULT : FALSE; } + if (empty($provider_id)) { + return key($negotiation); + } + if (isset($negotiation[$provider_id])) { + return $provider_id; + } + return FALSE; +} - // Fall back on the default if everything else fails. - return language_default(); +/** + * Check if the given language provider is enabled for any configurable language + * type. + * + * @param $provider_id + * The language provider ID. + * + * @return + * TRUE if there is at least one language type for which the give language + * provider is enabled, FALSE otherwise. + */ +function language_negotiation_get_any($provider_id) { + foreach (language_types_configurable() as $type => $info) { + if (language_negotiation_get($type, $provider_id)) { + return TRUE; + } + } + return FALSE; } /** - * Identify language from the Accept-language HTTP header we got. + * Save a list of language providers. + * + * @param $type + * The language negotiation type. + * @param $language_providers + * An array of language provider ids. */ -function language_from_browser() { - // Specified by the user via the browser's Accept Language setting - // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" - $browser_langs = array(); - - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - $browser_accept = explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']); - for ($i = 0; $i < count($browser_accept); $i++) { - // The language part is either a code or a code with a quality. - // We cannot do anything with a * code, so it is skipped. - // If the quality is missing, it is assumed to be 1 according to the RFC. - if (preg_match("!([a-z-]+)(;q=([0-9\\.]+))?!", trim($browser_accept[$i]), $found)) { - $browser_langs[$found[1]] = (isset($found[3]) ? (float) $found[3] : 1.0); +function language_negotiation_set($type, $language_providers) { + // Save only the necessary fields. + $provider_fields = array('callback', 'file', 'cache'); + + $negotiation = array(); + $providers_weight = array(); + $defined_providers = language_negotiation_info(); + $default_types = language_types_configurable(); + + // Initialize the providers weight list. + foreach ($language_providers as $id => $provider) { + $providers_weight[$id] = language_provider_weight($provider); + } + + // Order providers list by weight. + asort($providers_weight); + + foreach ($providers_weight as $id => $weight) { + if (isset($defined_providers[$id])) { + $provider = $defined_providers[$id]; + // If the provider does not express any preference about types, make it + // available for any configurable type. + $types = isset($provider['types']) ? array_flip($provider['types']) : $default_types; + // Check if the provider is defined and has the right type. + if (isset($types[$type])) { + $provider_data = array(); + foreach ($provider_fields as $field) { + if (isset($provider[$field])) { + $provider_data[$field] = $provider[$field]; + } + } + $negotiation[$id] = $provider_data; } } } - // Order the codes by quality - arsort($browser_langs); + variable_set("language_negotiation_$type", $negotiation); +} - // Try to find the first preferred language we have - $languages = language_list('enabled'); - foreach ($browser_langs as $langcode => $q) { - if (isset($languages['1'][$langcode])) { - return $languages['1'][$langcode]; +/** + * Return all the defined language providers. + * + * @return + * An array of language providers. + */ +function language_negotiation_info() { + $language_providers = &drupal_static(__FUNCTION__); + + if (!isset($language_providers)) { + // Collect all the module-defined language negotiation providers. + $language_providers = module_invoke_all('language_negotiation_info'); + + // Add the default language provider. + $language_providers[LANGUAGE_NEGOTIATION_DEFAULT] = array( + 'title' => t('Default'), + 'description' => t('The default site language (@language_name) is used.', array('@language_name' => language_default()->native)), + 'callback' => 'language_from_default', + 'weight' => 10, + ); + + // Let other modules alter the list of language providers. + drupal_alter('language_negotiation_info', $language_providers); + } + + return $language_providers; +} + +/** + * Helper function used to cache the language providers results. + * + * @param $provider_id + * The language provider ID. + * @param $provider + * The language provider to be invoked. If not passed it will be explicitly + * loaded through language_negotiation_info(). + * + * @return + * The language provider's return value. + */ +function language_provider_invoke($provider_id, $provider = NULL) { + $results = &drupal_static(__FUNCTION__); + + if (!isset($results[$provider_id])) { + global $user; + + // Get languages grouped by status and select only the enabled ones. + $languages = language_list('enabled'); + $languages = $languages[1]; + + if (!isset($provider)) { + $providers = language_negotiation_info(); + $provider = $providers[$provider_id]; + } + + if (isset($provider['file'])) { + require_once DRUPAL_ROOT .'/'. $provider['file']; } + + // If the language provider has no cache preference or this is satisified + // we can execute the callback. + $cache = !isset($provider['cache']) || $user->uid || $provider['cache'] == variable_get('cache', CACHE_DISABLED); + $callback = $provider['callback']; + $langcode = $cache && function_exists($callback) ? $callback($languages) : FALSE; + $results[$provider_id] = isset($languages[$langcode]) ? $languages[$langcode] : FALSE; } + + return $results[$provider_id]; } /** - * Rewrite URLs with language based prefix. Parameters are the same - * as those of the url() function. + * Return the passed language provider weight or a default value. + * + * @param $provider + * A language provider data structure. + * + * @return + * A numeric weight. */ -function language_url_rewrite(&$path, &$options) { - global $language; +function language_provider_weight($provider) { + $default = is_numeric($provider) ? $provider : 0; + return isset($provider['weight']) && is_numeric($provider['weight']) ? $provider['weight'] : $default; +} - // Only modify relative (insite) URLs. - if (!$options['external']) { +/** + * Choose a language for the given type based on language negotiation settings. + * + * @param $type + * The language type. + * + * @return + * The negotiated language object. + */ +function language_initialize($type) { + // Execute the language providers in the order they were set up and return the + // first valid language found. + $negotiation = variable_get("language_negotiation_$type", array()); + foreach ($negotiation as $id => $provider) { + $language = language_provider_invoke($id, $provider); + if ($language) { + return $language; + } + } + // If no other language was found use the default one. + return language_default(); +} - // Language can be passed as an option, or we go for current language. - if (!isset($options['language'])) { - $options['language'] = $language; - } - - switch (variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE)) { - case LANGUAGE_NEGOTIATION_NONE: - // No language dependent path allowed in this mode. - unset($options['language']); - break; - - case LANGUAGE_NEGOTIATION_DOMAIN: - if ($options['language']->domain) { - // Ask for an absolute URL with our modified base_url. - $options['absolute'] = TRUE; - $options['base_url'] = $options['language']->domain; - } - break; +/** + * Default language provider. + * + * @return + * The default language code. + */ +function language_from_default() { + return language_default()->language; +} - case LANGUAGE_NEGOTIATION_PATH_DEFAULT: - $default = language_default(); - if ($options['language']->language == $default->language) { - break; - } - // Intentionally no break here. +/** + * Rewrite URLs allowing modules to hook in. + * + * @param $path + * The path to rewrite. + * @param $options + * An associative array of additional options as in url(). + */ +function language_url_rewrite(&$path, &$options) { + // Only modify relative (insite) URLs. + if (!$options['external']) { + $data = array('__drupal_alter_by_ref' => array(&$path, &$options)); + drupal_alter('language_url_rewrite', $data); + } +} - case LANGUAGE_NEGOTIATION_PATH: - if (!empty($options['language']->prefix)) { - $options['prefix'] = $options['language']->prefix . '/'; - } - break; +/** + * Split the given path into prefix and actual path. + * + * Parse the given path and return the language object identified by the + * prefix and the actual path. + * + * @param $path + * The path to split. + * @param $languages + * An array of valid languages. + * + * @return + * An array composed of: + * - A language object corresponding to the identified prefix on success, + * FALSE otherwise. + * - The path without the prefix on success, the given path otherwise. + */ +function language_split_url_prefix($path, $languages) { + $args = empty($path) ? array() : explode('/', $path); + $prefix = array_shift($args); + // Search prefix within enabled languages. + foreach ($languages as $language) { + if (!empty($language->prefix) && $language->prefix == $prefix) { + // Rebuild $path with the language removed. + return array($language, implode('/', $args)); } } + return array(FALSE, $path); } Index: includes/locale.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/locale.inc,v retrieving revision 1.226 diff -u -p -r1.226 locale.inc --- includes/locale.inc 22 Aug 2009 14:34:17 -0000 1.226 +++ includes/locale.inc 11 Sep 2009 16:01:00 -0000 @@ -24,6 +24,18 @@ define('LOCALE_IMPORT_OVERWRITE', 0); define('LOCALE_IMPORT_KEEP', 1); /** + * URL language negotiation: use the path prefix as URL language + * indicator. + */ +define('LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX', 0); + +/** + * URL language negotiation: use the domain as URL language + * indicator. + */ +define('LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN', 1); + +/** * @defgroup locale-language-overview Language overview functionality * @{ */ @@ -468,7 +480,7 @@ function locale_languages_delete_form_su */ /** - * @defgroup locale-languages-negotiation Language negotiation options screen + * @defgroup locale-languages-negotiation Language negotiation options * @{ */ @@ -476,33 +488,429 @@ function locale_languages_delete_form_su * Setting for language negotiation options */ function locale_languages_configure_form() { - $form['language_negotiation'] = array( - '#title' => t('Language negotiation'), - '#type' => 'radios', - '#options' => array( - LANGUAGE_NEGOTIATION_NONE => t('None.'), - LANGUAGE_NEGOTIATION_PATH_DEFAULT => t('Path prefix only.'), - LANGUAGE_NEGOTIATION_PATH => t('Path prefix with language fallback.'), - LANGUAGE_NEGOTIATION_DOMAIN => t('Domain name only.')), - '#default_value' => variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE), - '#description' => t("Select the mechanism used to determine your site's presentation language. Modifying this setting may break all incoming URLs and should be used with caution in a production environment.") + include_once DRUPAL_ROOT . '/includes/language.inc'; + + $form = array( + '#submit' => array('locale_languages_configure_form_submit'), + '#theme' => 'locale_languages_configure_form', + '#language_types' => language_types_configurable(), + '#language_providers' => language_negotiation_info(), ); + + foreach ($form['#language_types'] as $type => $info) { + _locale_languages_configure_form_language_table($form, $type, $info); + } + $form['submit'] = array( '#type' => 'submit', - '#value' => t('Save settings') + '#value' => t('Save settings'), ); + return $form; } /** - * Submit function for language negotiation settings. + * Helper function to build a language provider table. + */ +function _locale_languages_configure_form_language_table(&$form, $type, $info) { + + $table_form = array( + '#title' => $info['title'], + '#tree' => TRUE, + '#description' => $info['description'], + '#language_providers' => array(), + '#show_operations' => FALSE, + 'weight' => array('#tree' => TRUE), + 'enabled' => array('#tree' => TRUE), + ); + + $language_providers = $form['#language_providers']; + $enabled_providers = variable_get("locale_language_providers_enabled_$type", array()); + $providers_weight = variable_get("locale_language_providers_weight_$type", array()); + + // Add missing data to the providers lists. + foreach ($language_providers as $id => $provider) { + if (!isset($providers_weight[$id])) { + $providers_weight[$id] = language_provider_weight($provider); + } + if (!isset($enabled_providers[$id])) { + $enabled_providers[$id] = FALSE; + } + } + + // Order providers list by weight. + asort($providers_weight); + + foreach ($providers_weight as $id => $weight) { + $enabled = $enabled_providers[$id]; + $provider = $language_providers[$id]; + + // List the provider only if the current type is defined in its 'types' key. + // If it is not defined default to all the configurabe language types. + $types = isset($provider['types']) ? array_flip($provider['types']) : $form['#language_types']; + + if (isset($types[$type])) { + $table_form['#language_providers'][$id] = $provider; + + $table_form['weight'][$id] = array( + '#type' => 'weight', + '#default_value' => $weight, + '#attributes' => array('class' => array("language-provider-weight-$type")), + ); + + $table_form['title'][$id] = array('#markup' => check_plain($provider['title'])); + + $table_form['enabled'][$id] = array('#type' => 'checkbox', '#default_value' => $enabled); + if ($id === LANGUAGE_NEGOTIATION_DEFAULT) { + $table_form['enabled'][$id]['#default_value'] = TRUE; + $table_form['enabled'][$id]['#attributes'] = array('disabled' => 'disabled'); + } + + $table_form['description'][$id] = array('#markup' => check_markup($provider['description'])); + + $config_op = ''; + if (isset($provider['config'])) { + $config_op = l(t('Configure'), $provider['config']); + // If there is at least one operation enabled show the operation column. + $table_form['#show_operations'] = TRUE; + } + $table_form['operation'][$id] = array('#markup' => $config_op); + } + } + + $form[$type] = $table_form; +} + +/** + * Theme the language configure form. + * + * @ingroup themeable + */ +function theme_locale_languages_configure_form($form) { + $output = ''; + + foreach ($form['#language_types'] as $type => $info) { + $rows = array(); + $title = ''; + $description = '
' . $form[$type]['#description'] . '
'; + + foreach ($form[$type]['title'] as $id => $element) { + // Do not take form control structures. + if (is_array($element) && element_child($id)) { + $row = array( + 'data' => array( + '' . drupal_render($form[$type]['title'][$id]) . '', + drupal_render($form[$type]['description'][$id]), + drupal_render($form[$type]['enabled'][$id]), + drupal_render($form[$type]['weight'][$id]), + ), + 'class' => array('draggable'), + ); + if ($form[$type]['#show_operations']) { + $row['data'][] = drupal_render($form[$type]['operation'][$id]); + } + $rows[] = $row; + } + } + + $header = array( + array('data' => t('Detection method')), + array('data' => t('Description')), + array('data' => t('Enabled')), + array('data' => t('Weight')), + ); + + // If there is at least one operation enabled show the operation column. + if ($form[$type]['#show_operations']) { + $header[] = array('data' => t('Operations')); + } + + $table = theme('table', $header, $rows, array('id' => "language-negotiation-providers-$type")); + $table .= drupal_render_children($form[$type]); + + drupal_add_tabledrag("language-negotiation-providers-$type", 'order', 'sibling', "language-provider-weight-$type"); + + $output .= '
' . $title . $description . $table . '
'; + } + + $output .= drupal_render_children($form); + return $output; +} + +/** + * Submit handler for language negotiation settings. */ function locale_languages_configure_form_submit($form, &$form_state) { - variable_set('language_negotiation', $form_state['values']['language_negotiation']); - drupal_set_message(t('Language negotiation configuration saved.')); + $configurable_types = array_keys($form['#language_types']); + + foreach ($configurable_types as $type) { + $negotiation = array(); + $enabled_providers = $form_state['values'][$type]['enabled']; + $enabled_providers[LANGUAGE_NEGOTIATION_DEFAULT] = TRUE; + $providers_weight = $form_state['values'][$type]['weight']; + + foreach ($providers_weight as $id => $weight) { + if ($enabled_providers[$id]) { + $provider = $form[$type]['#language_providers'][$id]; + $provider['weight'] = $weight; + $negotiation[$id] = $provider; + } + } + + language_negotiation_set($type, $negotiation); + variable_set("locale_language_providers_enabled_$type", $enabled_providers); + variable_set("locale_language_providers_weight_$type", $providers_weight); + } + + // Save non-configurable language types negotiation. + $language_types_info = language_types_info(); + $defined_providers = $form['#language_providers']; + foreach ($language_types_info as $type => $info) { + if (isset($info['fixed'])) { + $negotiation = array(); + foreach ($info['fixed'] as $id) { + if (isset($defined_providers[$id])) { + $negotiation[$id] = $defined_providers[$id]; + } + } + language_negotiation_set($type, $negotiation); + } + } + + // Save language types. + variable_set('language_types', array_keys($language_types_info)); + $form_state['redirect'] = 'admin/config/regional/language'; - return; + drupal_set_message(t('Language negotiation configuration saved.')); +} + +/** + * The URL language provider configuration form. + */ +function locale_language_providers_url_form() { + $form = array(); + + $form['locale_language_negotiation_url_part'] = array( + '#title' => t('URL language indicator'), + '#type' => 'radios', + '#options' => array( + LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX => t('Path prefix'), + LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN => t('Domain'), + ), + '#default_value' => variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX), + '#description' => t('Select which part of the URL will determine the language. Modifying this setting may break all incoming URLs and should be used with caution in a production environment.'), + ); + + $form['#redirect'] = 'admin/config/regional/language/configure'; + + return system_settings_form($form); +} + +/** + * The URL language provider configuration form. + */ +function locale_language_providers_session_form() { + $form = array(); + + $form['locale_language_negotiation_session_param'] = array( + '#title' => t('Request/session parameter'), + '#type' => 'textfield', + '#default_value' => variable_get('locale_language_negotiation_session_param', 'language'), + '#description' => t('This value will be the name of the request/session parameter which will be used to determine the desired language.'), + ); + + $form['#redirect'] = 'admin/config/regional/language/configure'; + + return system_settings_form($form); +} + +/** + * Identify the language from the current content language. + * + * @return + * The current content language code. + */ +function locale_language_from_content() { + global $language; + return isset($language->language) ? $language->language : FALSE; +} + +/** + * Identify language from the Accept-language HTTP header we got. + * + * We perform browser accept-language parsing only if page cache is disabled, + * otherwise we would cache a user-specific preference. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on success, FALSE otherwise. + */ +function locale_language_from_browser($languages) { + // Specified by the user via the browser's Accept Language setting + // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" + $browser_langs = array(); + + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $browser_accept = explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']); + foreach ($browser_accept as $langpart) { + // The language part is either a code or a code with a quality. + // We cannot do anything with a * code, so it is skipped. + // If the quality is missing, it is assumed to be 1 according to the RFC. + if (preg_match("!([a-z-]+)(;q=([0-9\\.]+))?!", trim($langpart), $found)) { + $browser_langs[$found[1]] = (isset($found[3]) ? (float) $found[3] : 1.0); + } + } + } + + // Order the codes by quality + arsort($browser_langs); + + // Try to find the first preferred language we have + foreach ($browser_langs as $langcode => $q) { + if (isset($languages[$langcode])) { + return $langcode; + } + } + + return FALSE; } + +/** + * Identify language from the user preferences. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on succes, FALSE otherwise. + */ +function locale_language_from_user($languages) { + // User preference (only for logged users). + global $user; + if ($user->uid) { + return $user->language; + } + // No language preference from the user. + return FALSE; +} + +/** + * Identify language from a request/session parameter. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on succes, FALSE otherwise. + */ +function locale_language_from_session($languages) { + $param = variable_get('locale_language_negotiation_session_param', 'language'); + // Request parameter. + if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) { + return $_SESSION[$param] = $langcode; + } + // Session parameter. + if (isset($_SESSION[$param])) { + return $_SESSION[$param]; + } + return FALSE; +} + +/** + * Identify language via URL prefix or domain. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on succes, FALSE otherwise. + */ +function locale_language_from_url($languages) { + $language_url = FALSE; + + switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) { + case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX: + // $_GET['q'] might not be available at this time, because + // path initialization runs after the language bootstrap phase. + list($language, $_GET['q']) = language_split_url_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages); + if ($language !== FALSE) { + $language_url = $language->language; + } + break; + + case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN: + foreach ($languages as $language) { + $host = parse_url($language->domain, PHP_URL_HOST); + if ($host && ($_SERVER['HTTP_HOST'] == $host)) { + $language_url = $language->language; + break; + } + } + break; + } + + return $language_url; +} + +/** + * Implement hook_language_url_rewrite_alter(). + */ +function locale_language_url_rewrite_alter(&$data, &$path, &$options) { + static $query, $query_param, $query_value, $url_language, $url_rewrite; + + // The following values are not supposed to change during a single page + // request processing. + if (!isset($query)) { + global $user; + $languages = language_list('enabled'); + $languages = $languages[1]; + $query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language')); + $query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL; + $query = !$user->uid && isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION); + $url_rewrite = language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL); + } + + // If the user is anonymous, the user language provider is enabled, and the + // corresponding option has been set, we must preserve any explicit user + // language preference even with cookies disabled. + if ($query) { + if (is_string($options['query'])) { + $options['query'] = drupal_query_string_decode($options['query']); + } + if (!isset($options['query'][$query_param])) { + $options['query'][$query_param] = $query_value; + } + } + + if ($url_rewrite) { + // Language can be passed as an option, or we go for current URL language. + if (!isset($options['language'])) { + global $language_url; + $options['language'] = $language_url; + } + + if (isset($options['language'])) { + switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) { + case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN: + if ($options['language']->domain) { + // Ask for an absolute URL with our modified base_url. + $options['absolute'] = TRUE; + $options['base_url'] = $options['language']->domain; + } + break; + + case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX: + if (!empty($options['language']->prefix)) { + $options['prefix'] = $options['language']->prefix . '/'; + } + break; + } + } + } +} + /** * @} End of "locale-languages-negotiation" */ Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.522 diff -u -p -r1.522 theme.inc --- includes/theme.inc 11 Sep 2009 14:14:16 -0000 1.522 +++ includes/theme.inc 11 Sep 2009 14:47:58 -0000 @@ -1404,7 +1404,7 @@ function theme_status_messages($display * A string containing an unordered list of links. */ function theme_links($links, $attributes = array('class' => array('links')), $heading = array()) { - global $language; + global $language_url; $output = ''; if (count($links) > 0) { @@ -1445,7 +1445,7 @@ function theme_links($links, $attributes $class[] = 'last'; } if (isset($link['href']) && ($link['href'] == $_GET['q'] || ($link['href'] == '' && drupal_is_front_page())) - && (empty($link['language']) || $link['language']->language == $language->language)) { + && (empty($link['language']) || $link['language']->language == $language_url->language)) { $class[] = 'active'; } $output .= ' $class)) . '>'; Index: modules/locale/locale.css =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.css,v retrieving revision 1.4 diff -u -p -r1.4 locale.css --- modules/locale/locale.css 24 Aug 2009 03:11:34 -0000 1.4 +++ modules/locale/locale.css 11 Sep 2009 14:47:58 -0000 @@ -19,3 +19,11 @@ #locale-translation-filter-form .form-item select.form-select { width: 100%; } + +.session-links .active a.active { + color: #0062A0; +} + +.session-links .active a.session-active { + color: #000000; +} Index: modules/locale/locale.install =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.install,v retrieving revision 1.48 diff -u -p -r1.48 locale.install --- modules/locale/locale.install 10 Sep 2009 06:38:18 -0000 1.48 +++ modules/locale/locale.install 11 Sep 2009 16:05:46 -0000 @@ -43,6 +43,47 @@ function locale_update_7000() { } /** + * Upgrade language negotiation settings. + */ +function locale_update_7001() { + require_once DRUPAL_ROOT . '/includes/language.inc'; + + switch (variable_get('language_negotiation', 0)) { + // LANGUAGE_NEGOTIATION_NONE. + case 0: + $negotiation = array(); + break; + + // LANGUAGE_NEGOTIATION_PATH_DEFAULT. + case 1: + $negotiation = array(LOCALE_LANGUAGE_NEGOTIATION_URL); + break; + + // LANGUAGE_NEGOTIATION_PATH. + case 2: + $negotiation = array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_USER, LOCALE_LANGUAGE_NEGOTIATION_BROWSER); + break; + + // LANGUAGE_NEGOTIATION_DOMAIN. + case 3: + variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN); + $negotiation = array(LOCALE_LANGUAGE_NEGOTIATION_URL); + break; + } + + // Save new language negotiation options: UI language is tied to content + // language as this was Drupal 6 behavior. + language_negotiation_set(LANGUAGE_TYPE_CONTENT, array_flip($negotiation)); + language_negotiation_set(LANGUAGE_TYPE_INTERFACE, array(LOCALE_LANGUAGE_NEGOTIATION_CONTENT => 0)); + language_negotiation_set(LANGUAGE_TYPE_URL, array(LOCALE_LANGUAGE_NEGOTIATION_URL => 0)); + + // Unset the old language negotiation system variable. + variable_del('language_negotiation'); + + return array(); +} + +/** * @} End of "defgroup updates-6.x-to-7.x" */ @@ -64,15 +105,22 @@ function locale_uninstall() { // Clear variables. variable_del('language_default'); variable_del('language_count'); - variable_del('language_negotiation'); - variable_del('javascript_parsed'); + variable_del('locale_language_negotiation_url_part'); + variable_del('locale_language_negotiation_session_param'); variable_del('language_content_type_default'); variable_del('language_content_type_negotiation'); variable_del('locale_cache_strings'); variable_del('locale_js_directory'); + variable_del('javascript_parsed'); + + foreach (drupal_language_types() as $type) { + variable_del("language_negotiation_$type"); + variable_del("locale_language_providers_enabled_$type"); + variable_del("locale_language_providers_weight_$type"); + } foreach (node_type_get_types() as $type => $content_type) { - $setting = variable_del('language_content_type_' . $type); + $setting = variable_del("language_content_type_$type"); } // Switch back to English: with a $language->language value different from 'en' Index: modules/locale/locale.module =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v retrieving revision 1.258 diff -u -p -r1.258 locale.module --- modules/locale/locale.module 31 Aug 2009 17:06:09 -0000 1.258 +++ modules/locale/locale.module 11 Sep 2009 14:47:58 -0000 @@ -12,6 +12,32 @@ * Gettext portable object files are supported. */ +/** + * The language is determined using a URL language indicator: + * path prefix or domain according to the configuration. + */ +define('LOCALE_LANGUAGE_NEGOTIATION_URL', 'locale-url'); + +/** + * The language is set basing on the browser language settings. + */ +define('LOCALE_LANGUAGE_NEGOTIATION_BROWSER', 'locale-browser'); + +/** + * The language is determined using the current content language. + */ +define('LOCALE_LANGUAGE_NEGOTIATION_CONTENT', 'locale-content'); + +/** + * The language is set basing on the user language settings. + */ +define('LOCALE_LANGUAGE_NEGOTIATION_USER', 'locale-user'); + +/** + * The language is set basing the request/session parameters. + */ +define('LOCALE_LANGUAGE_NEGOTIATION_SESSION', 'locale-session'); + // --------------------------------------------------------------------------------- // Hook implementations @@ -38,12 +64,8 @@ function locale_help($path, $arg) { case 'admin/config/regional/language/add': return '

' . t('Add all languages to be supported by your site. If your desired language is not available in the Language name drop-down, click Custom language and provide a language code and other details manually. When providing a language code manually, be sure to enter a standardized language code, since this code may be used by browsers to determine an appropriate display language.') . '

'; case 'admin/config/regional/language/configure': - $output = '

' . t("Language negotiation settings determine the site's presentation language. Available options include:") . '

'; - $output .= '
  • ' . t('None. The default language is used for site presentation, though users may (optionally) select a preferred language on the My Account page. (User language preferences will be used for site e-mails, if available.)') . '
  • '; - $output .= '
  • ' . t('Path prefix only. The presentation language is determined by examining the path for a language code or other custom string that matches the path prefix (if any) specified for each language. If a suitable prefix is not identified, the default language is used. Example: "example.com/de/contact" sets presentation language to German based on the use of "de" within the path.') . '
  • '; - $output .= '
  • ' . t("Path prefix with language fallback. The presentation language is determined by examining the path for a language code or other custom string that matches the path prefix (if any) specified for each language. If a suitable prefix is not identified, the display language is determined by the user's language preferences from the My Account page, or by the browser's language settings. If a presentation language cannot be determined, the default language is used.") . '
  • '; - $output .= '
  • ' . t('Domain name only. The presentation language is determined by examining the domain used to access the site, and comparing it to the language domain (if any) specified for each language. If a match is not identified, the default language is used. Example: "http://de.example.com/contact" sets presentation language to German based on the use of "http://de.example.com" in the domain.') . '
'; - $output .= '

' . t('The path prefix or domain name for a language may be set by editing the available languages. In the absence of an appropriate match, the site is displayed in the default language.', array('@languages' => url('admin/config/regional/language'))) . '

'; + // @todo: Help text needed. + $output = ''; return $output; case 'admin/config/regional/translate': $output = '

' . 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.') . '

'; @@ -103,6 +125,22 @@ function locale_menu() { 'file path' => 'includes', 'type' => MENU_LOCAL_TASK, ); + $items['admin/config/regional/language/configure/url'] = array( + 'title' => 'URL language provider configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_language_providers_url_form'), + 'access arguments' => array('administer languages'), + 'file' => 'locale.inc', + 'file path' => 'includes', + ); + $items['admin/config/regional/language/configure/session'] = array( + 'title' => 'Session language provider configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_language_providers_session_form'), + 'access arguments' => array('administer languages'), + 'file' => 'locale.inc', + 'file path' => 'includes', + ); $items['admin/config/regional/language/edit/%'] = array( 'title' => 'Edit language', 'page callback' => 'drupal_get_form', @@ -265,13 +303,13 @@ function locale_language_selector_form($ ); // Get language negotiation settings. - $mode = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE); + $mode = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT; $form['locale']['language'] = array( '#type' => (count($names) <= 5 ? 'radios' : 'select'), '#title' => t('Language'), '#default_value' => $user_preferred_language->language, '#options' => $names, - '#description' => ($mode == LANGUAGE_NEGOTIATION_PATH) ? t("This account's default language for e-mails, and preferred language for site presentation.") : t("This account's default language for e-mails."), + '#description' => $mode ? t("This account's default language for e-mails, and preferred language for site presentation.") : t("This account's default language for e-mails."), ); return $form; } @@ -337,12 +375,89 @@ function locale_theme() { 'locale_languages_overview_form' => array( 'arguments' => array('form' => array()), ), + 'locale_languages_configure_form' => array( + 'arguments' => array('form' => array()), + ), 'locale_translation_filters' => array( 'arguments' => array('form' => array()), ), ); } +/** + * Implement hook_language_types_info(). + */ +function locale_language_types_info() { + return array( + LANGUAGE_TYPE_CONTENT => array( + 'title' => t('Content language negotiation'), + 'description' => t(''), + ), + LANGUAGE_TYPE_INTERFACE => array( + 'title' => t('Interface language negotiation'), + 'description' => t(''), + ), + LANGUAGE_TYPE_URL => array( + 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_URL), + ), + ); +} + +/** + * Implement hook_language_negotiation_info(). + */ +function locale_language_negotiation_info() { + $file = 'includes/locale.inc'; + $providers = array(); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_URL] = array( + 'types' => array(LANGUAGE_TYPE_CONTENT, LANGUAGE_TYPE_INTERFACE, LANGUAGE_TYPE_URL), + 'callback' => 'locale_language_from_url', + 'file' => $file, + 'weight' => -5, + 'title' => t('URL'), + 'description' => t('The language is determined from the URL (Path prefix or domain).'), + 'config' => 'admin/config/regional/language/configure/url', + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_CONTENT] = array( + 'types' => array(LANGUAGE_TYPE_INTERFACE), + 'callback' => 'locale_language_from_content', + 'file' => $file, + 'weight' => 8, + 'title' => t('Content'), + 'description' => t('The interface language is the same as the negotiated content language.'), + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_USER] = array( + 'callback' => 'locale_language_from_user', + 'file' => $file, + 'weight' => -3, + 'title' => t('User'), + 'description' => t('The language is determined from the language preference set in the user account.'), + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_BROWSER] = array( + 'callback' => 'locale_language_from_browser', + 'file' => $file, + 'weight' => -2, + 'cache' => CACHE_DISABLED, + 'title' => t('Browser'), + 'description' => t('The language is determined from the browser\'s language settings.'), + ); + + $providers[LOCALE_LANGUAGE_NEGOTIATION_SESSION] = array( + 'callback' => 'locale_language_from_session', + 'file' => $file, + 'weight' => -4, + 'title' => t('Session language'), + 'description' => t('The language is determined from a request/session parameter.'), + 'config' => 'admin/config/regional/language/configure/session', + ); + + return $providers; +} + // --------------------------------------------------------------------------------- // Locale core functionality @@ -632,9 +747,11 @@ function locale_css_alter(&$css) { * Implement hook_block_info(). */ function locale_block_info() { - $block['language-switcher']['info'] = t('Language switcher'); + $block['language-switcher']['info'] = t('Language switcher (URL)'); + $block['language-switcher-query']['info'] = t('Language switcher (Session)'); // Not worth caching. $block['language-switcher']['cache'] = DRUPAL_NO_CACHE; + $block['language-switcher-query']['cache'] = DRUPAL_NO_CACHE; return $block; } @@ -646,10 +763,23 @@ function locale_block_info() { * web addresses, so we can actually link to other language versions. */ function locale_block_view($delta = '') { - if (variable_get('language_count', 1) > 1 && variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE) != LANGUAGE_NEGOTIATION_NONE) { + switch ($delta) { + case 'language-switcher': + return _locale_language_switcher_url(); + case 'language-switcher-query': + return _locale_language_switcher_session(); + } +} + +/** + * Return the URL language switcher block. + */ +function _locale_language_switcher_url() { + if (variable_get('language_count', 1) > 1 && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) { $path = drupal_is_front_page() ? '' : $_GET['q']; $languages = language_list('enabled'); $links = array(); + foreach ($languages[1] as $language) { $links[$language->language] = array( 'href' => $path, @@ -669,6 +799,45 @@ function locale_block_view($delta = '') } /** + * Return the session language switcher block. + */ +function _locale_language_switcher_session() { + if (variable_get('language_count', 1) > 1 && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION)) { + drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css'); + + $param = variable_get('locale_language_negotiation_session_param', 'language'); + $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : NULL; + + $path = drupal_is_front_page() ? '' : $_GET['q']; + $languages = language_list('enabled'); + $links = array(); + + $query = $_GET; + unset($query['q']); + + foreach ($languages[1] as $language) { + $langcode = $language->language; + $links[$langcode] = array( + 'href' => $path, + 'title' => $language->native, + 'attributes' => array('class' => array('language-link')), + 'query' => $query, + ); + if (!isset($language_query) || $language_query != $langcode) { + $links[$langcode]['query'][$param] = $langcode; + } + else { + $links[$langcode]['attributes']['class'][] = ' session-active'; + } + } + + $block['subject'] = t('Languages'); + $block['content'] = theme('links', $links, array('class' => array('session-links'))); + return $block; + } +} + +/** * Theme locale translation filter selector. * * @ingroup themeable Index: modules/locale/locale.test =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.test,v retrieving revision 1.40 diff -u -p -r1.40 locale.test --- modules/locale/locale.test 28 Aug 2009 14:40:12 -0000 1.40 +++ modules/locale/locale.test 11 Sep 2009 15:49:51 -0000 @@ -910,11 +910,11 @@ class LocaleUninstallFunctionalTest exte /** * The default language set for the UI before uninstall. */ - protected $ui_language; + protected $language_interface; function setUp() { parent::setUp('locale'); - $this->ui_language = 'en'; + $this->language_interface = 'en'; } /** @@ -925,15 +925,12 @@ class LocaleUninstallFunctionalTest exte // Add a new language and optionally set it as default. require_once DRUPAL_ROOT . '/includes/locale.inc'; - locale_add_language('fr', 'French', 'Français', LANGUAGE_LTR, '', '', TRUE, $this->ui_language == 'fr'); + locale_add_language('fr', 'French', 'Français', LANGUAGE_LTR, '', '', TRUE, $this->language_interface == 'fr'); // Check the UI language. drupal_language_initialize(); - global $language; - $this->assertEqual($language->language, $this->ui_language, t('Current language: %lang', array('%lang' => $language->language))); - - // Change language negotiation options. - variable_set('language_negotiation', LANGUAGE_NEGOTIATION_PATH_DEFAULT); + global $language_interface; + $this->assertEqual($language_interface->language, $this->language_interface, t('Current language: %lang', array('%lang' => $language_interface->language))); // Enable multilingual workflow option for articles. variable_set('language_content_type_article', 1); @@ -959,6 +956,16 @@ class LocaleUninstallFunctionalTest exte // Disable string caching. variable_set('locale_cache_strings', 0); + // Change language negotiation options. + drupal_load('module', 'locale'); + variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info()); + variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info()); + variable_set('language_negotiation_' . LANGUAGE_TYPE_URL, locale_language_negotiation_info()); + + // Change language providers settings. + variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX); + variable_set('locale_language_negotiation_session_param', TRUE); + // Uninstall Locale. module_disable($locale_module); drupal_uninstall_modules($locale_module); @@ -968,7 +975,7 @@ class LocaleUninstallFunctionalTest exte // Check the init language logic. drupal_language_initialize(); - $this->assertEqual($language->language, 'en', t('Language after uninstall: %lang', array('%lang' => $language->language))); + $this->assertEqual($language_interface->language, 'en', t('Language after uninstall: %lang', array('%lang' => $language_interface->language))); // Check JavaScript files deletion. $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found')))); @@ -978,8 +985,16 @@ class LocaleUninstallFunctionalTest exte $this->assertEqual($language_count, 1, t('Language count: %count', array('%count' => $language_count))); // Check language negotiation. - $language_negotiation = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE) == LANGUAGE_NEGOTIATION_NONE; - $this->assertTrue($language_negotiation, t('Language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) == LANGUAGE_NEGOTIATION_DEFAULT; + $this->assertTrue($language_negotiation, t('Interface language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_CONTENT) == LANGUAGE_NEGOTIATION_DEFAULT; + $this->assertTrue($language_negotiation, t('Content language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_URL) == LANGUAGE_NEGOTIATION_DEFAULT; + $this->assertTrue($language_negotiation, t('URL language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + + // Check language providers settings. + $this->assertFalse(variable_get('locale_language_negotiation_url_part', FALSE), t('URL language provider indicator settings cleared.')); + $this->assertFalse(variable_get('locale_language_negotiation_session_param', FALSE), t('Visit language provider settings cleared.')); // Check JavaScript parsed. $javascript_parsed_count = count(variable_get('javascript_parsed', array())); @@ -1011,14 +1026,14 @@ class LocaleUninstallFrenchFunctionalTes public static function getInfo() { return array( 'name' => 'Locale uninstall (FR)', - 'description' => 'Tests the uninstall process using French as UI language.', + 'description' => 'Tests the uninstall process using French as interface language.', 'group' => 'Locale', ); } function setUp() { parent::setUp(); - $this->ui_language = 'fr'; + $this->language_interface = 'fr'; } } @@ -1061,11 +1076,8 @@ class LanguageSwitchingFunctionalTest ex $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); // Set language negotiation. - $edit = array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH_DEFAULT, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + drupal_load('module', 'locale'); + variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info()); // Assert that the language switching block is displayed on the frontpage. $this->drupalGet(''); @@ -1243,10 +1255,8 @@ class LocalePathFunctionalTest extends D $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); // Set language negotiation. - $edit = array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH_DEFAULT, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + drupal_load('module', 'locale'); + variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info()); // Create a node. $node = $this->drupalCreateNode(array('type' => 'page')); @@ -1352,12 +1362,6 @@ class LocaleContentFunctionalTest extend ); $this->drupalPost($path, $edit, t('Save configuration')); - // Set language negotiation. - $edit = array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH_DEFAULT, - ); - $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - // Set page content type to use multilingual support. $this->drupalGet('admin/structure/node-type/page'); $this->assertText(t('Multilingual support'), t('Multilingual support fieldset present on content type configuration form.')); @@ -1412,7 +1416,7 @@ class LocaleContentFunctionalTest extend /** * Test UI language negotiation - * 1. LANGUAGE_NEGOTIATION_PATH_DEFAULT + * 1. URL (PATH) > DEFAULT * UI Language base on URL prefix, browser language preference has no * influence: * admin/config @@ -1421,7 +1425,7 @@ class LocaleContentFunctionalTest extend * UI in Chinese * blah-blah/admin/config * 404 - * 2. LANGUAGE_NEGOTIATION_PATH + * 2. URL (PATH) > BROWSER > DEFAULT * admin/config * UI in user's browser language preference if the site has that * language enabled, if not, the default language @@ -1429,7 +1433,7 @@ class LocaleContentFunctionalTest extend * UI in Chinese * blah-blah/admin/config * 404 - * 3. LANGUAGE_NEGOTIATION_DOMAIN + * 3. URL (DOMAIN) > DEFAULT * http://example.com/admin/config * UI language in site default * http://example.cn/admin/config @@ -1446,6 +1450,8 @@ class UILanguageNegotiationTest extends function setUp() { parent::setUp('locale', 'locale_test'); + require_once DRUPAL_ROOT . '/includes/language.inc'; + drupal_load('module', 'locale'); } /** @@ -1505,46 +1511,49 @@ class UILanguageNegotiationTest extends ); $this->drupalPost(NULL, $edit, t('Save translations')); + // Configure URL language rewrite. + variable_set('locale_language_negotiation_url_type', LANGUAGE_TYPE_INTERFACE); + $tests = array( // Default, browser preference should have no influence. array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH_DEFAULT, + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), 'path' => 'admin/config', 'expect' => $default_string, 'http_header' => $http_header_browser_fallback, - 'message' => 'LANGUAGE_NEGOTIATION_PATH_DEFAULT: no language prefix, UI language is default and not the browser language preference setting is used.', + 'message' => 'URL (PATH) > DEFAULT: no language prefix, UI language is default and not the browser language preference setting is used.', ), // Language prefix. array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH_DEFAULT, + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), 'path' => "$language/admin/config", 'expect' => $language_string, 'http_header' => $http_header_browser_fallback, - 'message' => 'LANGUAGE_NEGOTIATION_PATH_DEFAULT: with language prefix, UI language is switched based on path prefix', + 'message' => 'URL (PATH) > DEFAULT: with language prefix, UI language is switched based on path prefix', ), // Default, go by browser preference. array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH, + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), 'path' => 'admin/config', 'expect' => $language_browser_fallback_string, 'http_header' => $http_header_browser_fallback, - 'message' => 'LANGUAGE_NEGOTIATION_PATH: no language prefix, UI language is determined by browser language preference', + 'message' => 'URL (PATH) > BROWSER: no language prefix, UI language is determined by browser language preference', ), // Prefix, switch to the language. array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH, + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), 'path' => "$language/admin/config", 'expect' => $language_string, 'http_header' => $http_header_browser_fallback, - 'message' => 'LANGUAGE_NEGOTIATION_PATH: with langage prefix, UI language is based on path prefix', + 'message' => 'URL (PATH) > BROWSER: with langage prefix, UI language is based on path prefix', ), // Default, browser language preference is not one of site's lang. array( - 'language_negotiation' => LANGUAGE_NEGOTIATION_PATH, + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER, LANGUAGE_NEGOTIATION_DEFAULT), 'path' => 'admin/config', 'expect' => $default_string, 'http_header' => $http_header_blah, - 'message' => 'LANGUAGE_NEGOTIATION_PATH: no language prefix and browser language preference set to unknown language should use default language', + 'message' => 'URL (PATH) > BROWSER > DEFAULT: no language prefix and browser language preference set to unknown language should use default language', ), ); @@ -1553,35 +1562,36 @@ class UILanguageNegotiationTest extends } // Unknown language prefix should return 404. - foreach(array(LANGUAGE_NEGOTIATION_PATH_DEFAULT, LANGUAGE_NEGOTIATION_PATH) as $negotiation) { - variable_set('language_negotiation', $negotiation); - $this->drupalGet("$language_unknown/admin/config", array(), $http_header_browser_fallback); - $this->assertResponse(404, "Unknown language path prefix should return 404, code = $negotiation"); - } + variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info()); + $this->drupalGet("$language_unknown/admin/config", array(), $http_header_browser_fallback); + $this->assertResponse(404, "Unknown language path prefix should return 404"); // Setup for domain negotiation, first configure the language to have domain // URL. $edit = array('prefix' => '', 'domain' => "http://$language_domain"); $this->drupalPost("admin/config/regional/language/edit/$language", $edit, t('Save language')); // Set the site to use domain language negotiation. - variable_set('language_negotiation', LANGUAGE_NEGOTIATION_DOMAIN); $tests = array( // Default domain, browser preference should have no influence. array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), + 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, 'path' => 'admin/config', 'expect' => $default_string, 'http_header' => $http_header_browser_fallback, - 'message' => 'LANGUAGE_NEGOTIATION_DOMAIN: default domain should get default language', + 'message' => 'URL (DOMAIN) > DEFAULT: default domain should get default language', ), // Language domain specific URL, we set the $_SERVER['HTTP_HOST'] in // locale_test.module hook_boot() to simulate this. array( + 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), + 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, 'locale_test_domain' => $language_domain, 'path' => 'admin/config', 'expect' => $language_string, 'http_header' => $http_header_browser_fallback, - 'message' => 'LANGUAGE_NEGOTIATION_DOMAIN: domain example.cn should switch to Chinese', + 'message' => 'URL (DOMAIN) > DEFAULT: domain example.cn should switch to Chinese', ), ); @@ -1592,7 +1602,11 @@ class UILanguageNegotiationTest extends private function runTest($test) { if (!empty($test['language_negotiation'])) { - variable_set('language_negotiation', $test['language_negotiation']); + $negotiation = array_flip($test['language_negotiation']); + language_negotiation_set(LANGUAGE_TYPE_INTERFACE, $negotiation); + } + if (!empty($test['locale_language_negotiation_url_part'])) { + variable_set('locale_language_negotiation_url_part', $test['locale_language_negotiation_url_part']); } if (!empty($test['locale_test_domain'])) { variable_set('locale_test_domain', $test['locale_test_domain']); Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1122 diff -u -p -r1.1122 node.module --- modules/node/node.module 11 Sep 2009 04:06:39 -0000 1.1122 +++ modules/node/node.module 11 Sep 2009 14:47:58 -0000 @@ -1106,6 +1106,52 @@ function node_build_content($node, $buil } /** + * Implement hook_language_negotiation_info(). + */ +function node_language_negotiation_info() { + $providers = array(); + + $providers['node-language'] = array( + 'types' => array(LANGUAGE_TYPE_CONTENT), + 'title' => t('Node language'), + 'description' => t('The current node language is used.'), + 'callback' => 'node_language_provider', + 'file' => drupal_get_path('module', 'node') . '/node.module', + ); + + return $providers; +} + +/** + * Return the language of the current node. + * + * @param $languages + * An array of valid language objects. + * + * @return + * A valid language code on succes, FALSE otherwise. + */ +function node_language_provider($languages) { + require_once DRUPAL_ROOT . '/includes/path.inc'; + + $path = isset($_GET['q']) ? $_GET['q'] : ''; + list($language, $path) = language_split_url_prefix($path, $languages); + $language = $language ? $language : language_default(); + $path = drupal_get_normal_path($path, $language->language); + + // We cannot use args now. + $path = explode('/', $path); + // Act only if we are in a node page. + if ($path[0] == 'node' && $nid = intval($path[1])) { + // We cannot perform a node load here. + $result = db_query('SELECT n.language FROM {node} n WHERE n.nid = :nid', array(':nid' => $nid))->fetchAssoc(); + return $result['language']; + } + + return FALSE; +} + +/** * Generate an array which displays a node detail page. * * @param $node Index: modules/path/path.test =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.test,v retrieving revision 1.20 diff -u -p -r1.20 path.test --- modules/path/path.test 28 Aug 2009 14:40:12 -0000 1.20 +++ modules/path/path.test 11 Sep 2009 14:47:58 -0000 @@ -183,7 +183,9 @@ class PathLanguageTestCase extends Drupa drupal_static_reset('language_list'); // Set language negotiation to "Path prefix with fallback". - variable_set('language_negotiation', LANGUAGE_NEGOTIATION_PATH); + include_once DRUPAL_ROOT . '/includes/locale.inc'; + variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info()); + variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX); // Force inclusion of language.inc. drupal_language_initialize();