Software localization

A Step-by-Step Tutorial on Python Tornado

Tornado is a Python framework and an asynchronous networking library that serves web applications. Learn its ins and outs in this Tornado tutorial.
Software localization blog category featured image | Phrase

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.


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


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" is the key while "Home" the translation text.

Add the following translations to the en_US.csv file.




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:




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:

demo app landing page | Phrase

Create a new folder called templates and save the entire HTML file as index.html inside it.


Inside the HTML file, string translation is done via a global function with the following syntax:


For example, to properly translate...

home,Home 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" represents the placeholder's name while "s" is the string data type. If you have an integer, you should use the following instead:


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.


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:


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:


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 "".

Importing modules

Add the following import declaration at the top of your "" file.

import tornado.ioloop

import tornado.web


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 "" 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".


# capture en_US as locale for http://<ip>:<port>/en_US/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 "" file.

if __name__ == "__main__":


    app = make_app()



It will load translations from the "locale" folder and serve the app at port 8888.

🗒 Note » You can find the complete code for "" via the following link.


Run the following command in the terminal to start the Tornado server.


English (en_US)

Open up a browser and head over to the following URL:


You should see the following web interface:

English interface | Phrase

The footer looks something like this:

English footer | Phrase

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:


The web interface should be as follows:

German interface | Phrase

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.