Web2py I18n Step by Step

Developing a web app for global markets in web2py? Follow this step-by-step tutorial to implement multilingual support with web2py's built-in internationalization and pluralization engine.

One of the pioneer web frameworks in Python, web2py, lets you build fast, scalable, and secure database-driven, server-side web applications. It provides everything you need in a single package, without any third-party dependencies.

When it comes to internationalization, web2py stands out from other Python web frameworks. In this step-by-step web2py I18n tutorial, we will have a detailed look at the internationalization and pluralization aspects of building web2py web apps.

Directory structure

By default, the main web2py project contains the directory structure specified below. Each folder inside the applications folder represents an app—each one of them has its own endpoints defined by its own models, views, and controllers.

web2py/
    README
    LICENSE
    VERSION                    > this web2py version
    web2py.py                  > the startup script
    anyserver.py               > to run with third party servers
    ...                        > other handlers and example files
    gluon/                     > the core libraries
        packages/              > web2py submodules
          dal/
        contrib/               > third party libraries
        tests/                 > unit tests
    applications/              > user-facing apps
        admin/                 > web based IDE
            ...
        examples/              > examples, docs, links
            ...
        welcome/               > the scaffolding app (welcome)
            ...
            models/
            views/
            controllers/       > controller for implementing 
                               > RESTful routes
            languages/         > language files
            sessions/
            tests/
            ...
        ...                    > other apps

