I’ve built a website with Flask and Python and it’s only published in English. As the website will be mainly visited by people in China and other countries, I want to make the website a multilingual one, at least bilingual: Chinese and English.
So I started to build the multilingual website with Flask-Babel. Here are the steps.
Existing project structure
├── venv
│ ├── bin
│ └── include
├── webapp
│ ├── __init__.py
│ ├── config.py
│ ├── routes.py
│ ├── static
│ │ ├── favicon.ico
│ │ └── styles.css
│ └── templates
│ ├── about.html
│ ├── layout.html
│ └── index.html
├── app.py
└── requirements.txt
Install Flask-Babel
The flask extension we need to install is Flask-Babel.
(venv) $ pip install Flask-Babel
Flask-Babel is easy to use, just like others, we need to add the following lines in the __init__.py
file to initialize it:
# ...
from flask_babel import Babel
app = Flask(__name__)
# ...
babel = Babel(app)
As we are going to translate the website into Chinese, let us add a configuration variable in the config.py
file:
class Config(object):
# ...
LANGUAGES = ['en_US', 'zh_CN']
en_US
and zh_CN
are the language codes of English and Chinese respectively, with the _US
and _CN
suffixes to indicate the country code. We can use two-letter codes like en
and zh
as well.
The Babel
instance has a localeselector
function that is called when a request is made to the website and will return the language code of the user’s browser. The accept_languages
object works with the Accept-Language
header in the request that specifies the client language and locale perferences. best_match
will return the best matches from the list of available languages.
In the __init__.py
:
from flask import request
# ...
@babel.localeselector
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'])
Marking Texts need to be translated
Text in the Python Source Code
As we have literal strings in the Python source code, like in the flash()
statement, we need to mark them as translatable by wrapping them in the _()
function. The _()
function is a shortcut for gettext()
.
# ...
flash('Hello, world!')
# ...
Change the flash()
statement to:
from flask_babel import _ # from flask_babel import gettext as _
# ...
flash(_('Hello, world!'))
📝 Note: Don’t to forget to import the
_()
function fromflask_babel
. The_()
function is a shortcut forgettext()
.
Texts in the Templates
For the text in the templates, the _()
function is also available so the text can be marked as translatable in the similar way. For example we have a template about.html
:
<h1>About Us</h1>
Change it to:
<h1>{{ _('About Us') }}</h1>
📝 Note:
{{ ... }}
should be added to enforce the_()
function to be called instead of being treated as a literal string.
Extracting Texts
Once all the texts are marked as translatable with _()
, we can translate them. As mentioned in the tutorial of Flask-Babel, the way is to use the pybabel
to extract the texts from the source code and put them in the messages.pot
file.
First of all, we need to have some config, create babel.cfg
file next to app.py
:
[python: webapp/**.py]
[jinja2: webapp/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
The first two lines should indicate the source files of source code and template html files respectively. The extensions
line indicates two extensions provided by the Jinja2 template engine that help Flask-Babel properly parse template files.
Then extract the texts from the source code and put them in the messages.pot
file:
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
extracting messages from webapp/__init__.py (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
extracting messages from webapp/config.py (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
extracting messages from webapp/routes.py (extensions="jinja2.ext.autoescape,jinja2.ext.with_")
writing PO template file to messages.pot
📝 Note: The
-o
option indicates the output file. The-k
option indicates the keyword to be used to mark the texts. If we uselazy_gettext()
(imported as_l()
) to mark the texts, the-k
option should be_l
. The_()
function is by default.
Creating a language translation
Now let’s create the language translation for the languages we want to translate to. For this time, we want to translate the website into Chinese:
(venv) $ pybabel init -i messages.pot -d webapp/translations -l zh
creating catalog webapp/translations/zh/LC_MESSAGES/messages.po based on messages.pot
📝 Note: The
-d
option indicates the directory where the translations are stored.
If we want to translate to other languages, just repeate the above steps seperately.
Following is the messages.po that are created:
# Chinese translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-04-29 14:30-0700\n"
"PO-Revision-Date: 2022-04-29 14:32-0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
"Language-Team: zh <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.10.1\n"
#: webapp/routes.py:29
msgid "Hello, world!"
msgstr ""
📝 Note: After the header, the first line begins with
msgid
is the original text extracted by_()
or_l()
, the second line begins withmsgstr
is the translation.
We can translate the texts in the messages.po
file to the languages we want to translate to. We can do it manually or using the translation editor like poedit.
Compile the translations
When our translation work is done, we need to compile the translation for use:
(venv) $ pybabel compile -d webapp/translations
compiling catalog webapp/translations/zh/LC_MESSAGES/messages.po to webapp/translations/zh/LC_MESSAGES/messages.mo
This action will create the messages.mo
file in the webapp/translations/zh/LC_MESSAGES
directory.
Now it’s time to test our translation work. One way is to adjust the language preference in the browser. For example, if we want to translate the website into Chinese, we can set the language preference to Simplified Chinese (zh-CN) in the browser’s language settings.
The other way is to force the language by making the localeselector
function return the language we want to translate to. In the __init__.py
file, make a change:
@babel.localeselector
def get_locale():
# return request.accept_languages.best_match(app.config['LANGUAGES'])
return 'zh_CN'
Restart the server and we will see the website in Chinese.
Updating the translations
If the website has changed or we want to update the translations, we can use the pybabel
to update:
First, we need to re-extract the messages.pot
file and then use the pybabel update
to update the translations. This update process is kind of intelligent merge, only the changes will be updated.
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
(venv) $ pybabel update -i messages.pot -d webapp/translations
Now we can exam the messages.po
file in the webapp/translations/zh/LC_MESSAGES
directory to edit the translations. After we finish the editing, we can compile the translations:
(venv) $ pybabel compile -d webapp/translations
Let the users decide the language
The flask app has user login feature so we decided to let the users can decide the language they want to use after they logged in, if they don’t like the language that the browser locale chosed for them.
The language that the user selected will be stored in the session
dictionary. So we will first check if the session['language']
is set. If it is, I will use it. Otherwise, we can use the browser locale.
So in the __init__.py
file, make a change:
@babel.localeselector
def get_locale():
try:
language = session['language']
except KeyError:
language = None
if language is not None:
return language
else:
return request.accept_languages.best_match(app.config['LANGUAGES'])
Then in the routes.py
file, add a new route:
@app.route("/language/<language>")
def set_language(language):
session["language"] = language
return redirect(request.referrer or url_for("index"))
📝 Note: Very straight forward, get the language from the URL and set it to the
session
. Then redirect to the page that the user came from or to theindex
page if the user didn’t come from any page. Please note that if the user logged out, thesession
will be cleared and the language will be set to the browser locale next time the user visit the website.