Building a multilanguage Flask website with Flask-Babel
Not many examples exist for multilanguage Flask. Here we follow the suggestions from the Flask documentation.
With a single language there is not really a problem, we just forget about the rest of the world and build our single language Flask application. We start getting a headache when the website must support multiple languages. What exactly is a website supporting multiple languages? How many languages will be supported, and which languages? For English there are for example en-GB and en-US. Which parts of the website must be available in all languages? What are the parts of a website anyway? I will limit myself here answering the most trivial questions. For a good introduction you also can check e.g. the Wordpress Polylang plugin guide, see below.
When you build a website without supporting multiple languages you use urls for your content, when you want to add more languages you must maintain all urls, i.e. what was already created. From a SEO point of view you better start with all the multilanguage components in place even if you doubt now if you will ever add another language.
Language selection
When the language is in a cookie (session) proper language viewing can be a problem when cookies are disabled. With the language in the domain / url this problem is eliminated and also the website is more SEO friendly.
Option 1: language in domain extension
example.com
example.es
example.pl
Example: toyota
https://www.toyota.de
https://www.toyota.es
Option 2: language in subdomain
en.example.com
es.example.com
pl.example.com
Example: cnn
https://edition.cnn.com
https://cnnespanol.cnn.com
Option 3: language in url path
example.com/en
example.com/es
example.com/pl
Translated urls
Translated urls are very friendly, but also introduce more complexity:
https://edition.cnn.com/entertainment
https://cnnespanol.cnn.com/seccion/entretenimiento
Some websites do not have translated urls:
https://www.tesla.com/nl_NL/model3/design#battery
https://www.tesla.com/de_CH/model3/design#battery
If we want to have translated language urls, and why wouldn't we, then we also need multiple endpoints for a view function:
@pages.route('/<lang_code>/about-us', methods=['GET', 'POST'])
@pages.route('/<lang_code>/uber-uns', methods=['GET', 'POST'])
def about():
return render_template(...)
Of course we want this somehow automated ... difficult.
Making choices
For the moment I focus on a website with the 'language id' in the url path, see above option 3, and I will not deal with translated urls. Because we are using Flask, we use Flask-Babel for our translations. Some texts, e.g. blogs, categories, tags, are in the database. We will deal with these later.
You may want a website with the main language without the 'language id' in the url path and the others languages with the 'language id' in the url path. I do not really know how to do this with Flask at the moment and also believe this very much complicates things. Also think of SEO. Assume you want to add another language after a few years then you cannot change your urls. Summary: I think it is not a bad idea to start with the language in the url path.
In a first implementation I used a cookie for the language selection, now I must remove this and use the language in the url. Fortunately the Flask documentation gives good information about internationalization. I suggest you read this (I did many times). I have many blueprints, for the home page view.py I add the following to the view:
...
home_blueprint = Blueprint('home', __name__)
# lang_code in urls
@home_blueprint.url_defaults
def add_language_code(endpoint, values):
values.setdefault('lang_code', g.lang_code)
@home_blueprint.url_value_preprocessor
def pull_lang_code(endpoint, values):
url_lang_code_items_values = get_url_lang_code_items_values()
url_lang_code_default_item_value = get_url_lang_code_default_item_value()
g.lang_code = url_lang_code_default_item_value
if values:
if 'lang_code' in values:
if values['lang_code'] in url_lang_code_items_values:
g.lang_code = values.pop('lang_code', None)
else:
pass
...
The function get_url_lang_code_items_values() returns a list of lang_codes: en, nl, es, and function get_url_lang_code_default_item_value() returns en, so English is the default language. Then in __init__.py I register the home blueprint:
from .blueprints.home.views import home_blueprint
app.register_blueprint(home_blueprint, url_prefix='/<lang_code>')
What happens if we type a url without a path, or a totally random url? You will get a error message:
TypeError: homepage() got an unexpected keyword argument 'lang_code'
The Flask docs do not give a solution, but after some headaches (again) I think I found a solution to solve this using the before_request handler. In this handler I look at the request url. The path of this url is split into parts. The first part must be our language. If the first part is in our list of languages then just continue. If the page cannot be found Flask will generate a 404, page not found, which is fine. If the first part is not our list of languages, then the before_request handler returns a redirect url to the home page of the default language.
After implementing this the website was shown without styles and I got strange error messages in the Developer tools console. The solution was to exclude the static directory. So here is what is going on in the before_request handler:
@app.before_request
def before_request():
# try to handle missing lang_code in url interceptor
url_lang_code_items_values = get_url_lang_code_items_values()
url_lang_code_default_item_value = get_url_lang_code_default_item_value()
# check for a valid url = starts with /lang_code/
request_path = request.path.strip('/')
request_path_parts = urlparse(request_path).path.split('/')
if request.method in ['GET'] and len(request_path_parts) > 0:
request_path_part_0 = request_path_parts[0]
# do nothing with static urls !!!
if request_path_part_0 != 'static' and request_path_part_0 not in url_lang_code_items_values:
# fucked up url
redir_url_parts = []
redir_url_parts.append( request.url_root.strip('/') )
redir_url_parts.append( url_lang_code_default_item_value.strip('/') )
redir_url = '/'.join(redir_url_parts)
return redirect(redir_url)
This works but how do we change the language?
In the base.html template I have a dropdown language selector:
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ language_selected }}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ request.path }}?lc=en_US">{{ _('EN') }}</a>
<a class="dropdown-item" href="{{ request.path }}?lc=nl_NL">{{ _('NL') }}</a>
<a class="dropdown-item" href="{{ request.path }}?lc=es_ES">{{ _('ES') }}</a>
</div>
</li>
Next we trigger the Babel language localeselector when the language is changed:
@babel.localeselector
def get_locale():
request_lc = request.args.get('lc')
if not request_lc:
if not 'lang_code' in g:
# use default
g.lang_code = 'en'
request_lc = 'en_US'
else:
if g.lang_code == 'es':
request_lc = 'es_ES'
elif g.lang_code == 'nl':
request_lc = 'nl_NL'
else:
request_lc = 'en_US'
else:
# set g.lang_code to the requested language
if request_lc == 'nl_NL':
g.lang_code = 'nl'
elif request_lc == 'es_ES':
g.lang_code = 'es'
else:
request_lc = 'en_US'
g.lang_code = 'en'
#sys.exit()
session['lc'] = request_lc
return request_lc
Ok, it's working now but must be optimized. Also there are still two problems at the moment:
- When the language is changed, this is not shown in url afterwards.
You have to go to a new page. After changing the language with the dropdown the url does not show the language in the url immediately, only on the next click. Maybe I should move the Babel code to the before_request handler? - Many calls to url_defaults on every call
I would like to have the code:
@home_blueprint.url_defaults
@home_blueprint.url_value_preprocessor
in the __init__.py instead of in the blueprint views.py but this does not work.
Why do I want this? Because I do not like duplicating the same code and I see many calls to @auth_blueprint.url etc. I believe they should be done once in __init__.py but maybe I am wrong.
Links / credits
Flask Series: Internationalization
https://damyanon.net/post/flask-series-internationalization/
Flask-multilang-demo
https://github.com/DusanMadar/Flask-multilang-demo
How to Easily Create a Multilingual WordPress Site
https://www.wpbeginner.com/beginners-guide/how-to-easily-create-a-multilingual-wordpress-site/
Internationalized Application URLs
https://flask.palletsprojects.com/en/1.1.x/patterns/urlprocessors/
Multilingual flask application
https://stackoverflow.com/questions/3420897/multilingual-flask-application
Set languages and locales
https://docs.microsoft.com/en-us/windows-hardware/customize/mobile/mcsf/set-languages-and-locales
Read more
Babel Flask Multilanguage
Leave a comment
Comment anonymously or log in to comment.
Comments (1)
Leave a reply
Reply anonymously or log in to reply.
From where 'get_url_lang_code_items_values()' and 'get_url_lang_code_default_item_value()' come from?
Recent
- Hiding database UUID primary keys of your web application
- Don't Repeat Yourself (DRY) with Jinja2
- SQLAlchemy, PostgreSQL, maximum number of rows per user
- Show the values in SQLAlchemy dynamic filters
- Secure data transfer with Public Key encryption and pyNaCl
- rqlite: a high-availability and distributed SQLite alternative
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Using PyInstaller and Cython to create a Python executable
- Connect to a service on a Docker host from a Docker container
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Flask RESTful API request parameter validation with Marshmallow schemas