If you have a webapp built by Flask, at some point you may need to localize the app and add support for more languages. In this post I will go over the basic steps to start supporting new languages using Flask framework.
Table of Contents
Step 1: Install flask-babel
We are going to use flask-babel. This package implements i18n and l10n support for Flask. The installation is simple:
pip install flask-babel
Step 2: Configure the app to use flask-babel
In your webapp, at the file where you instantiate the flask app (this is usually the __init__.py), add the flask-babel import:
from flask_babel import Babel
We also need to provide the app to Babel, so that it can do its job. Add the Babel instantiation after where you create the Flask app, e.g.:
app = Flask(__name__)
babel = Babel(app) #after app is created
We also need to add here is a the localeselector. This will be called on a method that needs localization every time a HTTP request is received. We can also add it to __init__.py.
@babel.localeselector
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'])
From the above we are trying to match the accept language in the HTTP header to a supported language of our app. To specify the supported language, in config.py, we add:
LANGUAGES = ['en', 'zh']
I am using Traditional Chinese as an example in addition to English here, since I happen to know this language.
Step 3: Mark strings to localize
With Flask-babel configured, the next step is to mark all strings we would like to localize in the code. This is a quite tedious job. Generally, what we need to do is:
- Go over all the code written in .py, and replace the to-be-localized strings which are supposed to be generated after receiving a request by _(<the_string>)
For example,
from flask_babel import _
# ...
flash(_("You are now logged in"))
#This replaces flash("You are now logged in")
2. Go over all the code writte in .py, and replace the to-be-localized strings which are supposed to be generated before receiving a request by l_(<the_string>). This is commonly seen in form.py
For example,
from flask_babel import lazy_gettext as _l
class LoginForm(FlaskForm):
username = StringField(_l('Username'))
# ...
3. Go over all the code written in .html, and replace the strings relevant to localization.
For example,
<h1>{{ _('Welcome!') }}</h1>
Step 4: Extract strings for translations
Now that we have marked all strings to localize, we can use pybabel to extract the strings.
Before extracting, we need to add a config file in our project called babel.cfg with the following contents telling pybabel where to find relevant files:
1 [python: app/**.py]
2 [jinja2: app/templates/**.html]
3 extensions=jinja2.ext.autoescape,jinja2.ext.with_
We can then use this command to extract the strings to a messages.pot file:
pybabel extract -F babel.cfg -k _l -o messages.pot .
We then need to create a translation file for each language we want to support. In this case, I want to support zh, so I will initiate a translation with:
pybabel init -i messages.pot -d app/translations -l zh
Once we execute this command, we can find a new file generated under app/translations/zh/messages.po
Step 5: Translation
The messages.po now contains the strings we marked. For example, if I only marked “Username” in form.py, I would have a messages.po which looks like this:
# Chinese translations for PROJECT.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-31 19:14-0800\n"
"PO-Revision-Date: 2021-01-31 16:13-0800\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.9.0\n"
#: app/forms.py:14 app/forms.py:21
msgid "Username"
msgstr ""
All we need to do is to fill the msgstr content with translation of the string in msgid. So in my case after adding the translation it looks like:
# Chinese translations for PROJECT.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-31 19:14-0800\n"
"PO-Revision-Date: 2021-01-31 16:13-0800\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.9.0\n"
#: app/forms.py:14 app/forms.py:21
msgid "Username"
msgstr "使用者名稱"
Step 6: Compile translations
This is an easy step. When all translations are ready, simply compile the translation by
pybabel compile -d app/translations
This will generate messages.mo in the same translations/zh folder and we are now ready to go.
Step 7: Testing
We can now test our code. If you are using Google Chrome, add in the preference->Advanced a your preferred language and make it at the top of the list. Deploy the test and check if you can see your site in the newly supported languages.
Adding new strings
Of course we would not just have one iteration. It is very likely later on we will be adding new pages and need new strings to be added. Since we already have existing translation, we do NOT want to re-initialize the translation file. To update the translation, the typical step is first mark the new strings, and then execute the following:
pybabel extract -F babel.cfg -k _l -o messages.pot .
pybabel update -i messages.pot -d app/translations
The first command will extract the strings again to messages.pot. Then instead of initializing the translation we will update it according to the new messages.pot. This way the previous translation will be preserved and we only need to add the new string’s translation. Of course, after this we will have to compile again to see the results.