The user interface needs to be adapted for i18n applications. Templates must be able to display labels, messages, and navigation in several languages but with the same presentation. Symfony recommends that you build your templates with the default language, and that you provide a translation for the phrases used in your templates in a dictionary file. That way, you don't need to change your templates each time you modify, add, or remove a translation.
13.4.1. Configuring Translation
The templates are not translated by default, which means that you need to activate the template translation feature in the settings.yml
file prior to everything else, as shown in Listing 13-11.
Listing 13-11 - Activating Interface Translation, in frontend/config/settings.yml
all:
.settings:
i18n: on
13.4.2. Using the Translation Helper
Let's say that you want to create a website in English and French, with English being the default language. Before even thinking about having the site translated, you probably wrote the templates something like the example shown in Listing 13-12.
Listing 13-12 - A Single-Language Template
Welcome to our website. Today's date is <?php echo format_date(date()) ?>
For symfony to translate the phrases of a template, they must be identified as text to be translated. This is the purpose of the __()
helper (two underscores), a member of the I18N helper group. So all your templates need to enclose the phrases to translate in such function calls. Listing 13-12, for example, can be modified to look like Listing 13-13 (as you will see in the "Handling Complex Translation Needs" section later in this chapter, there is an even better way to call the translation helper in this example).
Listing 13-13 - A Multiple-Language-Ready Template
<?php use_helper('I18N') ?>
<?php echo __('Welcome to our website.') ?>
<?php echo __("Today's date is ") ?>
<?php echo format_date(date()) ?>
Tip If your application uses the I18N helper group for every page, it is probably a good idea to include it in the standard_helpers
setting in the settings.yml
file, so that you avoid repeating use_helper('I18N')
for each template.
13.4.3. Using Dictionary Files
Each time the __()
function is called, symfony looks for a translation of its argument in the dictionary of the current user's culture. If it finds a corresponding phrase, the translation is sent back and displayed in the response. So the user interface translation relies on a dictionary file.
The dictionary files are written in the XML Localization Interchange File Format (XLIFF), named according to the pattern messages.[language code].xml
, and stored in the application i18n/
directory.
XLIFF is a standard format based on XML. As it is well known, you can use third-party translation tools to reference all text in your website and translate it. Translation firms know how to handle such files and to translate an entire site just by adding a new XLIFF translation.
Tip In addition to the XLIFF standard, symfony also supports several other translation back-ends for dictionaries: gettext, MySQL, and SQLite. Refer to the API documentation for more information about configuring these back-ends.
Listing 13-14 shows an example of the XLIFF syntax with the messages.fr.xml
file necessary to translate Listing 13-13 into French.
Listing 13-14 - An XLIFF Dictionary, in frontend/i18n/messages.fr.xml
<?xml version="1.0" ?>
<xliff version="1.0">
<file orginal="global" source-language="en_US" datatype="plaintext">
<body>
<trans-unit id="1">
<source>Welcome to our website.</source>
<target>Bienvenue sur notre site web.</target>
</trans-unit>
<trans-unit id="2">
<source>Today's date is </source>
<target>La date d'aujourd'hui est </target>
</trans-unit>
</body>
</file>
</xliff>
The source-language
attribute must always contain the full ISO code of your default culture. Each translation is written in a trans-unit
tag with a unique id
attribute.
With the default user culture (set to en_US), the phrases are not translated and the raw arguments of the __()
calls are displayed. The result of Listing 13-13 is then similar to Listing 13-12. However, if the culture is changed to fr_FR
or fr_BE
, the translations from the messages.fr.xml
file are displayed instead, and the result looks like Listing 13-15.
Listing 13-15 - A Translated Template
Bienvenue sur notre site web. La date d'aujourd'hui est
<?php echo format_date(date()) ?>
If additional translations need to be done, simply add a new messages.``XX``.xml
translation file in the same directory.
Tip As looking for dictionary files, parsing them, and finding the correct translation for a given string takes some time, symfony uses an internal cache to speedup the process. By default, this cache uses the filesystem. You can configure how the i18N cache works (for instance, to share the cache between several servers) in the factories.yml
(see Chapter 19).
13.4.4. Managing Dictionaries
If your messages.XX.xml
file becomes too long to be readable, you can always split the translations into several dictionary files, named by theme. For instance, you can split the messages.fr.xml
file into these three files in the application i18n/
directory:
navigation.fr.xml
terms_of_service.fr.xml
search.fr.xml
Note that as soon as a translation is not to be found in the default messages.XX.xml
file, you must declare which dictionary is to be used each time you call the __()
helper, using its third argument. For instance, to output a string that is translated in the navigation.fr.xml
dictionary, write this:
<?php echo __('Welcome to our website', null, 'navigation') ?>
Another way to organize translation dictionaries is to split them by module. Instead of writing a single messages.XX.xml
file for the whole application, you can write one in each modules/[module_name]/i18n/
directory. It makes modules more independent from the application, which is necessary if you want to reuse them, such as in plug-ins (see Chapter 17).
New in symfony 1.1
As updating the i18n dictionaries by hand is quite error prone, symfony provides a task to automate the process. The i18n:extract
task parses a symfony application to extract all the strings that need to be translated. It takes an application and a culture as its arguments:
> php symfony i18n:extract frontend en
By default, the task does not modify your dictionaries, it just outputs the number of new and old i18n strings. To append the new strings to your dictionary, you can pass the --auto-save
option:
> php symfony i18n:extract --auto-save frontend en
You can also delete old strings automatically by passing the --auto-delete
option:
> php symfony i18n:extract --auto-save --auto-delete frontend en
Note The current task has some known limitations. It can only works with the default messages
dictionary, and for file based backends (XLIFF
or gettext
), it only saves and deletes strings in the main apps/frontend/i18n/messages.XX.xml
file.
13.4.5. Handling Other Elements Requiring Translation
The following are other elements that may require translation:
- Images, text documents, or any other type of assets can also vary according to the user culture. The best example is a piece of text with a special typography that is actually an image. For these, you can create subdirectories named after the user
culture
:
<?php echo image_tag($sf_user->getCulture().'/myText.gif') ?>
- Error messages from validation files are automatically output by a
__()
, so you just need to add their translation to a dictionary to have them translated. - The default symfony pages (page not found, internal server error, restricted access, and so on) are in English and must be rewritten in an i18n application. You should probably create your own
default
module in your application and use__()
in its templates. Refer to Chapter 19 to see how to customize these pages.
13.4.6. Handling Complex Translation Needs
Translation only makes sense if the __()
argument is a full sentence. However, as you sometimes have formatting or variables mixed with words, you could be tempted to cut sentences into several chunks, thus calling the helper on senseless phrases. Fortunately, the __()
helper offers a replacement feature based on tokens, which will help you to have a meaningful dictionary that is easier to handle by translators. As with HTML formatting, you can leave it in the helper call as well. Listing 13-16 shows an example.
Listing 13-16 - Translating Sentences That Contain Code
// Base example
Welcome to all the <b>new</b> users.<br />
There are <?php echo count_logged() ?> persons logged.
// Bad way to enable text translation
<?php echo __('Welcome to all the') ?>
<b><?php echo __('new') ?></b>
<?php echo __('users') ?>.<br />
<?php echo __('There are') ?>
<?php echo count_logged() ?>
<?php echo __('persons logged') ?>
// Good way to enable text translation
<?php echo __('Welcome to all the <b>new</b> users') ?> <br />
<?php echo __('There are %1% persons logged', array('%1%' => count_logged())) ?>
In this example, the token is %1%
, but it can be anything, since the replacement function used by the translation helper is strtr()
.
One of the common problems with translation is the use of the plural form. According to the number of results, the text changes but not in the same way according to the language. For instance, the last sentence in Listing 13-16 is not correct if count_logged()
returns 0 or 1. You could do a test on the return value of this function and choose which sentence to use accordingly, but that would represent a lot of code. Additionally, different languages have different grammar rules, and the declension rules of plural can be quite complex. As this problem is very common, symfony provides a helper to deal with it, called format_number_choice()
. Listing 13-17 demonstrates how to use this helper.
Listing 13-17 - Translating Sentences Depending on the Value of Parameters
<?php echo format_number_choice(
'[0]Nobody is logged|[1]There is 1 person logged|(1,+Inf]There are %1% persons logged', array('%1%' => count_logged()), count_logged()) ?>
The first argument is the multiple possibilities of text. The second argument is the replacement pattern (as with the __()
helper) and is optional. The third argument is the number on which the test is made to determine which text is taken.
The message/string choices are separated by the pipe (|
) character followed by an array of acceptable values, using the following syntax:
[1,2]
: Accepts values between 1 and 2, inclusive(1,2)
: Accepts values between 1 and 2, excluding 1 and 2{1,2,3,4}
: Only values defined in the set are accepted[-Inf,0)
: Accepts values greater or equal to negative infinity and strictly less than 0{n: n % 10 > 1 && n % 10 < 5} pliki
: Matches numbers like 2, 3, 4, 22, 23, 24 (useful for languages like polish or russian) New in development version
Any nonempty combinations of the delimiters of square brackets and parentheses are acceptable.
The message will need to appear explicitly in the XLIFF file for the translation to work properly. Listing 13-18 shows an example.
Listing 13-18 - XLIFF Dictionary for a format_number_choice()
Argument
...
<trans-unit id="3">
<source>[0]Nobody is logged|[1]There is 1 person logged|(1,+Inf]There are %1% persons logged</source>
<target>[0]Personne n'est connecté|[1]Une personne est connectée|(1,+Inf]Il y a %1% personnes en ligne</target>
</trans-unit>
...
13.4.7. Calling the Translation Helper Outside a Template
Not all the text that is displayed in a page comes from templates. That's why you often need to call the __()
helper in other parts of your application: actions, filters, model classes, and so on. Listing 13-19 shows how to call the helper in an action by retrieving the current instance of the I18N
object through the context singleton.
Listing 13-19 - Calling __()
in an Action
$this->getContext()->getI18N()->__($text, $args, 'messages');