I don't understand the logic behind the implementation of the site-wide language negotiation setting vs. user-specified language preference setting. In my opinion, the help text seems to indicate that the user has some option of enforcing a language for the interface, but according to keith, the language is limited only to site e-mails.

I'm trying to clear the confusion by looking at the code.

This is the language_initialize() function taken from language.inc. I have broken it in smaller blocks and provided my interpretation of the code:

/**
 *  Choose a language for the page, based on language negotiation settings.
 */
function language_initialize() {
  global $user;

  // Configured presentation language mode.
  $mode = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE);

This retrieves the language negotiation mode, which should be an integer from 0 to 3 corresponding to the four choices defined in bootstrap.inc.

In case no setting for this value was found, the default value of LANGUAGE_NEGOTIATION_NONE is used. This is the usual path, since the variable language_negotiation doesn't exist on a fresh Drupal 6 install.

  // Get a list of enabled languages.
  $languages = language_list('enabled');
  $languages = $languages[1];
  
  switch ($mode) {
    case LANGUAGE_NEGOTIATION_NONE:
      return language_default();

This case statement ends the execution of the function and returns the default language specified for the site. I see this as a very deterministic phase: now we have the language, nothing to do here anymore, so let's exit and use this language. And this is the part that will be executed always, when no negotiation setting has been set. (I'm starting to repeat myself...)

In the case some other negotiation mode is selected, the appropriate processing will be done:

    case LANGUAGE_NEGOTIATION_DOMAIN:
      foreach ($languages as $language) {
        $parts = parse_url($language->domain);
        if (!empty($parts['host']) && ($_SERVER['SERVER_NAME'] == $parts['host'])) {
          return $language;
        }
      }
      return language_default();

    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;
  }

Lot's of "returns $languages;", as if all cases would be covered?

Actually, no.

If none of the above conditions (yes, there are more of them!) is met, the function still continues with these lines:

  // User language.
  if ($user->uid && isset($languages[$user->language])) {
    return $languages[$user->language];
  }

  // Browser accept-language parsing.
  if ($language = language_from_browser()) {
    return $language;
  }

  // Fall back on the default if everything else fails.
  return language_default();
}

This was the interesting part, since I don't see when the execution is supposed to get to this last part:

  if ($user->uid && isset($languages[$user->language])) {
    return $languages[$user->language];
  }

I would really like that Drupal just execute this block. Now the whole function seems to be coded as if this section is not meant to be executed at all. Or is it? I don't know. It's confusing -- exactly like the help text for the setting.

By looking at the code, it looks like I am supposed to cheat by inserting any value NOT listed in the case blocks:

UPDATE variable SET value = 's:2:"-1";' WHERE name = 'language_negotiation';

To make it cleaner, I would create a new constant:

define('LANGUAGE_NEGOTIATION_REALLY_NONE', -1);

.. and make a proper case block in language_initialize() to handle this.

In my opinion, this should be classified as a bug, not a user interface text issue. And definitively not something to be pushed to 7.0.

Comments

Damien Tournoud’s picture

Priority: Critical » Normal
Status: Active » Closed (duplicate)

Ok, just for fun, let's look at the available options, from the help text:

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.

That's what you are seeing, right? (In all cases, the email preferences are dealt with by user_preferred_language())

    case LANGUAGE_NEGOTIATION_NONE:
      return language_default();
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.

This is:

    case LANGUAGE_NEGOTIATION_DOMAIN:
      foreach ($languages as $language) {
        $parts = parse_url($language->domain);
        if (!empty($parts['host']) && ($_SERVER['SERVER_NAME'] == $parts['host'])) {
          return $language;
        }
      }
      return language_default();
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.

This dealt with by this block:

    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();
      }
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.

This is why the code flow continues below to:

  // User language.
  if ($user->uid && isset($languages[$user->language])) {
    return $languages[$user->language];
  }

  // Browser accept-language parsing.
  if ($language = language_from_browser()) {
    return $language;
  }

  // Fall back on the default if everything else fails.
  return language_default();

Ok. So guess what? There is no implementation error here. As I told you on #222401: Code block never executes in language_initialize(), please open a new issue only if you want to discuss the design choice behind language management. The proper way to do this is by opening a *feature request*. The request will have to go to 7.x, because Drupal 6 is feature-frozen.

This is nothing more than a duplicate of #222401.

htalvitie’s picture

Priority: Normal » Critical
Status: Closed (duplicate) » Active

I don't agree with the part where you say "This is why the code flow continues below to:".

How could it?

There are four constants available for the language negotiation:

- LANGUAGE_NEGOTIATION_NONE
- LANGUAGE_NEGOTIATION_DOMAIN
- LANGUAGE_NEGOTIATION_PATH_DEFAULT
- LANGUAGE_NEGOTIATION_PATH

The switch-case block in the beginning of language_initialize() covers all these, and always returns before this code is reached, where the user option is processed:

  // User language.
  if ($user->uid && isset($languages[$user->language])) {
    return $languages[$user->language];
  }

If you disagree, please describe when and how that block could be reached? Also, debug and confirm it before you come back.

Can we maybe get a second opinion here, please? I am really getting tired arguing with somebody, who clearly doesn't have a debugger in hand.

Damien Tournoud’s picture

Priority: Critical » Normal
Status: Active » Closed (duplicate)

The switch-case block in the beginning of language_initialize() covers all these, and always returns before this code is reached.

Of course not, open your eyes:

      foreach ($languages as $language) {
        if (!empty($language->prefix) && $language->prefix == $prefix) {
          // Rebuild $GET['q'] with the language removed.
          $_GET['q'] = implode('/', $args);
          return $language;
        }
      }

This will not return if $prefix is not found in any of the $language->prefix.

And the following:

      if ($mode == LANGUAGE_NEGOTIATION_PATH_DEFAULT) {
        // If we did not found the language by prefix, choose the default.
        return language_default();
      }

And this will not return if $mode is LANGUAGE_NEGOTIATION_PATH.

So if $mode == LANGUAGE_NEGOTIATION_PATH *and* $prefix is not found in any of the $language->prefix, the code flow continues.

God, I wrote that 5 times already. I'm *really* tired of it. This is the time you say "ok, you are right, I'm a moron", and you apologize.

htalvitie’s picture

Ok, I checked and confirmed that when $mode is LANGUAGE_NEGOTIATION_PATH and when there are no prefixes to match the path, the code continues.

But (and this is the big one): When I add a new language, it creates a prefix value to the language table. This also has a side-effect, which alters the links (from http://mysite.com/page to http://mysite.com/[prefix]/page). This breaks things, because I don't want to have separate urls to the same page.

Why would I want to have a separate URL for the same page for a user who has selected the UI language to be different from another user? The content is still the same, only the menus and navigation etc. are in a different language.

So to resolve the issue, I have to manually remove the prefix values from the language table. This makes my links stay as they are, and honors the user language option.

Is this really necessary?

(The status could be changed to "Needs more information?")

htalvitie’s picture

Also, to clarify why our views of the code flow differed:

An option with a name "LANGUAGE_NEGOTIATION_PATH" implies, that I would want to perform the prefix-checking for every page request, when I really don't. I simply want to provide the user a selection of languages for the UI, and not to have my links break based on user's preferences.

So, please allow Drupal to work without a forced language negotiation overhead and allow for a mode, where the user still can choose the UI language.

Gábor Hojtsy’s picture