Symfony Translation: Internationalization Made Easy

Share this article

If you’ve ever worked to develop a site which needed to be available in multiple languages then you know how difficult it can be. With the help of Symfony2’s Translation component you can easily make internationalized sites. I’ll show you how with some sample code and some discussion on its API.

A Basic Example

Let’s start by creating the file translation.php with the contents below.
<?php
require 'vendor/autoload.php';
use SymfonyComponentTranslationTranslator,
   SymfonyComponentTranslationLoaderArrayLoader;

$translator = new Translator('fr_FR');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array(
   'Hello World!' => 'Bonjour tout le monde!',
   ),
   'fr_FR'
);

echo $translator->trans('Hello World!') . "n";
echo $translator->trans('How are you?') . "n";
A Translator object is created by passing the locale you want to use (in this case French/France) and optionally an instance of a MessageSelector object as the second argument. The  MessageSelector is used for pluralization. If you do not pass one, the Translator constructor creates its own. A loader is then set which is used to load the language strings. The addLoader() method accepts a name (you can give any name you like) and an object of type LoaderInterface. In this case I’m making use of ArrayLoader which implements the interface. The addResource() method adds the translation messages. The first argument is the name of the loader given to addLoader() and the second argument in this case is an array (since I am using ArrayLoader). Depending upon the loader, the second argument can change. The third argument is the language string. The translation lookup is done via the trans() method. If no matching translation is found, the original string is returned. Translations are not typically done by programmers. Instead they are done by professional translators. Translators may not have programming knowledge and we may not want to give them code. Luckily, Symfony can use different loaders from which the list of translation messages are loaded. The complete list of supported loaders are: ArrayLoader, CsvFileLoader, MoFileLoader, XliffFileLoader, IcuDatFileLoader, PhpFileLoader, YamlFileLoader, IcuResFileLoader
, PoFileLoader, IniFileLoader, and QtTranslationsLoader. Let’s see how to load translation strings using PoFileLoader instead of array so things will be easier for the people who translate. Change the second argument of addLoader() to an instance of a PoFileLoader object. Also in the addResource() method, pass the path where the PO file resides.
<?php
require 'vendor/autoload.php';
use SymfonyComponentTranslationTranslator,
   SymfonyComponentTranslationLoaderPoFileLoader;

$translator = new Translator('fr_FR');
$translator->addLoader('pofile', new PoFileLoader());
$translator->addResource('pofile', 'languages/po/fr_FR.po', 'fr_FR');

Handling Fallback Locales

You can make use of PHP’s Locale class to get the requested locale from the Accept-Language header, or make use of the Symfony Locale component (which extends PHP’s class with some additional functionality), to set the locale dynamically.
<?php
$locale = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
$translator = new Translator($locale);
But what if the locale is unavailable? You can set a fallback locale using the setFallbackLocale() method. If a locale isn’t found, or the locale exists but is missing the translation, then Translator will look to the fallback.
<?php
$translator->setFallbackLocale('fr');
The same language can differ between countries; consider English for example and you’ll notice en_GB for British English, en_US for American English, etc. Each language can have differences depending on the region. So what about having multiple fallbacks? Le’s say in this case the requested locale is fr_CA and you don’t have fr_CA translations. As a fallback you could try to get translations from fr_FR, and if not then from en_US, and then general English.
<?php
$translator->setFallbackLocale(array('fr_FR', 'en_US', 'en'));
There can be cases when you want to split one locale’s messages into many small units. By default, all strings are added to and looked up in the messages domain (this is why we didn’t need to pass the domain to trans()). The naming convention for translation files is: domain.localeformat, for example messages.fr.po, navigation.fr.po, etc. If you want to get translations from a specific domain then you must specify the domain as the third argument to trans()
. Fallbacks happen within the same domain only and not to other domains. Together, this looks like this:
<?php
require 'vendor/autoload.php';
use SymfonyComponentTranslationTranslator,
   SymfonyComponentTranslationLoaderPoFileLoader;

