The definitive guide of Symfony 1.2

13.4. Interface Translation

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');