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 VMenu which will allow users to choose the locale they prefer for display. Since we want the VMenu 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 TwoColumnPage.
A TwoColumnPage has a .secondary attribute to which one can add children that will appear in the smaller, secondary column. The novel thing here is how the VMenu is constructed: by using the Menu.from_languages() class method:
class AddressBookPage(TwoColumnPage):
def __init__(self, view):
super(AddressBookPage, self).__init__(view, style='basic')
self.secondary.add_child(VMenu.from_languages(view))
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 TwoColumnPage, Form, TextInput, LabelledBlockInput, Button, Panel, P, H, InputGroup, VMenu
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(Panel):
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