$locale = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
$translator = new Translator($locale);
$translator->setFallbackLocale(array('fr_FR', 'en_US', 'en'));

$translator->addLoader('pofile', new PoFileLoader());
$iterator = new FilesystemIterator("languages/po");
$filter = new RegexIterator($iterator, '/.(po)$/');
foreach($filter as $entry) {
    $name = $entry->getBasename('.po');
    list($domain, $locale) = explode('.', $name);
    $translator->addResource('pofile',
        $entry->getPathname(), $locale, $domain
    );
}

echo $translator->trans('Hello World!') . "n";
echo $translator->trans('How are you?') . "n";
echo $translator->trans('How are you?', array(), 'navigation') . "n";
If you are naming your translation files with regions, for instance fr_FR.po, fr_CA.po, etc., then its might be a good idea to name some of your fallbacks solely by language (fr.po). Consider someone from Belgium who is visiting your website and the Accept-Language header requests fr_BE. You may not have fr_BE, and fr_FR wouldn’t match. It’s better fallback to fr.po than switch to a different language entirely such as English. A language-only named fallback can be extracted from the locale and placed at the head of the array like so:
<?php
$fallbacks = array('fr_FR', 'en_US', 'en');
$locale = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
array_unshift($fallbacks, substr($locale, 0, 2));
$translator->setFallbackLocale($fallbacks);

Pluralization

