Software localization
A Step-by-Step Tutorial on Python Tornado

Tornado is a Python web framework and an asynchronous networking library that relies on non-blocking network I/O to serve web applications. It is the preferred choice for any project that requires a long-lived connection to each user.
One of the main advantages of the Tornado web framework is its built-in internationalization support. This allows developers to build multilingual web applications right away—without going through the hassle of installing other libraries or frameworks for internationalization.
In this step-by-step tutorial, we'll cover everything you need to know about implementing i18n in Tornado. You can find the source code and all project files in our GitHub repo.
Setup
To start off, let's create a new virtual environment—it is a good habit to create a new virtual environment for each project.
Python Modules
Activate the virtual environment and run the following command:
pip install tornado
Files
Language files
For your information, there are two ways to load language files in Tornado:
- CSV
- gettext's locale tree
To keep things simple and short, this tutorial will stay focused CSV files.
Create a new folder called "locale" in your working directory. Then, create the following CSV files inside the "locale" folder:
- en_US.csv
- de_DE.csv
Each translation file should have the following columns.
- string—a key representative of the translation
- translation—the translation text
- plural indicator—an optional column indicating if the translation is singular or plural.
For example, a new translation file should look like this:
home,Home
"home" is the key while "Home" the translation text.
Add the following translations to the en_US.csv file.
home,Home plans,Plans ... promises-description,High-quality product to customers,singular promises-description,High-quality products to customers,plural created-by,Created by %(author)s total-view,%(view)d total views
Repeat the same step for the German translation using the following data:
home,Zuhause plans,Pläne ... promises-description,Hochwertige Produkt für Kunden,singular promises-description,Hochwertige Produkte für Kunden,plural created-by,Erstellt von %(author)s total-view,%(view)d Gesamtansichten
HTML file
The landing page for this tutorial is based on a strip-down version of W3CSS marketing templates. You can find the complete code at the following link. It will look like this when rendered:
Create a new folder called templates and save the entire HTML file as index.html inside it.
Pluralization
Inside the HTML file, string translation is done via a global function with the following syntax:
_("key")
For example, to properly translate...
home,Home
...you should use the following syntax inside your HTML file:
<a href="...">{{ _("home") }}</a>
In addition, the global function also has another form that accepts three input parameters:
- First translation text
- Second translation text
- An integer that represents the plural determiner
If the third argument is 1, it will return the first translation text. Otherwise, it will return the second translation text. The following code illustrates the code for pluralization in the HTML file, where "num" is an integer variable.
<p>{{ _("person liked this", "people liked this", num) }}</p>
The example given above only works for a single language. If you intend to support multiple languages, you should pass in a global function as an argument instead of a plain string.
<p>{{ _(_("liked-this"), _("liked-this"), num) }}</p>
"liked-this" represents the key for the translation based on the following translation data:
liked-this,person liked this,singular liked-this,people liked this,plural
Python-style named placeholder
Besides, you can insert a Python-style named placeholder inside any translation. It follows the syntax below:
%(name)s
"name" represents the placeholder's name while "s" is the string data type. If you have an integer, you should use the following instead:
%(count)d
Have a look at the following example that showcases a translation file with placeholders:
created-by,Created by %(author)s total-view,%(view)d total views
Previously, the following translations are defined inside the en_US.csv file.
created-by,Created by %(author)s total-view,%(view)d total views
In order to use it inside HTML, you should code it as follows:
<p>{{ _("created-by") % {"author": author} }} </p> <p>{{ _("total-view") % {"view": view} }} </p>
"author" and "view" are variables that will be passed directly from the main Python file later on.
Tornado i18n support
Let's explore a few useful built-in functions that can be called programmatically inside any Python file.
Loading translation files
The basics of internationalization are above all mirrored in loading all the translation files dynamically. You can do so via the "load_translation" function.
tornado.locale.load_translations('locale/')
It accepts two input parameters:
- directory—a string that represents a directory with all the translation files in CSV format.
- encoding—an optional parameter for encoding; if encoding is not present, it will default to UTF-8 instead unless the files contain byte-order market (BOM).
Please note that the "locale" directory is not a convention used by Tornado. You can name it anything that you preferred. Just make sure to provide the correct path when calling the "load_translations" function.
Get supported locales
Supported locales are determined from the directory loaded by the "load_translations" function. You can check all the supported locales via the following function call:
tornado.locale.get_supported_locales()
It will return a frozenset. Each element represents a locale and is based on the name of the translation files.
frozenset({"de_DE", "en_US"})
Set default locale
Setting the default fallback locale is as simple as running the following function:
tornado.locale.set_default_locale('de_DE')
By default, it will use "en_US" if you have not specified the default locale.
Locale object and translate function
Once you have loaded the translation files, you can get a locale object and obtain the corresponding translation as follows:
user_locale = tornado.locale.get("de_DE") text = user_locale.translate("home") # returns Zuhause
Optionally, the "translate" function also accepts "plural message" and "count".
text = user_locale.translate("promises-description", "promises-description", 3) # returns Hochwertige Produkte für Kunden
Tornado Web Server
Once you are done with the basics of i18n, create a new Python file called "myapp.py".
Importing modules
Add the following import declaration at the top of your "myapp.py" file.
import tornado.ioloop import tornado.web
Handlers
Next, add the following code below it:
class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") class LocaleHandler(tornado.web.RequestHandler): def get(self, locale): self.locale = tornado.locale.get(locale) self.render("index.html", product=1, author='Wai Foong', view=1234)
Both classes serve as route handlers. The first class returns "Hello, world" as a text response. By default, "tornado.web.RequestHandler" will capture the user's locale based on the "Accept-Language" header sent by the user's browser. You can obtain it via as follows:
class MainHandler(tornado.web.RequestHandler): def get(self): user_locale = self.get_user_locale() # user_locale is None if there is no Accept_Language header self.write("Hello, world")
On the other hand, the second class returns the rendered HTML page of "index.html". Unlike the first handler, the second one accepts another argument that represents the locale obtained via a RESTful API. Then, it will use the input locale and attempt to set the current locale via the "get" function.
The function also returns three additional variables:
- product—an integer for showcasing pluralization; feel free to modify and re-run the server later on to see the effect.
- author—it acts as a variable for the string placeholder
- view—it acts as the variable for the integer placeholder
Localized routes
Implement the following code that serves as application context for our "myapp.py" file.
def make_app(): return tornado.web.Application([ (r"/", MainHandler), (r"/([^/]+)/about-us", LocaleHandler), ], template_path='templates/')
The following regex is used:
([^/]+)
When serving the "about-us" route, this regex will capture any string and map it as the first argument to "LocaleHandler".
r"/([^/]+)/about-us" # capture en_US as locale for http://<ip>:<port>/en_US/about-us r/"about-us/([^/]+)" # capture en_US as locale for http://<ip>:<port>/about-us/en_US
Main function
Finally, add the following main function to the "myapp.py" file.
if __name__ == "__main__": tornado.locale.load_translations('locale/') app = make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()
It will load translations from the "locale" folder and serve the app at port 8888.
🗒 Note » You can find the complete code for "myapp.py" via the following link.
Test
Run the following command in the terminal to start the Tornado server.
python myapp.py
English (en_US)
Open up a browser and head over to the following URL:
http://localhost:8888/en_US/about-us
You should see the following web interface:
The footer looks something like this:
Feel free to change the product variable to a number larger than one in the following line of code:
# change product to 3 self.render("index.html", product=1, author='Wai Foong', view=1234)
Re-run the server, and you should notice that the translation will be "3 High-quality products to customers".
German (de_DE)
Next, let's test out German translation by changing the URL as follows:
http://localhost:8888/de_DE/about-us
The web interface should be as follows:
And we're done!
The Tornado web framework is a powerful yet lightweight library to serve web applications in Python. Its built-in internationalization support is a big plus for multilingual software projects.
If you want to learn even more about Python i18n, make sure you check out the following guides as well:
Finally, if you want to improve your i18n process, consider signing up for Phrase, the most reliable software localization platform on the market. It comes with a 14-day free trial.
Phrase will equip you with everything you need to:
- Build production-ready integrations with your development workflow,
- Invite as many users as you wish to collaborate on your projects,
- Edit and convert localization files online with more context for higher translation quality.