Translation and Localization in Symfony 3.4 and 4.0
Website translation is a very popular and light topic, ideal for an introduction to Symfony. In this article, I’m will discuss the topic in a structured manner, focusing on Symfony 3.4 and 4.0. The included code snippets have been tested on both versions, but the application created while writing this article uses Symfony 4.0.
A Traditional Approach to the Problem
When developing a project, only rarely do we give thought to its future internationalization until the day when a different language version is needed. For projects developed in compliance with industry standards, regardless of technology, the translation should not present a serious problem. But is that really the case in practice? All messages, labels, titles and comments in your templates should be easily translatable, regardless of whether the translation was planned or not. Such data, which is exclusively constant, should be grouped together in one or several files, e.g. translation files.
But what about your database? The products, categories, and items entered into it should also be somehow included in the translation process. How should you proceed? Should you create a table with translations for each table that includes content to be translated? Or maybe you should use one table to translate everything? Maybe you can solve this problem with some sort of record duplication in your tables? Let’s consider this in the next steps.
Translation vs Localization
Let’s first ask yourself: what do you expect? Translation? Localization? They are not one and the same thing. Localization is a broader process ‒ in addition to translating text, it involves things such as changing the date format or, for a store, converting the currency, or making some other change to your project.
Before starting to implement different language versions, all constants ‒ both numeric and those about to become strings translated into other languages, as well as other set values, should be grouped and separated from the rest of the code. That last part is important!
Just like it should be obvious to keep CSS directives in style templates, not embedded in HTML content, the same should apply to all labels embedded in templates. Grouping them together, even in translation files, should not appear strange at all. But what about exception messages? Did you remember about them and did you group them together? Those are constants, too.
In addition to providing translations, you also need to localize. You need to remember about displayed dates and, sometimes, about including currency or measurement units. Here, too, order will be needed. Let’s make sure you have only one service processing DateTime objects and responsible for formatting them. If your project contains meaningfully written tests, there is a good chance that the object is not a global variable, singleton, or other static chimera, but a correctly injected service.
Assuming that you already have a Symfony project and have handled a mess of constants and duplicated responsibilities, translation and localization should be pretty easy. Let’s start by identifying the language versions of the site. To make things interesting, I suggest two language versions: Polish and English, and three localizations: Poland, United Kingdom and United States.
This means 3 available
locales: en_US, en_GB & pl_PL. Also, note the
locale format. A format compliant with
ISO 639-1 and
ISO 3166-1 alpha-2 standards has been chosen. The first part refers to the language, the second to the country. Sometimes, however, your
locale will only contain the language. When using external libraries and bundles, you must be prepared that code developers don’t always comply with applicable norms and standards. If this proves to be the case, your locale may not be compatible with those standards. You may be tempted to use a simple solution, which may lead you to mirroring other people’s mistakes. You shouldn’t do that, but it might happen because everyone likes to take shortcuts from time to time.
It’s time to choose the default locale. Go to
app/config/config.yml or parameters.yml (or
config/services.yaml file in Symfony 4) and add the following new parameter:
It will be responsible for the default location of the site.
The next step is to add the use of the newly added parameter to
config/packages/translation.yaml for Symfony 4):
These two settings are responsible for default localization.
Please remember that, in Symfony 4, the translator will not be available by default and it will have to be prompted by:
composer require translator.
Arranging Basic Translations
Once you set up the default translator, it’s time to choose the format of translations and populate the right files with them. I choose
yaml files for us. Now, let’s create a coherent concept of populating files with translations. Whatever it might be, it should be consistent and all project members should know where to look for and add new translations.
As an example of such an arrangement, I suggest:
This concept involves dividing translations according to project features. I assume that the translations will be sorted alphabetically, according to a full translation key, which will help avoid the accidental addition of similar translations. I suggest creating a set of a number of generic translations, common to different features of the project. I’m not going to make any suggestions as to what should be considered a separate feature from the translation point of view and what should be seen as a part of another feature. I’m not familiar with your systems.
However, I strongly urge you to make sure that translation names and their values are consistent with each other domain-wise. Try to have the same names in the translations and code and in the specifications provided by your business. This can save you some misunderstandings and many lines of unnecessary code.
Using the Translator
Now we’ve set up the translator and populated a translation file, it’s time for your project to start using the translations. Now let’s inject a translator service into each of your classes and enjoy well-translated text. However it’s good to set some limits as to where the translator will be needed, because it’s better not to have it elsewhere.
Personally, I see the translator as something very much related to the view. I would like my translator to be used only in twig templates. It will not always be possible though. If your application produces any APIs, even for the simplest AJAX, it may be sometimes necessary to use the translator outside the template.
What about the controller? Preferably not, I would prefer to use it in some handlers and sometimes in event listers, but for performance reasons, it is better not to use it here either.
With time, there will be more and more translations. Some will become obsolete, but they won’t always be removed from the translation files. Sooner or later, these files will be messy. But this is where Symfony comes to the rescue. The Symfony documentation contains the following example of a command:
php bin/console debug:translation en-EN AppBundle
or, for Symfony 4:
php bin/console debug:translation en-EN
This may be helpful for organizing translations, but don’t expect too much. The more complicated your project is and, consequently, the more errors you expect in your translations, the more false alarms this command is going to output.
It is also worth noting that, in our case, the application assumes two English language localizations. If your project is simple enough, the contents of
messages.en_GB.yaml may be the same, so instead of creating two files I’d suggest using a symbolic link.
Now let’s talk about the language switch, or rather the localization switch. The most traditional version assumes that localization information is kept in session and the listener is plugged into the
onKernelRequest event. However, you should remember that you are adding code that will be executed on every request. If you are after high performance, this may prove a damper.
A listener may look like this:
services.yml let’s put:
Make sure that the
autoconfigure options are set correctly.
The next thing to do is to create a controller action responsible for handling
In the example above, you store the locale in a cookie. The simplest locale switch would look like this:
You may also pass the current address to your controller action:
Then your controller action could take you to that address instead of the site/page.
You may also store your locale in a database, separately for each user. It shouldn’t be too much of a hassle to change the controller.
The correct choice of translation form depending on the plurality of the argument is a non-trivial problem. Polish is a good example. Let’s imagine a list with zero, one, two or five items. In each case the message will be different:
> Nie znaleziono wyników (No results found)
> Znaleziono 1 wynik (1 result found)
> Znaleziono 2 wyniki (2 results fund)
> Znaleziono 5 wyników (5 results found)
Languages have different rules. To address this, Symfony has a
To translate such a message, you just need to add to the template the following:
What’s more, you need to add translations and specify the exact ranges of values for which each value should be chosen:
This extremely simple solution almost solves the plurality problem. However, a careful reader will notice that it doesn’t always work correctly. For 22 results, the displayed message will be incorrect: *Znaleziono 22 wyników*.
In order to display this message correctly, the
transchoice method must be fed not the number of records but a number indicating which translation should be used and the actual number as an additional parameter. Your translation should look like this:
And depending on the number of results you need to use the right form:
In order to correctly select a translation, you will still need a bit of logic to take care of this. Regardless of where this logic is placed, it will be different for different languages. A method used to check which form should be used may look like this:
Correctly formatted and displayed dates may be useful for your project. The
n/j/y format is common in the US,
d M Y is prevalent in the UK, and
d.m.y in Poland. So let’s create a service responsible for formatting dates.
The next element to be translated is exceptions. PHP, Symfony, and Doctrine provide us with many different exceptions. Usually, only some of them should reach the user. The content of an exception concerning a problem along with the database name, user name, and password should not be carelessly displayed to the user. To filter which exceptions should and which shouldn’t be displayed, I suggest following the white list rule.
Create your own type of exception and display only these exceptions to the user:
Wherever you add an exception to your code, the contents of which are to be displayed to the user if an error occurs, use
ParametrizedException. The constructor of the exception is modified and takes a table of parameters that will be passed to translation. Below is an example of controller code to handle such an exception:
If you want to log some exceptions, it is useful to have the content log in the default language. Here’s the relevant exception handling code:
And this is the method responsible for translating to the default language:
The last thing I would like to discuss is translating the content of your database. The way to proceed depends on the kind of project. If your project is more like a blog with entries in different languages, where not every article is translated into other languages, I would recommend adding a language column to the content table. If you have some simple dictionaries in the database, for example, categories or tags, you can use a Doctrine extension: Doctrine2 behavioral extensions. It will allow you to translate entity columns on the fly. I don’t recommend this solution as it is not the fastest but it may prove sufficient.
For small tag or status tables not editable by the user, it may be a good idea to consider whether to move the storage of translated names to the project, either by storing only translation entries in a database or by adding all the values of those objects to the code.
For data editable by the user, it may be necessary to create an additional intermediate table for translations, or if you know the number of all languages of the site in advance and the number is small, it may be enough to add several columns to your table for translations. Sometimes the pages of different site localizations have no counterparts. Sometimes the computational effort associated with different site localizations may negatively affect your service. It is then worth considering whether it would be worthwhile to create a properly configured copy of the system for a new localization or language version.
We have briefly discussed a few localization problems to finally arrive at the suggestion of working around the problem by duplicating your system. Projects are different and require different solutions. Perhaps this solution will prove useful too. It is all up to you and the demands of your project.
At Unity Group, we share our knowledge during internal gatherings such as Unity Tech Talks* or Coders. If you want to join us as a PHP programmer, we encourage you to explore these job opportunities:
- PHP Developer (new shopping solution Spryker)
- PHP Developer (Product Information Management solutions)
* – we’re also running a public Open UTT series (PL), you may always meet us face-to-face there in Poland.
Stay up to date with our event calendar (PL) or follow us on social media!