Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.302
diff -u -p -r1.302 bootstrap.inc
--- includes/bootstrap.inc	24 Aug 2009 00:14:18 -0000	1.302
+++ includes/bootstrap.inc	31 Aug 2009 10:24:25 -0000
@@ -161,26 +161,57 @@ define('DRUPAL_KILOBYTE', 1024);
 /**
  * No language negotiation. The default language is used.
  */
-define('LANGUAGE_NEGOTIATION_NONE', 0);
+define('LANGUAGE_NEGOTIATION_DEFAULT', 0);
 
 /**
- * Path based negotiation with fallback to default language
- * if no defined path prefix identified.
+ * The language is determined using a URL language indicator:
+ * path prefix or domain according to the configuration.
  */
-define('LANGUAGE_NEGOTIATION_PATH_DEFAULT', 1);
+define('LANGUAGE_NEGOTIATION_URL', 1);
 
 /**
- * Path based negotiation with fallback to user preferences
- * and browser language detection if no defined path prefix
- * identified.
+ * The language is set basing on the user preferences. Request
+ * parameters are checked, then session parameters, and eventually
+ * the user language settings.
  */
-define('LANGUAGE_NEGOTIATION_PATH', 2);
+define('LANGUAGE_NEGOTIATION_USER', 2);
 
 /**
- * Domain based negotiation with fallback to default language
- * if no language identified by domain.
+ * The language is set basing on the browser language settings.
  */
-define('LANGUAGE_NEGOTIATION_DOMAIN', 3);
+define('LANGUAGE_NEGOTIATION_BROWSER', 3);
+
+/**
+ * The language is determined using the current content language.
+ */
+define('LANGUAGE_NEGOTIATION_CONTENT', 4);
+
+/**
+ * Content language negotiation: use the path prefix as URL language
+ * indicator.
+ */
+define('LANGUAGE_NEGOTIATION_URL_PREFIX', 0);
+
+/**
+ * Content language negotiation: use the domain as URL language
+ * indicator.
+ */
+define('LANGUAGE_NEGOTIATION_URL_DOMAIN', 1);
+
+/**
+ * The type of language used to select the user interface.
+ */
+define('LANGUAGE_TYPE_INTERFACE', 0x1);
+
+/**
+ * The type of language used to define the content language.
+ */
+define('LANGUAGE_TYPE_CONTENT', 0x2);
+
+/**
+ * Any type of language.
+ */
+define('LANGUAGE_TYPE_ANY', LANGUAGE_TYPE_INTERFACE | LANGUAGE_TYPE_CONTENT);
 
 /**
  * Language written left to right. Possible value of $language->direction.
@@ -1641,16 +1672,17 @@ function get_t() {
  *  Choose a language for the current page, based on site and user preferences.
  */
 function drupal_language_initialize() {
-  global $language, $user;
+  global $language, $language_interface, $user;
 
   // 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();
+    $language = $language_interface = language_default();
   }
   else {
     include_once DRUPAL_ROOT . '/includes/language.inc';
-    $language = language_initialize();
+    $language = language_initialize(LANGUAGE_TYPE_CONTENT);
+    $language_interface = language_initialize(LANGUAGE_TYPE_INTERFACE);
   }
 }
 
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.978
diff -u -p -r1.978 common.inc
--- includes/common.inc	31 Aug 2009 05:51:07 -0000	1.978
+++ includes/common.inc	31 Aug 2009 12:27:53 -0000
@@ -295,6 +295,25 @@ function drupal_query_string_encode($que
 }
 
 /**
+ * Split an urlencoded 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.
@@ -1193,12 +1212,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'] = '';
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	31 Aug 2009 12:28:08 -0000
@@ -7,69 +7,159 @@
  */
 
 /**
- *  Choose a language for the page, based on language negotiation settings.
+ * 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_initialize() {
-  global $user;
+function language_get_negotiation($type, $provider_id = NULL) {
+  $negotiation = variable_get("language_negotiation_$type");
+  if (empty($negotiation)) {
+    return LANGUAGE_NEGOTIATION_DEFAULT;
+  }
+  foreach ($negotiation as $id => $provider) {
+    if (empty($provider_id) || $id == $provider_id) {
+      return $id;
+    }
+  }
+  return FALSE;
+}
 
-  // 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();
+/**
+ * Save a list of list of language providers.
+ *
+ * @param $type
+ *   The language type.
+ * @param $provider_list
+ *   An array of language provider ids.
+ */
+function language_set_negotiation($type, $provider_list) {
+  $negotiation = array();
+  $enabled_providers = array();
+  $providers_weight = array();
+  $weight = 0;
+
+  foreach (language_negotiation_info() as $id => $provider) {
+    $enabled_providers[$id] = FALSE;
+    $providers_weight[$id] = $weight++;
+    // Check if the provider is defined and has the right type.
+    if (in_array($id, $provider_list) && (!isset($provider['type']) || $provider['type'] & $type)) {
+      $negotiation[] = $provider;
+      $enabled_providers[$id] = TRUE;
+    }
+  }
 
-    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();
+  variable_set("language_negotiation_$type", $negotiation);
+  variable_set("language_providers_enabled_$type", $enabled_providers);
+  variable_set("language_providers_weight_$type", $providers_weight);
+}
 
