Carl Marshall
Carl Marshall

Reputation: 349

Django 1.8 set_language view does not redirect to specified language

In yet another variant of the "set_language isn't working" category of questions, I have this head scratcher.

In my page I have a language selector made of three parts:

  1. Hidden form

    <form action="{% url 'set_language' %}" method="post" id="language-form" name="language-form">
        {% csrf_token %}
        <input type="hidden" name="language" id="language" value="" />
    </form>
    
  2. Javascript form submission function

    function set_language(code) {
        $('#language').val(code);
        $('#language-form').submit();
    }
    
  3. And the html list (shown here as rendered in the browser, rather than the template language):

    <li class="active"><a href="javascript:set_language('en-gb')"><img src="/static/img/locale/en-gb.png"> Great Britain (English) (en-gb)</a></li>
    <li><a href="javascript:set_language('nl-be')"><img src="/static/img/locale/nl-be.png"> Belgium (Dutch) (nl-be)</a></li>
    <li><a href="javascript:set_language('fr-be')"><img src="/static/img/locale/fr-be.png"> Belgium (French) (fr-be)</a></li>
    <li><a href="javascript:set_language('nl-nl')"><img src="/static/img/locale/nl-nl.png"> Netherlands (Dutch) (nl-nl)</a></li>
    <li><a href="javascript:set_language('fy-nl')"><img src="/static/img/locale/fy-nl.png"> Netherlands (Frisian) (fy-nl)</a></li>
    

I can confirm that the above functions as expected, and submits a POST request with content such as:

csrfmiddlewaretoken=5Vcs9XdvHs7uJwPg4BHtnVPhPW3mMT0w&language=nl-nl

and that the development server sees the post, as shown by:

[08/Jul/2015 23:58:33]"POST /i18n/setlang/ HTTP/1.1" 302 0
[08/Jul/2015 23:58:33]"GET /en-gb/ HTTP/1.1" 200 6738

I can also confirm that the various site urls do work when manually entered, so I can goto localhost/en-gb/foo and localhost/fr-be/foo just fine.

I have specifically excluded the next field in the form so that the redirect returns to the current page (and that works as expected too).

The problem is that whilst all these things are triggering, the language isn't being changed after the redirect.

My cookie appears to be devoid of any language data. I've attempted to display the session._language value in the output - that failed as you would expect, so I tried:

{% with settings.LANGUAGE_SESSION_KEY as langkey %}
<p>Lang: {{ request.session.langkey }} {{ langkey }}</p>
{% endwith %}

which returns "Lang:" and that's it (i.e. it looks like it isn't set or is empty).

I'm now at a loss. I believe that set_language should redirect from /en-gb/foo to /fr-be/foo, but this isn't happening. Having looked through the code calls, I can also confirm that from set_language code that check_for_language(lang_code) when lang_code == 'fr-be' returns true (as it does for all the other languages specified in my settings - see below), and returns false for values like 'ff'. It also returns true for values of 'en', 'fr, 'nl', and 'fy' too, even though they don't work as urls (they result in 404 errors).

Can anyone:

  1. Confirm that my expectations for set_language are correct - that it should redirect using the language and country code?
  2. Tell me if this is a bug with Django 1.8.2?
  3. Suggest any other debug methods I may have overlooked?
  4. Suggest a simple work around if (1) or (2) are wrong?

Thank you

Settings.py

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.security.SecurityMiddleware',
)

ROOT_URLCONF = 'wp4.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.core.context_processors.i18n',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/

LANGUAGE_CODE = 'en-gb'

LANGUAGES = (
    ('en-gb', _('Great Britain (English)')),
    ('nl-be', _('Belgium (Dutch)')),
    ('fr-be', _('Belgium (French)')),
    ('nl-nl', _('Netherlands (Dutch)')),
    ('fy-nl', _('Netherlands (Frisian)')),
)

LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale'),
)

TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

Upvotes: 2

Views: 2473

Answers (1)

Carl Marshall
Carl Marshall

Reputation: 349

Ok, few bits of further observation have resulted in the following answers and opinions.

To answer my specific questions first:

  1. My expectations are mostly correct, set_language should redirect to the new language for the page, but there is a caveat. See below.
  2. I think there is a logic bug, and will submit as such to the django project.
  3. The only suggestion I received was to check the session variable values, and I'll report on that below too.
  4. The key workaround is to set 'next' field to your current url, but without the language code prefix.

This is where I venture into opinion based on reviewing the django code and paths.

I think there is a logic error in the set_language() code and documentation. This is perhaps more an expectation against what the code says it does, as the docs list the resolution of next as:

  1. Django looks for a next parameter in the POST data.
  2. If that doesn’t exist, or is empty, Django tries the URL in the Referrer header.
  3. If that’s empty – say, if a user’s browser suppresses that header – then the user will be redirected to / (the site root) as a fallback.

I left next empty thinking to trigger step 2. My expectation is that if the new language was 'fr-be', that it would translate the url from /nl-be/foo/bar/ into /fr-be/foo/bar/. It doesn't. Instead it sets next to remain as /nl-be/foo/bar/ and then sets the session._language value to 'fr-be'. This session value is then ignored as the page is loaded, because the locale middleware (get_language_from_request()) processes the request by looking for a language code in the url first, and only if it doesn't find one, does it look at the other options (such as session, cookie, request data, settings).

This means that the session value of 'fr-be' is ignored in favour of the 'nl-be' in the url, and then something else clears the session value before the template is rendered (which is why I find the session value to be None when I query it in the template).

I would argue that this is a logic misstep, because if you wanted to change the language and have i18n running, then the url is always going to have the language code of your present language in it, so step 2 would always result in the language change being ignored.

Whilst I'm going to use my workaround (i.e. ensuring that every page knows its own urls minus the language code prefix, and setting that as the next value explicitly), I think this is something that should be changed (or at least the docs made explicit that you can't change language using the second method of determining next).

Upvotes: 7

Related Questions