Internationalised applications¶
The output of an internationalised application is customised to conform to the local customs and language of users. To enable such customisation, a Reahl programmer writes code that take these user preferences into account. The programmer (or someone else) also provides translations of the user messages contained in the source code of the application – to different natural languages.
Rather than solving this entire topic on its own, Reahl makes use of the Babel library for internationalisation support. Glue is provided to access the features of Babel easily, in a way that also solves the intricacies of the component-based nature of a Reahl application.
The example in this section is an adaptation of the simple address book example introduced in User interface basics. In this version of the example, several parts of the application have been changed purely in order to internationalise the application. Only one bit of its actual functionality is also changed: the application now saves the date on which the Address was added to the database. This change in functionality gives us the opportunity to demonstrate how to format a date based on the current locale.
Making your component translatable¶
A way to switch locales¶
The first adaptation to the program is the addition of a Menu
which
will allow users to choose the locale they prefer for display. Since
we want the Menu
to be on all pages of the application, the correct
way to add it is to create a Widget
that can be used as page
for the application, derived from HTML5Page
.
A HTML5Page
has a .body attribute to which one can add
children that will appear in is body. The novel
thing here is how the Menu
is constructed: by using the
from_languages()
class method:
class AddressBookPage(HTML5Page):
def __init__(self, view):
super(AddressBookPage, self).__init__(view, style='basic')
contents_layout = ColumnLayout(('secondary', UnitSize('1/4')),
('main', UnitSize('3/4'))).with_slots()
self.use_layout(PageLayout())
self.layout.contents.use_layout(contents_layout)
contents_layout.columns['secondary'].add_child(Menu(view).use_layout(VerticalLayout()).with_languages())
Displaying translated messages¶
The messages displayed to the user by the application are contained in
literal strings in the source code of a program. The first step
towards offering translated versions of these messages is to replace
each such literal string with a call to a special function which takes
the original string as argument, but returns a different string as a
result: a translation of the original message to the target
language. This special function is given the name _
(underscore). Thus, replacing a literal string like 'hello world'
will result in _('hello world')
instead.
Automated tools are later used to parse the source code of your
application, looking for these calls to _
in order to find and
collect all the user messages so that they can be listed in a
“catalogue” where a human translator can supply the necessary
translation for each message.
In Reahl, _
is not a function, but an instance of callable class,
called Translator
. To make an instance of Translator
available to your
code, you need to declare it at the top of each module.
Here is the top of the file containing this example:
from __future__ import print_function, unicode_literals, absolute_import, division
import datetime
from sqlalchemy import Column, Integer, UnicodeText, Date
from reahl.sqlalchemysupport import Session, Base
from reahl.web.fw import UserInterface, Widget
from reahl.web.ui import HTML5Page, Form, TextInput, LabelledBlockInput, Button, Div, P, H, FieldSet, \
Menu, VerticalLayout
from reahl.web.layout import PageLayout
from reahl.web.pure import ColumnLayout, UnitSize
from reahl.component.modelinterface import exposed, EmailField, Field, Event, Action
from reahl.component.i18n import Translator
import babel.dates
_ = Translator('reahl-doc')
When a human translates messages, the messages of a particular
component are translated and stored together in something called a
catalogue. Thus, when creating your Translator
instance, you need to
specify the name of your component to its constructor so that the
Translator
instance can reference the correct catalogue – the
catalogue of your component.
This example lives inside the ‘reahl-doc’ component, and hence its translations need to be looked up in the language catalogues of reahl-doc. When you check the example out, this gets changed to i18nexample – that is what you would have put in the code if you developed this example independently of the Reahl documentation.
The Address class contains some examples of strings meant for human
consumption that are passed through _
for translation:
class Address(Base):
__tablename__ = 'i18nexample_address'
id = Column(Integer, primary_key=True)
email_address = Column(UnicodeText)
name = Column(UnicodeText)
added_date = Column(Date)
@exposed
def fields(self, fields):
fields.name = Field(label=_('Name'), required=True)
fields.email_address = EmailField(label=_('Email'), required=True)
def save(self):
self.added_date = datetime.date.today()
Session.add(self)
@exposed
def events(self, events):
events.save = Event(label=_('Save'), action=Action(self.save))
Note
The _
Translator
should never be called in module
scope. Module scope is executed once, when the application is
started. Looking up the translation of a message for a particular
user can only happen once the chosen locale is known – and that
will, of course, be different for each user. Hence, such code
cannot be done at application starting time.
Dealing with plural forms¶
In English, words have one form for singular, and another for plural. In some other languages there can be many different plural forms that differ depending on the number of items you are talking about. To deal with such plural forms, invoke the Translation.ngettext method instead. This method takes the English singular and plural form, and the number of items in question. It returns the correct plural form for the target language, given the number of items.
In order to show how this is done, we have customised the heading in AddressBookPanel to use a plural form if there is more than one Address listed:
class AddressBookPanel(Div):
def __init__(self, view):
super(AddressBookPanel, self).__init__(view)
self.add_child(H(view, 1, text=_.ngettext('Address', 'Addresses', Session.query(Address).count())))
for address in Session.query(Address).all():
self.add_child(AddressBox(view, address))
self.add_child(AddAddressForm(view))
Other customisations¶
Many other things can be done differently, depending on the chosen
locale. For all of these things, you can use the Babel library directly, usually passing it a
locale string which is obtainable from _.current_locale
in a Reahl
program.
AddressBox provides an example where the date is formatted according to locale:
class AddressBox(Widget):
def __init__(self, view, address):
super(AddressBox, self).__init__(view)
formatted_date = babel.dates.format_date(address.added_date, locale=_.current_locale)
self.add_child(P(view, text='%s: %s (%s)' % (address.name, address.email_address, formatted_date)))
Making it possible for others to translate your component¶
As shown up to now, the original author of a component merely makes it possible for others to provide locales for the component. Babel uses data from the CLDR locale database to determine many customs relating to a particular locale. The only thing that needs to be tailor-made for particular piece of software to support a particular locale is a set of translations for the actual user messages contained in the software.
Before anyone can provide a catalogue of translated versions of the messages in your component, though, you have to create a catalogue containing all the original versions of user messages contained in the source code of your component. Others use this list of messages to provide the necessary translation of each user message.
Message catalogues live in a Python package inside a component. The
first step to adding a catalogue is thus to create this package, and
tell the Reahl component infrastructure where to find it. In our
example, create the i18nexamplemessages package in the top level
directory of the component. (Remember that a Python package is only a
Python package when it contains a file called __init__.py.) To
inform the Reahl component infrastructure where to find the package
containing translation catalogues in your component, you need to add a
<translations>
tag to the .reahlproject file of your component
which names this “translations” package of your component:
<translations locator="reahl.doc.examples.tutorial.i18nexample.i18nexamplemessages"/>
Note
Remember to always run reahl setup -- develop -N
after changing
a .reahlproject file in order to update the project metadata.
Once a “translations” package is named, you can create a catalogue of user messages found in your component by running the following from inside the root directory of your component:
reahl extractmessages
Supplying translations to another component¶
At this point, our example is translatable. That means that anyone can make it support a new locale by adding translations of our user messages to the natural language of the needed locale. These new translations can be added in a totally different component without touching the original component at all, but nothing prevents us from adding a set of translations in the original component either.
In this example, the latter approach is taken because it keeps everything nicely together in a single egg, simplifying our example infrastructure somewhat. Whether you choose to provide translations inside the original component, or in a different one, the process is the same.
If you do provide translations for egg A in egg B, it is advisable to let egg B depend on the particular version of egg A for which it provides translations. Different versions of egg A may have a different set of user messages to translate, meaning different versions of your egg B!
Also, you can only provide translations in a component once you have
notified the Reahl component infrastructure in which package to look
for language catalogues in your component. Since we have already added
a <translations>
tag to the .reahlproject file of i18nexample
this has already been taken care of.
Adding a translation¶
To add a translation to the current component for the af locale, for i18nexample, run the following command inside the top-level directory of i18nexample:
reahl addlocale af i18nexample
Running this command creates the following directory structure inside the “translations” package mentioned before:
├── i18nexamplemessages
│ ├── af
└── LC_MESSAGES
└── i18nexample.po
The file you are interested in is af/LC_MESSAGES/i18nexample.po. This is a text file which lists each user message originating from the i18nexample component. (Look for the strings labelled msgid.) Underneath each message (see the msgstr labels), you can now supply the Afrikaans translation of each message in this file.
Once you are satisfied with all your translations, the catalogue has to be compiled before it can be used by a running application. To do this, run the following from the top level directory of the component which contains the new catalogue:
reahl compiletranslations
This command compiles all translations found in the “translations” package of the current component, regardless of language or which component they are for.
Warning
A Reahl application only supports a given locale if all the components it comprises of support that locale. Hence, when adding a new locale that is not supported by Reahl itself, you will have to provide the translations for your new locale for all of the Reahl components as well! Luckily you can do that easily, in your own component.
Maintaining translations¶
Software keeps changing. Chances are that newer versions of a component for which you provide translations may contain a different list of user messages than it had in a previous version. When this happens the bulk of the messages already translated usually stay unchanged, with a change or a new message here and there.
Running the following command updates all existing translations found in the “translations” package of the current component, merging new changes into the set of translations you already have:
reahl mergetranslations