wiki:SandBox

Version 32 (modified by apoirier, 6 years ago) (diff)

--

Project Internationalization with Nagare : setting the locale in your applications

Goals

In a first blog post, we explained how to extract the messages from the python sources in order to provide localized translations for them. We will now explain how to change the locale in your applications in order to make use of the localized string we first extracted.

Time has passed since the first blog post. From version 4.0 on, nagare integrates a i18n module that addresses common internationalization concerns. We will show how to use it effectively.

The i18n module from Nagare provides two ways to configure the locale, that we will describe hereafter:

  • using browser locale negotiation, i.e. using the language settings configured on the client browser
  • by allowing the user to change the language at runtime, for example by using language change links and/or users' profile settings

We will not explain the message extraction here, so we encourage you to read the previous blog post first. However, note that Nagare now greatly simplifies the message extraction process by providing a default setup.cfg file on application creation (i.e. nagare-admin create-app), which includes the python Babel command customizations mentioned in the first blog post.

Getting Started with Nagare i18n

To get started with the Nagare i18n feature, we will create a basic application with localized messages in two languages (French and English) in order to demonstrate how to switch the language at runtime.

First, install Nagare with the i18n option:

$ easy_install nagare[i18n]

Then, create a sample application:

$ nagare create-app i18n_example
$ cd i18n_example
$ python setup.py develop

Edit the app.py file, import the nagare.i18n._ function and mark the messages to be translated with it:

# -*- coding: utf-8 -*-

from __future__ import with_statement

from nagare import presentation
from nagare.i18n import _

class I18nExample(object):
    pass

@presentation.render_for(I18nExample)
def render(self, h, *args):
    h.head << h.head.title(_('Up and Running !'))

    h.head.css_url('/static/nagare/application.css')
    h.head.css('defaultapp', '#main { margin-left: 20px; padding-bottom: 100px; background: url(/static/nagare/img/sakura.jpg) no-repeat 123px 100% }')

    with h.div(id='body'):
        h << h.a(h.img(src='/static/nagare/img/logo.png'), id='logo', href='http://www.nagare.org/', title='Nagare home')

        with h.div(id='content'):
            h << h.div(_('Congratulations!'), id='title')

            with h.div(id='main'):
                h << h.h1(_('Your application is localized !'))

                with h.p:
                    h << _('Here is some localized text...')
    return h.root

# ---------------------------------------------------------------

app = I18nExample

Now, extract the messages and create the localized versions of the applications messages:

$ mkdir data/locale
$ python setup.py extract_messages
$ python setup.py init_catalog -l en_US
$ python setup.py init_catalog -l fr_FR

Edit the data/locale/<lang>/LC_MESSAGES/messages.po files and fill in the missing translations, then type:

$ python setup.py compile_catalog

You should now have .mo files in the data/locale/<lang>/LC_MESSAGES directories, ready to be used by our brand new application. Now, it's time to switch the locale of the application.

If anything is not clear at this point, please refer to the first blog post for details.

Switching the locale using browser negotiation

Since the i18n locale is scoped to a request (because a user may change the language settings of his browser between two requests), the proper place to change the locale of the application is in the start_request method of the wsgi.WSGIApp class. So, we must first introduce an application class that inherit from Nagare's nagare.wsgi.WSGIApp class:

from nagare import wsgi, i18n

class I18nExampleApp(wsgi.WSGIApp):
    AVAILABLE_LOCALES = (
        ('en', 'US'),  # English (US)
        ('fr', 'FR'),  # French (France)
    )

    def start_request(self, root, request, response):
        # set the default locale to a browser negotiated locale
        self.set_default_locale(i18n.NegotiatedLocale(request, self.AVAILABLE_LOCALES, default_locale=self.AVAILABLE_LOCALES[0]))

        # let nagare install the default locale
        super(I18nExampleApp, self).start_request(root, request, response)