-    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;
-  }
+/**
+ * Return all the defined language providers.
+ *
+ * @return
+ *   An array of language providers.
+ */
+function language_negotiation_info() {
+  $language_providers = &drupal_static(__FUNCTION__);
 
-  // User language.
-  if ($user->uid && isset($languages[$user->language])) {
-    return $languages[$user->language];
+  if (!isset($language_providers)) {
+    $language_providers = array();
+    foreach (module_implements('language_negotiation_info') as $module) {
+      // Preserve existing keys. We have the following drupal_alter() call to
+      // allow changes to the language providers list.
+      $language_providers += module_invoke($module, 'language_negotiation_info');
+    }
+    // Let other modules alter the list of language providers.
+    drupal_alter('language_negotiation_info', $language_providers);
   }
 
-  // Browser accept-language parsing.
-  if ($language = language_from_browser()) {
-    return $language;
+  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($result[$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;
   }
 
-  // Fall back on the default if everything else fails.
+  return $results[$provider_id];
+}
+
+/**
+ * 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();
 }
 
 /**
  * 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 an user-specific preference.
+ *
+ * @param $languages
+ *   An array of valid language objects.
+ * @return
+ *   A valid language code on success, FALSE otherwise.
  */
-function language_from_browser() {
+function 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();
@@ -90,20 +180,123 @@ function language_from_browser() {
   arsort($browser_langs);
 
   // 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];
+    if (isset($languages[$langcode])) {
+      return $langcode;
     }
   }
+
+  return FALSE;
 }
 
 /**
- * Rewrite URLs with language based prefix. Parameters are the same
- * as those of the url() function.
+ * Return the current content language code.
  */
-function language_url_rewrite(&$path, &$options) {
+function language_from_content() {
   global $language;
+  return $language->language;
+}
+
+/**
+ * Identify language from the user preferences.
+ *
+ * @param $languages
+ *   An array of valid language objects.
+ * @return
+ *   A valid language code on succes, FALSE otherwise.
+ */
+function language_from_user($languages) {
+  // Request parameter.
+  if (isset($_GET['language_ui']) && isset($languages[$langcode = $_GET['language_ui']])) {
+    return $_SESSION['language_ui'] = $langcode;
+  }
+  // Session parameter.
+  if (isset($_SESSION['language_ui'])) {
+    return $_SESSION['language_ui'];
+  }
+  // User preference (only for logged users).
+  global $user;
+  if ($user->uid) {
+    return $user->language;
+  }
+  // No language preference from the user.
+  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 language_from_url($languages) {
+  $url_language = FALSE;
+
+  switch (variable_get('language_negotiation_url', LANGUAGE_NEGOTIATION_URL_PREFIX)) {
+    case 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_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages);
+      if ($language !== FALSE) {
+        $url_language = $language->language;
+      }
+      break;
+
+    case LANGUAGE_NEGOTIATION_URL_DOMAIN:
+      foreach ($languages as $language) {
+        $host = parse_url($language->domain, PHP_URL_HOST);
+        if ($host && ($_SERVER['HTTP_HOST'] == $host)) {
+          $url_language = $language->language;
+          break;
+        }
+      }
+      break;
+  }
+
+  return $url_language;
+}
+
+/**
+ * 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_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);
+}
+
+/**
+ * Rewrite URLs with language based prefix.
+ *
+ * @param $path
+ *   The path to rewrite.
+ * @param $options
+ *   An associative array of additional options as in url().
+ */
+function language_url_rewrite(&$path, &$options) {
+  global $language, $user;
 
   // Only modify relative (insite) URLs.
   if (!$options['external']) {
@@ -113,32 +306,38 @@ function language_url_rewrite(&$path, &$
       $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;
+    // If the user is anonymous, the user language provider is enabled for
+    // the UI language negotiation, and the corresponding option has been set,
+    // we must preserve any explicit user language preference even with cookies
+    // disabled.
+    // We don't make any check on the language validity here, as it would be too
+    // much expensive in a page with many links.
+    if (!$user->uid && isset($_GET['language_interface']) && variable_get('language_negotiation_user_request_param', FALSE) &&
+      language_get_negotiation(LANGUAGE_TYPE_INTERFACE, LANGUAGE_NEGOTIATION_USER)) {
+      if (is_string($options['query'])) {
+        $options['query'] = drupal_query_string_decode($options['query']);
+      }
+      if (!isset($options['query']['language_interface'])) {
+        $options['query']['language_interface'] = check_plain($_GET['language_interface']);
+      }
+    }
 
-      case LANGUAGE_NEGOTIATION_PATH_DEFAULT:
-        $default = language_default();
-        if ($options['language']->language == $default->language) {
+    if (language_get_negotiation(LANGUAGE_TYPE_CONTENT, LANGUAGE_NEGOTIATION_URL)) {
+      switch (variable_get('language_negotiation_url', LANGUAGE_NEGOTIATION_URL_PREFIX)) {
+        case 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;
-        }
-        // Intentionally no break here.
 
-      case LANGUAGE_NEGOTIATION_PATH:
-        if (!empty($options['language']->prefix)) {
-          $options['prefix'] = $options['language']->prefix . '/';
-        }
-        break;
+        case LANGUAGE_NEGOTIATION_URL_PREFIX:
+          if (!empty($options['language']->prefix)) {
+            $options['prefix'] = $options['language']->prefix . '/';
+          }
+          break;
+      }
     }
   }
 }
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	31 Aug 2009 11:45:54 -0000
@@ -476,33 +476,209 @@ 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. <strong>Modifying this setting may break all incoming URLs and should be used with caution in a production environment.</strong>")
-  );
+  $form = array();
+
+  $title = t('Content language negotiation');
+  $description = '';
+  $table_form = _locale_languages_configure_form_language_table(LANGUAGE_TYPE_CONTENT, $title, $description);
+  $form[LANGUAGE_TYPE_CONTENT] = $table_form;
+
+  $title = t('Interface language negotiation');
+  $description = '';
+  $table_form = _locale_languages_configure_form_language_table(LANGUAGE_TYPE_INTERFACE, $title, $description);
+  $form[LANGUAGE_TYPE_INTERFACE] = $table_form;
+
   $form['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save settings')
   );
+
+  $form['#theme'] = 'locale_languages_configure_form';
+
   return $form;
 }
 
 /**
+ * Helper function to build a language provider table.
+ */
+function _locale_languages_configure_form_language_table($type, $title, $description) {
+  require_once DRUPAL_ROOT . '/includes/language.inc';
+
+  $table_form = array('#tree' => TRUE);
+  $table_form['weight'] = array('#tree' => TRUE);
+  $table_form['enabled'] = array('#tree' => TRUE);
+  $table_form['#language_providers'] = array();
+  $table_form['#title'] = $title;
+  $table_form['#description'] = $description;
+  $table_form['#show_operations'] = FALSE;
+
+  $language_providers = language_negotiation_info();
+  $enabled_providers = variable_get("language_providers_enabled_$type", array());
+  $providers_weight = variable_get("language_providers_weight_$type", array());
+  $ordered_list = $providers_weight + $language_providers;
+
+  foreach ($ordered_list as $id => $value) {
+    $provider = $language_providers[$id];
+    if (!isset($provider['type']) || $provider['type'] & $type) {
+      $table_form['#language_providers'][$id] = $provider;
+      $table_form['weight'][$id] = array(
+        '#type' => 'weight',
+        '#default_value' => @$providers_weight[$id],
+        '#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_providers[$id]);
+      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_path'])) {
+        $config_op = l(t('Configure'), $provider['config_path']);
+        // 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);
+    }
+  }
+
+  return $table_form;
+}
+
+/**
+ * Theme the language configure form.
+ *
+ * @ingroup themeable
+ */
+function theme_locale_languages_configure_form($form) {
+  $output = '';
+
+  foreach (array(LANGUAGE_TYPE_CONTENT, LANGUAGE_TYPE_INTERFACE) as $type) {
+    $rows = array();
+    $output .= '<label>' . $form[$type]['#title'] . '</label>';
+    $output .= '<div class="description">' . $form[$type]['#description'] . '</div>';
+
+    foreach ($form[$type]['title'] as $id => $element) {
+      // Do not take form control structures.
+      if (is_array($element) && element_child($id)) {
+        $row = array(
+          'data' => array(
+            '<strong>' . drupal_render($form[$type]['title'][$id]) . '</strong>',
+            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'));
+    }
+
+    $output .= theme('table', $header, $rows, array('id' => "language-negotiation-providers-$type"));
+    $output .= drupal_render_children($form[$type]);
+
+    drupal_add_tabledrag("language-negotiation-providers-$type", 'order', 'sibling', "language-provider-weight-$type");
+
+    $output = '<div class="form-item">' . $output . '</div>';
+  }
+
+  $output .= drupal_render_children($form);
+  return $output;
+}
+
+/**
  * Submit function for language negotiation settings.
  */
 function locale_languages_configure_form_submit($form, &$form_state) {
-  variable_set('language_negotiation', $form_state['values']['language_negotiation']);
+  // Save only the necessary fields.
+  $provider_fields = array('callback', 'file', 'cache');
+
+  foreach (array(LANGUAGE_TYPE_CONTENT, LANGUAGE_TYPE_INTERFACE) as $type) {
+    $weight = 0;
+    $negotiation = array();
+    $enabled_providers = $form_state['values'][$type]['enabled'];
+    $enabled_providers[LANGUAGE_NEGOTIATION_DEFAULT] = TRUE;
+    $providers_weight = $form_state['values'][$type]['weight'];
+    asort($providers_weight);
+
+    foreach ($providers_weight as $id => $null) {
+      if ($enabled_providers[$id]) {
+        $provider = $form[$type]['#language_providers'][$id];
+        $negotiation[$id] = array();
+        foreach ($provider_fields as $field) {
+          if (isset($provider[$field])) {
+            $negotiation[$id][$field] = $provider[$field];
+          }
+        }
+      }
+      $providers_weight[$id] = $weight++;
+    }
+
+    variable_set("language_negotiation_$type", $negotiation);
+    variable_set("language_providers_enabled_$type", $enabled_providers);
+    variable_set("language_providers_weight_$type", $providers_weight);
+  }
+
   drupal_set_message(t('Language negotiation configuration saved.'));
   $form_state['redirect'] = 'admin/config/regional/language';
-  return;
 }
+
+/**
+ * The URL language provider configuration form.
+ */
+function locale_language_providers_url_form() {
+  $form = array();
+
+  $form['language_negotiation_url'] = array(
+    '#title' => t('URL language indicator'),
+    '#type' => 'radios',
+    '#options' => array(
+      LANGUAGE_NEGOTIATION_URL_PREFIX => t('Path prefix.'),
+      LANGUAGE_NEGOTIATION_URL_DOMAIN => t('Domain.'),
+    ),
+    '#default_value' => variable_get('language_negotiation_url', LANGUAGE_NEGOTIATION_URL_PREFIX),
+    '#description' => t('Select which part of the URL will determine the language. <strong>Modifying this setting may break all incoming URLs and should be used with caution in a production environment.</strong>')
+  );
+
+  $form['#redirect'] = 'admin/config/regional/language/configure';
+
+  return system_settings_form($form);
+}
+
+/**
+ * The user language provider configuration form.
+ */
+function locale_language_providers_user_form() {
+  $form = array();
+
+  $form['language_negotiation_user_request_param'] = array(
+    '#title' => t('Enable request/session parameter'),
+    '#type' => 'checkbox',
+    '#default_value' => variable_get('language_negotiation_user_request_param', FALSE),
+    '#description' => t('If this is checked the user will be allowed to set its preferred language through the <code>language_ui</code> request parameter. If the user is logged this choice will be remembered through session, otherwise the request parameter will be sticky.')
+  );
+
+  $form['#redirect'] = 'admin/config/regional/language/configure';
+
+  return system_settings_form($form);
+}
+
 /**
  * @} End of "locale-languages-negotiation"
  */
Index: modules/locale/locale.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.install,v
retrieving revision 1.47
diff -u -p -r1.47 locale.install
--- modules/locale/locale.install	17 Aug 2009 19:14:40 -0000	1.47
+++ modules/locale/locale.install	31 Aug 2009 12:28:20 -0000
@@ -46,6 +46,42 @@ 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(LANGUAGE_NEGOTIATION_URL);
+      break;
+
+    // LANGUAGE_NEGOTIATION_PATH
+    case 2:
+      $negotiation = array(LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_USER, LANGUAGE_NEGOTIATION_BROWSER);
+      break;
+
+    // LANGUAGE_NEGOTIATION_DOMAIN
+    case 3:
+      variable_set('language_negotiation_url', LANGUAGE_NEGOTIATION_URL_DOMAIN);
+      $negotiation = array(LANGUAGE_NEGOTIATION_URL);
+      break;
+  }
+
+  language_set_negotiation(LANGUAGE_TYPE_CONTENT, $negotiation);
+  language_set_negotiation(LANGUAGE_TYPE_INTERFACE, array(LANGUAGE_NEGOTIATION_CONTENT));
+  variable_del('language_negotiation');
+
+  return array();
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  */
 
@@ -67,12 +103,19 @@ function locale_uninstall() {
   // Clear variables.
   variable_del('language_default');
   variable_del('language_count');
-  variable_del('language_negotiation');
-  variable_del('javascript_parsed');
+  variable_del('language_negotiation_url');
+  variable_del('language_negotiation_user_request_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 (array(LANGUAGE_TYPE_CONTENT, LANGUAGE_TYPE_INTERFACE) as $type) {
+    variable_del("language_negotiation_$type");
+    variable_del("language_providers_enabled_$type");
+    variable_del("language_providers_weight_$type");
+  }
 
   foreach (node_type_get_types() as $type => $content_type) {
     $setting = variable_del('language_content_type_' . $type);
Index: modules/locale/locale.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v
retrieving revision 1.257
diff -u -p -r1.257 locale.module
--- modules/locale/locale.module	29 Aug 2009 05:46:03 -0000	1.257
+++ modules/locale/locale.module	31 Aug 2009 10:24:25 -0000
@@ -37,14 +37,6 @@ function locale_help($path, $arg) {
       return $output;
     case 'admin/config/regional/language/add':
       return '<p>' . t('Add all languages to be supported by your site. If your desired language is not available in the <em>Language name</em> drop-down, click <em>Custom language</em> 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.') . '</p>';
-    case 'admin/config/regional/language/configure':
-      $output = '<p>' . t("Language negotiation settings determine the site's presentation language. Available options include:") . '</p>';
-      $output .= '<ul><li>' . t('<strong>None.</strong> The default language is used for site presentation, though users may (optionally) select a preferred language on the <em>My Account</em> page. (User language preferences will be used for site e-mails, if available.)') . '</li>';
-      $output .= '<li>' . t('<strong>Path prefix only.</strong> 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. <em>Example: "example.com/de/contact" sets presentation language to German based on the use of "de" within the path.</em>') . '</li>';
-      $output .= '<li>' . t("<strong>Path prefix with language fallback.</strong> 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 <em>My Account</em> page, or by the browser's language settings. If a presentation language cannot be determined, the default language is used.") . '</li>';
-      $output .= '<li>' . t('<strong>Domain name only.</strong> 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. <em>Example: "http://de.example.com/contact" sets presentation language to German based on the use of "http://de.example.com" in the domain.</em>') . '</li></ul>';
-      $output .= '<p>' . t('The path prefix or domain name for a language may be set by editing the <a href="@languages">available languages</a>. In the absence of an appropriate match, the site is displayed in the <a href="@languages">default language</a>.', array('@languages' => url('admin/config/regional/language'))) . '</p>';
-      return $output;
     case 'admin/config/regional/translate':
       $output = '<p>' . t('This page provides an overview of available translatable strings. Drupal displays translatable strings in text groups; modules may define additional text groups containing other translatable strings. Because text groups provide a method of grouping related strings, they are often used to focus translation efforts on specific areas of the Drupal interface.') . '</p>';
       $output .= '<p>' . t('Review the <a href="@languages">languages page</a> for more information on adding support for additional languages.', array('@languages' => url('admin/config/regional/language'))) . '</p>';
@@ -103,6 +95,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/user'] = array(
+    'title' => 'User language provider configuration',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('locale_language_providers_user_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 +273,13 @@ function locale_language_selector_form($
   );
 
   // Get language negotiation settings.
-  $mode = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE);
+  $mode = language_get_negotiation(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 +345,61 @@ 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_negotiation_info():
+ */
+function locale_language_negotiation_info() {
+  $providers = array();
+
+  $providers[LANGUAGE_NEGOTIATION_URL] = array(
+    'title' => t('URL'),
+    'description' => t('The language is determined from the URL (Path prefix or domain).'),
+    'callback' => 'language_from_url',
+    'config_path' => 'admin/config/regional/language/configure/url',
+  );
+
+  $providers[LANGUAGE_NEGOTIATION_CONTENT] = array(
+    'title' => t('Content'),
+    'description' => t('The interface language is the same the negotiated content language.'),
+    'callback' => 'language_from_content',
+    'type' => LANGUAGE_TYPE_INTERFACE,
+  );
+
+  $providers[LANGUAGE_NEGOTIATION_USER] = array(
+    'title' => t('User'),
+    'description' => t('The language is determined from the language preference set in the user account.'),
+    'callback' => 'language_from_user',
+    'config_path' => 'admin/config/regional/language/configure/user',
+    'type' => LANGUAGE_TYPE_INTERFACE,
+  );
+
+  $providers[LANGUAGE_NEGOTIATION_BROWSER] = array(
+    'title' => t('Browser'),
+    'description' => t('The language is determined from the browser\'s language settings.'),
+    'callback' => 'language_from_browser',
+    'type' => LANGUAGE_TYPE_INTERFACE,
+    'cache' => CACHE_DISABLED,
+  );
+
+  $language = language_default();
+  $providers[LANGUAGE_NEGOTIATION_DEFAULT] = array(
+    'title' => t('Default'),
+    'description' => t('The default site language (@language_name) is used.', array('@language_name' => $language->native)),
+    'callback' => '',
+  );
+
+  return $providers;
+}
+
 // ---------------------------------------------------------------------------------
 // Locale core functionality
 
@@ -633,8 +690,10 @@ function locale_css_alter(&$css) {
  */
 function locale_block_info() {
   $block['language-switcher']['info'] = t('Language switcher');
+  $block['language-ui-switcher']['info'] = t('Interface language switcher');
   // Not worth caching.
   $block['language-switcher']['cache'] = BLOCK_NO_CACHE;
+  $block['language-ui-switcher']['cache'] = BLOCK_NO_CACHE;
   return $block;
 }
 
@@ -646,10 +705,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();
+    case 'language-ui-switcher':
+      return _locale_language_ui_switcher();
+  }
+}
+
+/**
+ * Return the language switcher block.
+ */
+function _locale_language_switcher() {
+  if (variable_get('language_count', 1) > 1 && language_get_negotiation(LANGUAGE_TYPE_CONTENT) != LANGUAGE_NEGOTIATION_DEFAULT) {
     $path = drupal_is_front_page() ? '<front>' : $_GET['q'];
     $languages = language_list('enabled');
     $links = array();
+
     foreach ($languages[1] as $language) {
       $links[$language->language] = array(
         'href'       => $path,
@@ -669,6 +741,35 @@ function locale_block_view($delta = '') 
 }
 
 /**
+ * Return the UI language switcher block.
+ */
+function _locale_language_ui_switcher() {
+  if (variable_get('language_count', 1) > 1 && language_get_negotiation(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT) {
+    global $language_interface;
+
+    $path = drupal_is_front_page() ? '<front>' : $_GET['q'];
+    $languages = language_list('enabled');
+    $links = array();
+
+    foreach ($languages[1] as $language) {
+      $langcode = $language->language;
+      $links[$langcode] = array(
+        'href'       => $path,
+        'title'      => $language->native,
+        'attributes' => array('class' => array('language-link')),
+      );
+      if ($language_interface->language != $langcode) {
+        $links[$langcode]['query'] = 'language_ui=' . $langcode;
+      }
+    }
+
+    $block['subject'] = t('Languages');
+    $block['content'] = theme('links', $links, array());
+    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	31 Aug 2009 10:24:25 -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,21 @@ 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)));
+    global $language_interface;
+    $this->assertEqual($language_interface->language, $this->language_interface, t('Current language: %lang', array('%lang' => $language_interface->language)));
 
     // Change language negotiation options.
-    variable_set('language_negotiation', LANGUAGE_NEGOTIATION_PATH_DEFAULT);
+    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());
+
+    // Change language providers settings.
+    variable_set('language_negotiation_url', LANGUAGE_NEGOTIATION_URL_PREFIX);
+    variable_set('language_negotiation_user_request_param', TRUE);
 
     // Enable multilingual workflow option for articles.
     variable_set('language_content_type_article', 1);
@@ -968,7 +974,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,9 +984,15 @@ 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_get_negotiation(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_get_negotiation(LANGUAGE_TYPE_CONTENT) == LANGUAGE_NEGOTIATION_DEFAULT;
+    $this->assertTrue($language_negotiation, t('Content language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set'))));
+
+    // Check language providers settings.
+    $this->assertFalse(variable_get('language_negotiation_url', FALSE), t('URL language provider settings cleared.'));
+    $this->assertFalse(variable_get('language_negotiation_user_request_param', FALSE), t('User language provider settings cleared.'));
+    
     // Check JavaScript parsed.
     $javascript_parsed_count = count(variable_get('javascript_parsed', array()));
     $this->assertEqual($javascript_parsed_count, 0, t('JavaScript parsed count: %count', array('%count' => $javascript_parsed_count)));
@@ -1011,14 +1023,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 +1073,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 +1252,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 +1359,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 +1413,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 +1422,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 +1430,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 +1447,8 @@ class UILanguageNegotiationTest extends 
 
   function setUp() {
     parent::setUp('locale', 'locale_test');
+    require_once DRUPAL_ROOT . '/includes/language.inc';
+    drupal_load('module', 'locale');
   }
 
   /**
@@ -1508,43 +1511,43 @@ class UILanguageNegotiationTest extends 
     $tests = array(
       // Default, browser preference should have no influence.
       array(
-        'language_negotiation' => LANGUAGE_NEGOTIATION_PATH_DEFAULT,
+        'language_negotiation' => array(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(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(LANGUAGE_NEGOTIATION_URL, 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(LANGUAGE_NEGOTIATION_URL, 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(LANGUAGE_NEGOTIATION_URL, 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 +1556,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(LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT),
+        'language_negotiation_url' => 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(LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT),
+        'language_negotiation_url' => 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 +1596,10 @@ class UILanguageNegotiationTest extends 
 
   private function runTest($test) {
     if (!empty($test['language_negotiation'])) {
-      variable_set('language_negotiation', $test['language_negotiation']);
+      language_set_negotiation(LANGUAGE_TYPE_INTERFACE, $test['language_negotiation']);
+    }
+    if (!empty($test['language_negotiation_url'])) {
+      variable_set('language_negotiation_url', $test['language_negotiation_url']);
     }
     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.1116
diff -u -p -r1.1116 node.module
--- modules/node/node.module	29 Aug 2009 21:05:16 -0000	1.1116
+++ modules/node/node.module	31 Aug 2009 12:41:18 -0000
@@ -1079,6 +1079,51 @@ function node_build_content($node, $buil
 }
 
 /**
+ * Implement hook_language_negotiation_info():
+ */
+function node_language_negotiation_info() {
+  $providers = array();
+
+  $providers['node-language'] = array(
+    'title' => t('Node'),
+    'description' => t('The current node language is used.'),
+    'callback' => 'node_language_provider',
+    'type' => LANGUAGE_TYPE_CONTENT,
+    '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_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
@@ -1298,7 +1343,7 @@ function node_search_admin() {
   $form['content_ranking']['info'] = array(
     '#value' => '<em>' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em>'
   );
-  
+
   // Note: reversed to reflect that higher number = higher ranking.
   $options = drupal_map_assoc(range(0, 10));
   foreach (module_invoke_all('ranking') as $var => $values) {
@@ -1323,7 +1368,7 @@ function node_search_execute($keys = NUL
     ->condition('n.status', 1)
     ->addTag('node_access')
     ->searchExpression($keys, 'node');
-  
+
   // Insert special keywords.
   $query->setOption('type', 'n.type');
   $query->setOption('language', 'n.language');
@@ -1334,10 +1379,10 @@ function node_search_execute($keys = NUL
   if (!$query->executeFirstPass()) {
     return array();
   }
-  
+
   // Add the ranking expressions.
   _node_rankings($query);
-  
+
   // Add a count query.
   $inner_query = clone $query;
   $count_query = db_select($inner_query->fields('i', array('sid')));
@@ -1346,7 +1391,7 @@ function node_search_execute($keys = NUL
   $find = $query
     ->limit(10)
     ->execute();
-  
+
   // Load results.
   $results = array();
   foreach ($find as $item) {
@@ -1354,14 +1399,14 @@ function node_search_execute($keys = NUL
     $node = node_load($item->sid);
     $node = node_build_content($node, 'search_result');
     $node->rendered = drupal_render($node->content);
-  
+
     // Fetch comments for snippet.
     $node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node);
     // Fetch terms for snippet.
     $node->rendered .= ' ' . module_invoke('taxonomy', 'node_update_index', $node);
-  
+
     $extra = module_invoke_all('node_search_result', $node);
-  
+
     $results[] = array(
       'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
       'type' => check_plain(node_type_get_name($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	31 Aug 2009 10:24:25 -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);
+    drupal_load('module', 'locale');
+    variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info());
+    variable_set('language_negotiation_url', LANGUAGE_NEGOTIATION_URL_PREFIX);
 
     // Force inclusion of language.inc.
     drupal_language_initialize();