The focus here is mainly on the files in the following folders:

  • applications/<app>/controllers/*.py
  • applications/<app>/languages/*.py

Each app has its own language files to be found in the languages folder. The recommended naming convention is to use either en.py or en-us.py. Each language file should contain a single Python dictionary with key-value pairs as its core elements. For example:

# -*- coding: utf-8 -*-
{
...
'Live Chat': 'Live Chat',
'Log In': 'Log In',
'Logged in': 'Logged in',
'Logged out': 'Logged out',
'Login': 'Login',
'Hello World': 'Hello World!',
...
'Key': 'Value'
}

Basic translations

Object T represents the language translator global instance. You should mark all of the string constants that required translation with object T.

msg = T("Hello World")

Interpolating variables

web2py support variable interpolation with different syntaxes:

msg = T("welcome to %s", ('Phrase', ))
msg = T("welcome to %(name)s", dict(name='Phrase'))
msg = T("welcome to %s") % ('Phrase', )

# The following syntax is highly recommended as it makes
# translation a lot easier. The string is translated based
# on the requested language and the 'name' variable is
# replaced independently of the language.
msg = T("welcome to %(name)s") % dict(name='Phrase')  

#For all of the above > msg == "welcome to Phrase"

Comments

It is possible to add comments to the original string for context-based translation by adding ## <comment>. Comments will not be rendered and are merely used as a reference to find the most appropriate translation.

T("Hello ## first occurrence")
T("Hello ## second occurrence")

# Hello

Automatic key writing to language files

Apart from string translation, web2py also supports translation via values stored in variables:

key = 'Hello World'
msg = T(key)

In this case, it will translate the key that contains “Hello World”. If the “Hello World” key is not found and the system is writable, web2py will automatically add this key to the list of translatables in the respective language file. This only happens if you are using variables as keys. If you are using string literals and the key is not found, web2py will simply display the key as the translated text.

However, this might result in lots of file IO calls. To disable this feature and prevent web2py from updating the language files dynamically, make sure you add the following code:

T.is_writable = False

Pluralization

The built-in pluralization system is extremely powerful as it supports about 28 languages in the latest version. It consists of 3 main parts:

  • Plural rules
  • Dictionary with word plural forms
  • Placeholder string

Plural rules

You can find all the plural rules in the following directory:

./gluon/contrib/plural_rules/*.py

It is recommended to use the built-in pluralization functionality, but you can always customize it based on your preferences.

For example, the Slovenian language has 4 plural forms. You can define the rules as follows:

nplurals=4  # 4 plural forms

get_plural_id = lambda n: (0 if n % 100 == 1 else
                           1 if n % 100 == 2 else
                           2 if n % 100 in (3,4) else
                           3)

Plural dictionaries

You need to define your own pluralization dictionary file if you want to use the built-in pluralization engine for a certain language. One dictionary file is required per language. You should place the file under the languages folder and prefix the name with the plural- string. For example, plural-en.py can be used for English pluralization.

# -*- coding: utf-8 -*-
{
'account': ['accounts'],
'book': ['books'],
'is': ['are'],
'email': ['emails'],
...
}

Using plurals

In order to trigger pluralization, you need to include an extra symbols argument—a count parameter that will determine the plural form to be used.

msg = T("You have %s %%{email}", symbols=3)  # explicit
msg = T("You have %s %%{email}", 3)          # shorthand

# In both cases > msg == "You have 3 emails"

Placeholder strings

The syntax for a placeholder string should ideally be as follows:

%%{[<modifier>]<word>[<parameter>]}

The modifier can be one of the following:

  • !… Capitalize the text
  • !!… Use title case for every word inside the string
  • !!!… Convert every character into uppercase
%%{!hello world}   ## Hello world
%%{!!hello world}  ## Hello World
%%{!!!hello world} ## HELLO WORLD

In fact, you can pass either a tuple or a dictionary as a symbols argument. If you go for a tuple, you should use the %%{word[index]} syntax.

# defaults to %%email[0] and use value from var 1
msg = T("You have %s %%{email}", (var1, var2))

# use value from var2 instead
msg = T("You have %s %%{email[1]}", (var1, var2))

When using a dictionary as the symbols argument, the syntax is %%{word(dict_key)}.

# dictionary as symbols argument
msg = T("You have %s %%{email(var2)}", dict(var1=1, var2=3))

API

Get the currently language

The current language is based on the “Accept-Language” field within the HTTP header. You can easily obtain it by calling:

T.current_languages

# returns a list of string e.g. ['en']

Set the default language

There might be a situation in which the HTTP header comes with two languages. This can create unwanted effects during the process of internationalizing your web application. You can easily resolve such conflict by setting the default language strings beforehand.

T.set_current_languages('it')

Set the translation language

You can programmatically set the desired language via the force function.

T.force('it')

Turning off translations is as simple as passing None as an argument to the function.

T.force(None)

Alternatively, you can set it individually for each translation string as follows:

T("Hello World", language="it")

RESTful URL

Let us explore a little further how to create a RESTful URL to serve your internationalized application.

Modifying the endpoint

Open the welcome/controllers/default.py file, and you should see the following code snippet at the top of it:

def index():
    response.flash = T("Hello World")
    return dict(message=T('Welcome to web2py!'))

...

Assuming that web2py is run on port 8000, the endpoint can be accessed via the following URL:

http://127.0.0.1:8000/welcome/default/index

Here is how the user interface will look like:

Untranslated demo app | Phrase

Request object

Each endpoint contains its own Request object that contains a few important items or fields related to the incoming HTTP request. You can use the args field to get the input language based on the RESTful URL.

def index():
    args = request.args
    ...

A page accessed via…

http://127.0.0.1:8000/welcome/default/index/it

…will result in the following value inside of the request.args file:

['it']

Set a language RESTfully

You can simply process the incoming language argument before setting it via the force function for creating i18n RESTful routes.

def index():
    args = request.args
    if len(args) >= 1:
        T.force(args[0])
    ...

Web2py will render the following user interface when you access the same URL again:

Translated demo app | Phrase

URL rewriting

As you may have noticed, the locale is at the end of the RESTful URL:

http://127.0.0.1:8000/welcome/default/index/it

If you wanted to change the location of the locale argument, you could do so by modifying the routing functionality. Create a new file called “routes.py” at the root directory and add the following code to it:

routers = dict(
    # base router
    BASE=dict(
        default_application='welcome',
    ),
)

Next, define another routes.py file inside your application and add the following code snippet. Feel free to change the default_language or list of supported languages.

routers = {
    app: dict(
        default_language='en',
        languages=['en', 'it'],
    )
}

Web2py will process the incoming request URL and set the language to the uri_language variable. Let us add the final touch by modifying the index function of default.py. Your index() function should look like the following:

def index():
    # set language based on incoming request URL
    if request.uri_language:
        T.force(request.uri_language)
    ...

Restart your server and access the following URL

http://127.0.0.1:8000/welcome/it/default/index

Adding Babel

Since the underlying code for web2py is in Python, you can easily extend it to include other third-party modules such as Babel.

Babel is a Python package that is integrated with a collection of utilities for internationalization and localization. It is extremely useful as it provides great support for datetime formatting as well as number formatting.

Install it via the following command:

pip install babel

Formatting dates

The easiest method for formatting dates is to use the built-in format_date function:

import datetime
from babel.dates import format_date

format_date(datetime.datetime.now(), format='full', locale='it'])

# lunedì 8 marzo 2021

Formatting decimals

The use of the decimal separator differs from language and language, so you can let the format_decimal function do the trick for you:

from babel.numbers import format_decimal

format_decimal(-1.2345, locale='it')

# -1,234

Concluding our Web2py I18n Tutorial

By now, you should be able to internationalize your own web application using all built-in tools inside web2y. If you want to explore further aspects of localization in Python, feel free to have a look at the following tutorials for Python developers:

As soon as you have your app ready for localization, consider streamlining your workflow with Phrase, the most reliable software localization platform that 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.
Comments