In the start_request method, we set the default locale object to a i18n.NegotiatedLocale built from the request. This means that Nagare will scan the Accept-Language headers of the request to find out the best locale to use according to the available locales (expressed as an iterable of (language, territory) tuples). We also set the default locale to the first one available in case the negotiation fail. We then let Nagare install the default locale via the base start_request method that itself call the WSGIApp.set_locale method which initialize the dirname of the locale (i.e. data/locale) and initialize the current locale to the default locale.

Then, we use an instance of I18nExampleApp as our app:

from nagare import component

app = I18nExampleApp(lambda: component.Component(I18nExample()))

Serve the application:

$ nagare-admin serve i18_example --reload

And that's it! The browser now displays the web application either in English or in French depending on your browser language settings.

Allowing the user to change the locale

We can also allow the user to change the locale of the application. For example, we will demonstrate how to use language change links. But if you have an application with authentication and user profiles, you can also store and use the preferred locale of the authenticated user in his profile. As you will see, it doesn't really matter where the locale setting comes from.

Let's start. First, create a view with the language change links:

@presentation.render_for(I18nExample, model='locale_change')
def render(self, h, *args):
    AVAILABLE_LOCALES = (
        (('en', 'US'), u'English'),
        (('fr', 'FR'), u'Français'),
    )

    with h.div(class_='locale-change'):
        for locale_info, label in AVAILABLE_LOCALES:
            h << h.a(label, title=label).action(lambda locale_info=locale_info: self.set_locale(locale_info))
            h << " "

    return h.root

Also, don't forget to render the locale_change view in the default view:

with h.div(id='content'):
    h << h.div(_('Congratulations!'), id='title')
    h << comp.render(h, model='locale_change')

Nothing hard there: we added a view to the I18nExample component (the root component of our application) that shows two HTML <a> elements which set the locale to either English or French. The set_locale method has not been defined yet, so let us define it:

class I18nExample(object):
    def __init__(self):
        self.locale_override = None

    def set_locale(self, locale_info):
        # use the same locale directory as the default/current locale
        locale_dir_name = i18n.get_locale().dirname

        # create a new locale object from the locale information provided
        locale = i18n.Locale(*locale_info, dirname=locale_dir_name)

        # change the locale immediately so that it affects the current request
        i18n.set_locale(locale)

        # also remember the locale setting for the next requests
        self.locale_override = locale

First, we retrieve the locale directory of the current (default) locale. Then, we create a new locale object using the locale information provided in the link actions of the view and the directory name we just retrieved. Once created, we set the locale of the i18n service for the current request (remember, the locale setting is scoped to the request), and we also store the new locale object in a locale_override attribute in order to re-install this locale in subsequent requests.

The last step is to re-install the overridden locale in each request, so let's go back to the start_request method:

def start_request(self, root, request, response):
    # set the default locale to a browser negotiated locale
    self.set_default_locale(i18n.NegotiatedLocale(request, self.AVAILABLE_LOCALES, default_locale=self.AVAILABLE_LOCALES[0]))

    # let nagare install the default locale
    super(I18nExampleApp, self).start_request(root, request, response)

    # if the user has overridden the locale, use it instead of the default locale
    locale_override = root().locale_override
    if locale_override is not None:
        self.set_locale(locale_override)

See, if the locale_override attribute from the root component (an I18nExample instance) is not None (which means that the user has overridden the locale in a previous request), we use it instead of the default one.

Let's see if everything is OK. First, browse the application URL (i.e. http://localhost:8080/i18n_example). You should see the application in a language negotiated with the browser. Click on a language link: the page refreshes and the page content appears in the selected language. Let's ask the same page again, that is, refresh the page: same language again. The language setting is actually stored in the session since Nagare automatically saves the state of the components in the session at the end of each request. So let's remove the session information from the URL (that is, the part after the last slash, like this: ?_s=3363610770854449&_c=00000&_action450653339): we're back to the negotiated locale.

It's now up to you to store the language setting persistently (either via a language prefix in the URL or in authenticated users' profiles) or keep it in the session.

Attachments