Symfony Translation: Internationalization Made Easy
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