Software localization
Web2py I18n Step by Step
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:
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:
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:
- A Beginner’s Guide to Python’s locale Module
- All I18n Advantages of Babel for Your Python App
- How to Translate Python Apps with the GNU gettext Module
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.