This post covers the Symfony Translation component. The component  provides tools to internationalize our applications.

In today’s globalized world internationalizing our applications is a must

The Translation component

Modern applications need to be internationalized and localized to be able to reach people from all over the world. Internationalization – aka i18n – is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization – aka l10n – is the process of adapting internationalized software for a specific region or language by adding locale-specific components such as dates or currency. The Translation component provides different tools to get your application internationalized, but not localized.

The component is split in three main parts:

  • Catalogues: These are key-value collections of messages. The translator class looks in catalogues to find the required matching key.
  • Loaders: Loaders convert translation resources such as YAML, XLIFF or JSON files into PHP array.
  • Dumpers: Dumpers export a catalogue into a given format.

The component ships with several loaders and dumpers for the most common formats: PHP, XLIFF, JSON, YAML, PO, MO, INI and CSV. It it can also load PHP arrays.

Simple example

Let’s start with a very simple example. What we want to do here is translate a few strings into Spanish and Portuguese. Take a look at the code:

<?php

use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');

$translator->addLoader('array', new ArrayLoader());

$translator->addResource('array', array(
    'hello world!' => '¡hola mundo!',
    'hello %name%!' => '¡hola %name%!'
), 'es_ES');

$translator->addResource('array', array(
    'hello world!' => 'Olá mundo!',
    'hello %name%!' => 'Olá %name%!'
), 'pt');

var_dump($translator->trans('hello world!'));
var_dump($translator->trans('hello %name%!', array('%name%' => 'Raul')));
var_dump($translator->trans('hello %name%!', array('%name%' => 'Raul'), null, 'pt_PT'));

We created the $translator object, which is an instance of Translator, and set “es_ES” as default locale, which is the combination of an ISO 639-1 code for the language and ISO 3166-1 alpha-2 for the country. This will be the default locale.

Then, we add the ArrayLoader loader which allows us to pass the PHP code for the translation messages (instead of messing around with translation files in this basic example). Using this loader, we register translation messages for “es_ES” and “pt”.

Finally, we call the trans() method of the translator object to get the translated strings. The first string will output “¡hola mundo!”, as the default locale is “es_ES”. The second string will call “¡hola Raul!” as we use placeholders, while the last call will display “Olá Raul!”, in portuguese, as the translator first checks for “pt_PT” and then “pt”:

string(13) "¡hola mundo!"
string(12) "¡hola Raul!"
string(10) "Olá Raul!"

Instead of full strings, using keys are recommended:

$translator->addResource('array', array(
    'hello_world' => '¡hola mundo!',
    'hello_name!' => '¡hola %name%!'
), 'es_ES');

Load translations from translation files

Normally, we will have translation files instead of plain PHP arrays. Loading these files is as easy as registering the loader we want to use – specific for the file format – and adding the resource. For example, to load the following YAML file:

messages:
    good_morning: "Buenos días"

errors:
    invalid_email: "Email no válido"

We register a YamlFileLoader instance with the key yaml and then we add the full path of the YAML:

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Translator;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');
$translator->addLoader('yaml', new YamlFileLoader());

$translator->addResource('yaml', __DIR__ . '/translations/es_ES.yml' , 'es_ES');

var_dump($translator->trans('messages.good_morning'));
var_dump($translator->trans('errors.invalid_email'));

The output, as expected, is:

string(12) "Buenos días"
string(16) "Email no válido"

Keep in mind that all provided loaders are dependent on the Config component, and YamlFileLoader has a dependency on the Yaml component too.

Translation domains

In the previous example, we used the same YAML file to define “normal” and error messages, and as nested keys are flatten, we used “messages.good_morning” and “errors.invalid_email” as keys. While this works, it is usually a good idea to have different translation domains, so we can separate translation messages in different files by their purpose. It is common to have domains for normal messages, errors, form messages or validations.

In our example, we could split the YAML file into two files and load them separately indicating the domain. If we don’t provide a domain name, “messages” is the default one.

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Translator;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');
$translator->addLoader('yaml', new YamlFileLoader());