Handling plural forms is one of the toughest parts of Internationalization. To select different translations based on number, you use the transChoice() method. Here’s an exaggerated example to demonstrate its usage:
<?php
$value = 1;
echo $translator->transChoice(
   '[-Inf, 0]There is nothing to delete|{1}Are you sure you want to delete this file|]1,10[ %count% files will be deleted|[10,Inf] Are you sure you want to delete all files',
  $value,
  array('%count%' => $value)
) . "n";
Alternate strings are separated by a pipe character. Changing the value of $value and see how Translator changes the output. Initially you should get the message “Are you sure you want to delete this file”. If you change $value to 2 to 9 you will get the message “$value files will be deleted”. If you use 10 or greater you get the message “Are you sure you want to delete all files”. If $value is less than 1 you will get “There is nothing to delete”. Symfony Translation uses ISO 31-11 notation so the above uses interval classes to select the right sentence. We can write ranges like this:
[a, b] means a <= x <= b
[a, b[ means a <= x < b
]a, b] means  a < x <= b
]a, b[ means a < x < b
A helpful mnemonic device to easier understand the range is to look in which direction the square bracket is opening towards. If the square bracket is opening towards the number then it is inclusive.
[3 means value is <= 3 (inclusive)
]3 means value is < 3 (exclusive)
You can also define a set of specific values with braces, for example {1,2,3} to match just the values 1, 2, and 3.

Converting Between Translation Formats

I started this article using ArrayLoader because it’s probably the easiest for most developers to start with. But then I switched to PoFileLoader because PO files are easier for translators. What if you have a set of translations as arrays, or even another format like YAML, and want to convert them? You can convert between one format and another for any of Symfony’s loaders using dumpers.
<?php
require 'vendor/autoload.php';
   SymfonyComponentTranslationLoaderYamlFileLoader,
   SymfonyComponentTranslationMessageCatalogue,
   SymfonyComponentTranslationDumperPoFileDumper;

$loader = new YamlFileLoader();
$iterator = new FilesystemIterator("languages/yaml");
$filter = new RegexIterator($iterator, '/.(yml)$/');
foreach($filter as $file) {
    $name = $file->getBasename('.yml');
    list($domain, $locale) = explode('.', $name);
    $array = $loader->load($file->getPathname(), $locale, $domain);
    $catalogue = new MessageCatalogue($locale);
    $catalogue->addCatalogue($array);

    $dumper = new PoFileDumper();
    $dumper->dump($catalogue, array('path'=> __DIR__ . '/languages/pofile'));
}
Just substitute the loader with the appropriate one you are using for input and the dumper for whatever output you need.

Summary

We have covered how to translate strings, how to work with fallback locales, how pluralization is handled, and how to make use of the Dumper. I hope this tutorial helps you to start with internationalization which is made simple with the help of the Symfony Translation component. Image via Fotolia

Frequently Asked Questions (FAQs) about Symfony Translation and Internationalization

How can I use Symfony translation for pluralization?

Pluralization in Symfony translation is a feature that allows you to handle singular and plural forms of a word. To use it, you need to define your translations in the message file using the pipe character “|”. The first part before the pipe is for singular form and the second part is for plural form. For example, ‘apple|apples’. Then, in your Twig template, you can use the ‘transchoice’ filter to select the appropriate form based on a variable. For example, ‘{{ apples|transchoice(apple_count) }}’ where ‘apple_count’ is the number of apples.

How can I use Symfony translation in Twig templates?

To use Symfony translation in Twig templates, you need to use the ‘trans’ filter. This filter translates the given message. For example, ‘{{ ‘Hello’|trans }}’ will translate the word ‘Hello’ into the current locale. You can also pass parameters to the ‘trans’ filter. For example, ‘{{ ‘Hello %name%’|trans({‘%name%’: ‘John’}) }}’ will replace ‘%name%’ with ‘John’ in the translated string.

How can I change the current locale in Symfony?

The current locale in Symfony can be changed using the ‘setLocale’ method of the ‘Request’ object. For example, ‘$request->setLocale(‘fr’)’ will change the current locale to French. You can also change the locale for a specific user by storing their preferred locale in the session or the database.

How can I use Symfony translation for date and time formatting?

Symfony translation can be used for date and time formatting by using the ‘date’ and ‘time’ filters in Twig templates. These filters format a date or time according to the current locale. For example, ‘{{ date|date(‘F j, Y’) }}’ will format the date in the format ‘Month day, Year’ according to the current locale.

How can I use Symfony translation for number formatting?

Symfony translation can be used for number formatting by using the ‘number_format’ filter in Twig templates. This filter formats a number according to the current locale. For example, ‘{{ price|number_format(2, ‘.’, ‘,’) }}’ will format the price with 2 decimal places, a dot as the decimal point, and a comma as the thousands separator.

How can I use Symfony translation for currency formatting?

Symfony translation can be used for currency formatting by using the ‘money’ filter in Twig templates. This filter formats a number as a currency value according to the current locale. For example, ‘{{ price|money(‘USD’) }}’ will format the price in US dollars.

How can I use Symfony translation for percentage formatting?

Symfony translation can be used for percentage formatting by using the ‘percent’ filter in Twig templates. This filter formats a number as a percentage value according to the current locale. For example, ‘{{ discount|percent }}’ will format the discount as a percentage.

How can I use Symfony translation for measurement unit formatting?

Symfony translation can be used for measurement unit formatting by using the ‘unit’ filter in Twig templates. This filter formats a number with a measurement unit according to the current locale. For example, ‘{{ weight|unit(‘kg’) }}’ will format the weight in kilograms.

How can I use Symfony translation for spell out numbers?

Symfony translation can be used to spell out numbers by using the ‘spellout’ filter in Twig templates. This filter spells out a number according to the current locale. For example, ‘{{ 123|spellout }}’ will spell out the number 123.

How can I use Symfony translation for ordinal numbers?

Symfony translation can be used for ordinal numbers by using the ‘ordinal’ filter in Twig templates. This filter formats a number as an ordinal number according to the current locale. For example, ‘{{ 1|ordinal }}’ will format the number 1 as ‘1st’.

Hari K THari K T
View Author

Hari K T is a Freelance LAMP developer/consultant, open-source contributor auraphp and speaker.

Advanced
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week