Flask multilanguage processing, switching and the 404 Page Not Found exception

13 November 2019 Updated 15 November 2019 by Peter

In this post we discuss several conditions when processing the language in the url, using a default language and using a list of available languages.

post main image
https://unsplash.com/@sadswim

How to implement Flask multilanguage is explained in the Flask docs, see links below. But this is just a starting point. You need deeper understanding of the process to handle special cases like falling back to a default language, language switching, and the 404 Page Not Found exception.

Assumptions

In the remainder of this post we are using a language code, 'lang_code', that is available in the url, it is the first part of the url, e.g.:

https://www.example.com/en

https://www.example.com/en/login

We also are using Flask-Babel for the translations. I use blueprints and register them in create_app() as follows:

    # authentication (shared)
    from shared.blueprints.auth.views import auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>/auth')

    # pages (shared)
    from shared.blueprints.pages.views import pages_blueprint
    app.register_blueprint(pages_blueprint, url_prefix='/<lang_code>')

We use g.lang_code as the value of the selected language. I do not use a session variable to remember the language but instead rely on the availability of the language in every request.

Flask multilanguage processing

The way Flask handles multilanguage on a request is as follows:

First, url_value_preprocessor() is called to extract the language from the url. example:

@pages_blueprint.url_value_preprocessor
def pull_lang_code(endpoint, values):
    # pop lang_code from url and set g.lang_code
    ...
    if 'lang_code' in values:
        g.lang_code = values.pop('lang_code')

Next, Babels locale_selector() is called to provide the translations for the page, example:

@babel.localeselector
def get_locale():
    return g.get('lang_code')

Finally url_defaults() is called before the page is built to replace <lang_code> with lang_code in the urls, example:

@pages_blueprint.url_defaults
def add_language_code(endpoint, values):
    # stuff g.lang_code in urls
    ...
    values.setdefault('lang_code', g.lang_code)

This is pretty straightforward but as you can see conditions can occur where the language code is changed, not available or even not valid. The most important thing is to make sure that g.lang_code is always set to a valid language before localeselector() and url_defaults() are called. A number of conditions are discussed below.

Flask multilanguage processing: visitor switching to another language

To switch to another language we can use a GET to the current request with an additional lang_code parameter:

<a href="{{ request.script_root + request.path }}?lc=en">English</a>
<a href="{{ request.script_root + request.path }}?lc=de">Deutsch</a>

We must extend the functionality of the url_value_preprocessor() to support the language switch. Simplified, this extra code looks like:

@pages_blueprint.url_value_preprocessor
def pull_lang_code(endpoint, values):
    ...
    request_lang_code = request.args.get('lc')
    if request_lang_code:
        g.lang_code = request_lang_code
    ...

Flask multilanguage processing: language missing in url and default language

This may not be an error because you may want the vistor to type your domain url and be 'redirected' to the default language pages. But this can also happen if a visitor types a wrong url, a (search) bot calls a wrong url. Again we can handle this in the url_value_preprocessor(). In this case we set the lang_code to the lang_code of the default language:

@pages_blueprint.url_value_preprocessor
def pull_lang_code(endpoint, values):
    ...
    if g.get('lang_code') is None:
        g.lang_code = default_lang_code

Flask multilanguage processing: language not a supported language

Our application supports only a limited number of languages, e.g.:

    available_lang_codes = ['en', 'de']

Again we can handle the invalid language case in the url_value_preprocessor(). If the language is not valid we set the lang_code to the lang_code of the default language:

@pages_blueprint.url_value_preprocessor
def pull_lang_code(endpoint, values):
    ...
    if 'lang_code' in values:
        lang_code = values.pop('lang_code')
        if lang_code not in available_lang_codes:
            g.lang_code = default_lang_code

Flask multilanguage processing: Page Not Found (404) error

This one gave me some headaches, it took me some debugging to see that the flow is different in this case. What happens here is that if none of the blueprints match the request url, url_value_preprocessor() is NEVER called. For example, with the blueprints shown earlier, this is a valid url:

http://127.0.0.1:8000/en/auth/login

but this url gives a 404 exception:

http://127.0.0.1:8000/en/auth/login/something

What to do here? The answer is to process this condition in the Flask @before_request. On a normal flow before_request() is called after (!) url_value_preprocessor():

    @pages_blueprint.url_value_preprocessor
    def pull_lang_code(endpoint, values):
        ....

    @app.before_request
    def before_request():
        ....

In case of a 404 exception url_value_preprocessor() is NOT called but before_request() still is called:

    @app.before_request
    def before_request():
        ....

Normally url_value_preprocessor() will set g.lang_code to a value, a language code. But on a 404, url_value_preprocessor() is not called and g.lang_code is not set. In before_request() we check the value of g.lang_code. If it is not set we can process the request url ourselves. If the first part is a valid language code, we assume this is what we need and set g.lang_code. Otherwise we set g.lang_code to the default language. Then, when the 404 handler is called, the page can be displayed in the proper language.

Summary

I did not use a session variable to store the selected language but instead rely on the language in the url. This works fine if we handle all kinds of conditions like a missing language. Most important is to set the language in g before doing all other processing.

Links / credits

Flask Series: Internationalization
https://damyanon.net/post/flask-series-internationalization/

Flask-Babel
https://pythonhosted.org/Flask-Babel/

Using URL Processors
https://flask.palletsprojects.com/en/1.1.x/patterns/urlprocessors/