Internationalization of applications¶
The Nagare framework includes a nagare.i18n
module that can help you
to internationalize your Web application.
There are many aspects to take into account when internationalizing an application:
- extracting the messages (strings) used inside the application and translating them in each language
- formatting numbers, dates, times, currencies for display or parsing them after user input
- dealing with timezone calculations
- displaying the requested pages in the proper language according to the client browser language settings or a setting in the application
All these aspects are addressed by the nagare.i18n
module.
Setup an application for internationalization¶
First, the Nagare i18n
extras should be installed in your virtual environment:
$ easy_install "nagare[i18n]"
This command installs Babel
and pytz
which are necessary to use the nagare.i18n
module.
However, if you plan to use the internationalization feature of Nagare in your Web application, you’d
better change the setup.py
file that is generated when you create the application, in order to specify
that you need the i18n
extra as a requirement:
setup(
...
install_requires = ('nagare[i18n]', ...),
...
)
Then, Nagare will be installed with the i18n
extra when you install your application.
When you create a new application with nagare-admin create-app
, Nagare automatically creates a
setup.cfg
file that contains a default configuration for internationalization:
[extract_messages]
keywords = _ , _N:1,2 , _L , _LN:1,2 , gettext , ugettext , ngettext:1,2 , ungettext:1,2 , lazy_gettext , lazy_ugettext , lazy_ngettext:1,2 , lazy_ungettext:1,2
output_file = data/locale/messages.pot
[init_catalog]
input_file = data/locale/messages.pot
output_dir = data/locale
domain = messages
[update_catalog]
input_file = data/locale/messages.pot
output_dir = data/locale
domain = messages
[compile_catalog]
directory = data/locale
domain = messages
This configuration file is used by the distutils commands that are automatically registered to setuptools
when Babel
is installed. Here is the list of the commands:
Command | Effect |
---|---|
python setup.py extract_messages |
Extract the messages from the python sources to a .pot file |
python setup.py init_catalog -l <lang> |
Create a new message catalog (.po file) for a given
language, initialized from the .pot file |
python setup.py update_catalog [-l <lang> ] |
Update the message catalog(s) with the new messages found in the
.pot file after an extract_messages |
python setup.py compile_catalog [-l <lang> ] |
Compile the message catalog(s) in binary form so that they can be used inside the application |
See the Babel setup documentation for a complete reference of the commands options.
The catalog files are stored in the data/locale
directory by default.
Marking the messages to translate¶
The nagare.i18n
module provides functions to translate the strings that appear in your code. The most
useful function is nagare.i18n._
which returns the translation of the given string as a unicode string:
from nagare import presentation
from nagare.i18n import _
class I18nExample(object):
pass
@presentation.render_for(I18nExample)
def render(self, h, *args):
h << _('This is a translated string') << h.br
h << 'This string is not translated'
return h.root
app = I18nExample
There are also many more functions that deals with strings in the nagare.i18n
module:
Function | Effect |
---|---|
_ |
Shortcut to ugettext |
_N |
Shortcut to ungettext |
_L |
Shortcut to lazy_ugettext |
_LN |
Shortcut to lazy_ungettext |
gettext |
Return the localized translation of a message as a 8-bit string encoded with the catalog’s charset encoding, based on the current locale set by Nagare |
ugettext |
Return the localized translation of a message as a unicode string, based on the current locale set by Nagare |
ngettext |
Like gettext but consider plurals forms |
nugettext |
Like ugettext but consider plurals forms |
lazy_gettext |
Like gettext but with lazy evaluation |
lazy_ugettext |
Like ugettext but with lazy evaluation |
lazy_ngettext |
Like ngettext but with lazy evaluation |
lazy_nugettext |
Like nugettext but with lazy evaluation |
Note that the lazy_*
variants don’t fetch the translation when they are evaluated. Instead Nagare automatically
takes care of fetching the translation at the rendering time when it encounters a lazy translation. This is especially
useful when you want to use translated strings in module or class constants, which are evaluated at definition
time and consequently don’t have access to the current locale which, as you will see later, is scoped to the request.
Here is how you would define a class constant for use in a view:
from nagare import presentation
from nagare.i18n import _, _L
class I18nExample(object):
# the following line is evaluated when the module is loaded, so _ would not work here
TITLE = _L('Internationalization Example')
@presentation.render_for(I18nExample)
def render(self, h, *args):
with h.h1:
h << self.TITLE
h << _('This is a translated string') << h.br
h << 'This string is not translated'
return h.root
...
Extracting the messages¶
Babel
automatically takes care of the messages extraction from the sources when you run the
python setup.py extract_messages
command. Specifically, when it extracts the localizable strings from the sources
files, it only considers the strings enclosed in specific keywords calls. The list of the supported keywords appear in
the setup.cfg
file:
[extract_messages]
keywords = _ , _N:1,2 , _L , _LN:1,2 , gettext , ugettext , ngettext:1,2 , ungettext:1,2 , lazy_gettext , lazy_ugettext , lazy_ngettext:1,2 , lazy_ungettext:1,2
All the nagare.i18n
functions that deal with strings appear in this list, so you don’t have to change this line
in most cases. However, if you need to add additional translation functions, don’t forget to update the keywords
list in this file.
Furthermore, the setup.py
file created by nagare-admin create-app
contains a line that configures the messages
extractors that Babel
will use in order to extract the messages from your source files:
setup(
...
message_extractors = { 'i18n_example' : [('**.py', 'python', None)] },
...
)
By default, Nagare set up an extractor for the python files. If you have other files that contain localizable
strings (such as .js
files), you’ll have to add additional message extractors by yourself. Please refer the
Babel messages extractor documentation to understand how to add additional message extractors.
If you run the python setup.py extract_messages
command on the previous code, you should obtain a messages.pot
file that looks like this one:
# Translations template for nagare.examples.
# Copyright (C) 2012 ORGANIZATION
# This file is distributed under the same license as the nagare.examples
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: nagare.examples 0.3.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2012-03-05 11:05+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n"
#: nagare/examples/i18n.py:18
msgid "Internationalization Example"
msgstr ""
#: nagare/examples/i18n.py:25
msgid "This is a translated string"
msgstr ""
All the localizable strings of your application should appear in this file after extraction. Note that the string
This string is not translated
of the previous example was not included because it was not enclosed in a call
to a translation keyword.
Translating the messages¶
Once the .pot
file is produced (after message extraction), you have to create a message catalog for each language
with the help of the init_catalog
catalog, for example:
$ python setup.py init_catalog -l fr
$ python setup.py init_catalog -l en
Then update the .po
files that have just been created and provide the translations for all the strings that
do not have one. You can use either a dedicated .po
file editor, or a simple text editor and fill the
msgstr
lines for each msgid
. Please refer to the PO Files section of the Gettext Manual for more
information about this process.
Finally, compile the catalogs in binary form:
$ python setup.py compile_catalog
If you change the source files and add new messages to translate, you have to update the message catalogs:
$ python setup.py extract_messages
$ python setup.py update_catalog
Then, fill in the missing translations in the .po
files and compile the catalogs again.
$ python setup.py compile_catalog
Formatting numbers, dates, times, currencies and dealing with timezones¶
The nagare.i18n
module also defines functions for formatting numbers, dates, times, currencies and for timezones
calculations, according to the locale of the user. We have grouped these functions by category. See the Nagare
nagare.i18n
API documentation for more information on these functions.
Numbers¶
Function | Effect |
---|---|
get_decimal_symbol |
Return the symbol used to separate decimal fractions |
get_plus_sign_symbol |
Return the plus sign symbol |
get_minus_sign_symbol |
Return the minus sign symbol |
get_exponential_symbol |
Return the symbol used to separate mantissa and exponent |
get_group_symbol |
Return the symbol used to separate groups of thousands |
format_number |
Return the given integer number formatted |
format_decimal |
Return the given decimal number formatted |
format_percent |
Return the formatted percentage value |
format_scientific |
Return the value formatted in scientific notation, e.g. 1.23E06 |
parse_number |
Parse the localized number string into a long integer |
parse_decimal |
Parse the localized decimal string into a float |
Currencies¶
Function | Effect |
---|---|
get_currency_name |
Return the name used for the specified currency |
get_currency_symbol |
Return the symbol used for the specified currency |
format_currency |
Return the formatted currency value |
Dates & timezones¶
Function | Effect |
---|---|
get_period_names |
Return the names for day periods (AM/PM) |
get_day_names |
Return the day names in the specified format |
get_month_names |
Return the month names in the specified format |
get_quarter_names |
Return the quarter names in the specified format |
get_era_names |
Return the era names used in the specified format |
get_date_format |
Return the date formatting pattern in the specified format |
get_datetime_format |
Return the datetime formatting pattern in the specified format |
get_time_format |
Return the time formatting pattern in the specified format |
get_timezone_gmt |
Return the timezone associated with the given datetime object, formatted as a string indicating the offset from GMT |
get_timezone_location |
Return a representation of the given timezone using the “location format”, i.e. the human-readable name of the timezone |
get_timezone_name |
Return the localized display name for the given timezone |
format_time |
Return a time formatted according to the given pattern |
format_date |
Return a date formatted according to the given pattern |
format_datetime |
Return a datetime formatted according to the given pattern |
parse_time |
Parse a time from a string |
parse_date |
Parse a date from a string |
to_timezone |
Return a localized datetime object |
to_utc |
Return a UTC datetime object |
Setting the locale of the application¶
The last step of the internationalization process is to initialize the locale of the application according to a language setting. There are many ways to implement a language setting:
- use a per-application setting, for example provide the language setting in the application configuration
- use the client browser language settings, i.e. deal with the
Accept-Language
headers from the HTTP request - use a URL prefix, for example
/en
for english and/fr
for french (see Significative “RESTful” URLs to learn how to set up URLs) - use a per-user setting: for example, read the locale setting from a user profile stored in a database
- use a per-session setting, for example store the language setting as an attribute of a component file
You may even want to combine two or more of these schemes in your application! As you will see next, Nagare makes it easy to change the locale at runtime, it doesn’t really matter where the language setting comes from.
The locale can be changed with either the nagare.wsgi.WSGIApp.set_default_locale
method or the nagare.wsgi.WSGIApp.set_locale
method.
Example 1: application-wide locale¶
Use the set_default_locale()
if the locale if only set once for the application:
from nagare import wsgi, 18n
class I18nExampleApp(wsgi.WSGIApp):
def __init__(self, root_factory):
super(I18nExample, self).__init__(root_factory)
self.set_default_locale(i18n.Locale('fr')) # Hard coded locale
or:
from nagare import wsgi, 18n
class I18nExampleApp(wsgi.WSGIApp):
def set_config(self, config_filename, config, error):
super(I18nExample, self).set_config(config_filename, config, error)
language = ... # Read the langage from the ``config`` configuration
self.set_default_locale(i18n.Locale(language))
app = I18nExampleApp(lambda: component.Component(I18nExample()))
Example 2: using a negociated browser locale¶
Use the set_locale()
method if the locale can change between requests.
The best place where to change the locale is in the nagare.wsgi.WSGIApp.start_request
method
since we have access to both the request and to the authenticated user (when necessary)
there. Furthermore, since a user may change his language setting between two requests, the locale setting is scoped
to the HTTP request by design in Nagare.
As an illustration, let’s use a negociated browser locale:
from nagare import presentation, component, wsgi, i18n
...
class I18nExampleApp(wsgi.WSGIApp):
AVAILABLE_LANGUAGES = (
('en',), # English
('fr',), # French
)
def start_request(self, root, request, response):
super(I18nExampleApp, self).start_request(root, request, response)
# Set the default locale to a browser negotiated locale
locale = i18n.NegotiatedLocale(
request,
self.AVAILABLE_LANGUAGES,
default_locale=self.AVAILABLE_LANGUAGES[0]
)
self.set_locale(locale)
app = I18nExampleApp(lambda: component.Component(I18nExample()))
We first create an application class that inherit from wsgi.WSGIApp
so that we can override the default
start_request
behavior in order to install a negociated browser locale.
Since we must pass the list of the available locales to the i18n.NegotiatedLocale
constructor, we define a
AVAILABLE_LANGUAGES
class constant. The available locales should be given as a list of (language, territory)
tuples. The territory has been omitted here since we don’t use territory variants of the languages yet.
The nagare.i18n.NegotiatedLocale
constructor accepts
the request as first parameter, the list of the available locales as second parameter and some optional parameters.
It examines the Accept-Language
headers of the request in order to determine the best locale to use for the
current request according to the available locales. The default_locale
optional parameter can be used to define
the default (language, territory)
value when the negociation fails.
Example 3: using a per-session locale setting¶
Let’s show another example: we will allow the user to change the language setting using links and store the setting in the session in order to persist the change for the next requests. Here is the code:
# Encoding: utf-8
from nagare import presentation, component, wsgi, i18n, var
from nagare.i18n import _, _L
class I18nExample(object):
# The following line is evaluated when the module is loaded, so _ would not work here
TITLE = _L('Internationalization Example')
AVAILABLE_LANGUAGES = (
('en', u'English'), # English
('fr', u'Français'), # French
)
def __init__(self):
self.language = self.AVAILABLE_LANGUAGES[0][0]
def change_language(self, language):
self.language = language
# We will use the same directory than the current locale to
# locate the translation files
dirname = i18n.get_locale().get_translation_directory()
# Immediatly change the locale of the current request
i18n.set_locale(i18n.Locale(language, dirname=dirname))
@presentation.render_for(I18nExample)
def render(self, h, comp, *args):
with h.h1:
h << self.TITLE
h << _('This is a translated string') << h.br
h << 'This string is not translated'
h << h.hr
# Language change
for language, label in self.AVAILABLE_LANGUAGES:
h << h.a(label, title=label).action(self.change_language, language)
h << ' '
return h.root
class I18nExampleApp(wsgi.WSGIApp):
def start_request(self, root, request, response):
super(I18nExampleApp, self).start_request(root, request, response)
# Install the locale from the language stored in the root object
language = root().language
self.set_locale(i18n.Locale(language))
app = I18nExampleApp(lambda: component.Component(I18nExample()))
We moved the AVAILABLE_LANGUAGES
list to the I18nExample
component since we’ll show a list of
language change links in its view.
In the associated view, we iterate the AVAILABLE_LANGUAGES
list in order to display a link for each language.
The links actions call the set_language
method, passing the associated language code to it.
The I18nExample.set_language
method performs two operations. First, it remembers the language setting as an
attribute. Since the component will be pickled into the session after the request, the language
setting is effectively stored in the session and will be back in the next request when the session is unpickled.
The language setting is then used in the I18nExampleApp.start_request
method for reinstalling the locale for
subsequent requests.
Second, it changes the locale in the current request so that the
rendering of the component is immediately affected by the change. However, we can’t call
I18nExampleApp.set_locale
here, because we don’t have access to the application instance from the component.
Instead, we have to install the locale ourselves by calling the i18n.set_locale
directly.
These three examples explain the basics for setting the locale in a Nagare application. Real-world implementations may store the language setting by other means (see the list at the top-most paragraph of this section) and use a combination of these two techniques.