$translator->addResource('yaml', __DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');
$translator->addResource('yaml', __DIR__ . '/translations/errors.es_ES.yml' , 'es_ES', 'errors');

var_dump($translator->trans('good_morning'));
var_dump($translator->trans('invalid_email', array(), 'errors'));

Pluralization

Pluralization is a really hard task to achieve in some languages such as Russian or Polish, as rules can be quite complex. Anyway, the component provides the mechanisms to define these rules and be able to pluralize messages.

For example, to show the message “There is 1 cat” if there is actually only 1 cat, and “There are X cats” if there are two or more, it is defined using options and placeholders:

cats: "There is 1 cat|There are %number% cats"

Then, the transChoice() method of the translator instance takes care of it. It gets the translation key, the number of cats in this example and optionally the placeholder values:

var_dump($translator->transChoice('cats', 1));
var_dump($translator->transChoice('cats', 2, array('%number%' => 2)));

This code will output:

string(14) "There is 1 cat"
string(16) "There are 2 cats"

You can find more examples of pluralization in the Symfony documentation.

Dump messages

The component is also shipped with a few dumpers that allow us to export catalogues into different formats. This can be useful for using third-party translation files or if you just to have them in an easier to read format for a human.

In the following example, we load a catalogue from a YAML file and export it to JSON, using a JsonFileDumper instance:

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Dumper\JsonFileDumper;

include_once __DIR__. '/vendor/autoload.php';

$loader = new YamlFileLoader();
$catalogue = $loader->load(__DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');

$dumper = new JsonFileDumper();
$dumper->dump($catalogue, array('path' => __DIR__.'/dumps'));

This script creates the file ./dumps/messages.es_ES.yml, with the messages in JSON format:

{
    "good_morning": "Buenos d\u00edas",
    "good_night": "Buenas noches",
    "welcome": "Bienvenido"
}

Custom loaders

So, this is really cool, but what happens when we are trying to modernize a legacy project using Symfony2 components and it uses a weird custom format for translation files? Could we still use the Translation component? Absolutely! We just have to create a loader for that format.

Imagine we have a custom format where translation messages are defined using one line for each translation and parenthesis to wrap the key and the message. A translation file would look like this:

(welcome)(Bienvenido)
(goodbye)(Adios)
(hello)(Hola)

To define a custom loader able to read this kind of files, we must implement the LoaderInterface interface, which defines a load() method. In our loader, this method will get a filename and parse it to create an array. Then, it will create the catalog that will be returned.

<?php

namespace RaulFraile\Loader;

use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Loader\LoaderInterface;

class CustomLoader implements LoaderInterface
{

    public function load($resource, $locale, $domain = 'messages')
    {
        $messages = array();
        $lines = file($resource);

        foreach ($lines as $line) {
            if (preg_match('/\(([^\)]+)\)\(([^\)]+)\)/', $line, $matches)) {
                $messages[$matches[1]] = $matches[2];
            }
        }

        $catalogue = new MessageCatalogue($locale);
        $catalogue->add($messages, $domain);

        return $catalogue;
    }

}

As you see, adding support for new formats is fast and painless, and once you have it, it can be used as any other loader:

<?php

use Symfony\Component\Translation\Translator;
use RaulFraile\Loader\CustomLoader;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');
$translator->addLoader('custom', new CustomLoader());

$translator->addResource('custom', __DIR__.'/translations/messages.txt', 'es_ES');

var_dump($translator->trans('hello'));

It outputs:

string(4) "Hola"

That’s the magic of well designed software, extending it is like child’s play.

It is also possible to create a custom dumper for any made-up format invent. To achieve this, a new class implementing the DumperInterface interface must be created. To write the dump contents into a file, extending the FileDumper class is going to save us a few lines.

<?php

namespace RaulFraile\Dumper;

use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Dumper\FileDumper;

class CustomDumper extends FileDumper
{

    public function format(MessageCatalogue $messages, $domain = 'messages')
    {
        $output = '';

        foreach ($messages->all($domain) as $source => $target) {
            $output .= sprintf("(%s)(%s)\n", $source, $target);
        }

        return $output;
    }

    protected function getExtension()
    {
        return 'txt';
    }
}

The format method creates the output string, that will be used by the dump() method of the FileDumper class to create the file. The dumper can be used like any other built-in dumper. In this example, we export the translation messages defined in the YAML file into a text file with our custom format:

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use RaulFraile\Dumper\CustomDumper;

include_once __DIR__. '/vendor/autoload.php';

$loader = new YamlFileLoader();
$catalogue = $loader->load(__DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');

$dumper = new CustomDumper();
$dumper->dump($catalogue, array('path' => __DIR__.'/dumps'));

Conclusion

We have seen that the Translation component makes the arduous task of internazionalizating our application a bit easier, providing all the needed tools to do so: simple translations, placeholders, pluralization, loaders, and dumpers, as well as options to to extend it.

Who’s using it?

More info

Photo: World-map, by greyweed