Reading time: 7 minutes
Translations and localizations in Symfony 3.4 and 4.0 (Part 1)
01 / 07 / 2019
Website translation is a very popular and light subject, perfect for an introduction to Symfony. In this article, I will try and structure this issue a bit – we will focus on Symfony, versions 3.4 and 4.0. The posted code pieces have been tested on both Symfony versions but the app developed while this article was being written uses Symfony 4.0.
Standard approach to the issue
Very frequently, at the designing stage, developers fail to consider the project’s future internationalization until one day there is a need for yet another language version. If good practices are followed when developing designs, translation should not be a major problem regardless of the chosen technology. Is this indeed the truth?
Any messages, tags, titles, and comments found in our templates should be easy to translate, whether the translation had been envisaged or not. Such data, being only constant, should be grouped together in one or several files – for instance in a translations file.
What about our database, though? Products, categories, or items entered into such a database should also be given some consideration in our translations. What approach should be taken in this case? Should every content table to be translated have a matching table containing translation only? Or should we use a single table to translate everything? Or maybe the problem can be resolved by some kind of records’ duplication in our tables? Let us consider this in further steps.
Translation vs localization
First of all, we should ask ourselves: What do we expect? Translation? Localization? After all, they are not equivalent. Localization is a broader issue – depending on the website version, we must not only translate the text, but also frequently change displayed date formats or, in case of a store, change the currency or make other changes to our design.
Before we begin implementing various language versions of our design, we need to make sure that a reasonable sequence was followed when undertaking the works on such versions. All of our constants – both numerical, the ones to soon become strings translated to various languages, as well as any other object with a fixed value, should be somehow grouped and, most importantly, separated from the rest of the code.
As much as keeping CSS rules in stylesheets and not embedded in HTML content should be obvious to us, so it should be for any labels and messages interwoven into our templates. Grouping them together in translations files should not be odd to us at all. But what about our exception messages? Did we remember about them or group them together? These are constants too.
Another problem, aside from translation, is localization. We must bear in minds data formats, sometimes a currency or units of measure. What is also necessary here, is the tidiness. We need to make sure that there is only one service processing DateTime objects and responsible for their formatting. If our design is covered with reasonably written tests, there is a good chance that the object is not a global variable, a singleton, or another static chimera, but a correctly injected service.
Assuming we already have a Symfony project and have dealt with the mess in our constants and various duplicated responsibilities, then the translation and localization should be relatively easy. To start with, let us identify the language versions available on our website. To make it more interesting, I suggest two language versions – Polish and English, and three localizations: Poland, the United Kingdom, and the United States.
Then, we get 3 available locales: ‘en_US‘, ‘en_GB‘ and ‘pl_PL‘. Please note the locale format here. The format used is compliant with ‘ISO 639-1‘ and ‘ISO 3166-1 alpha-2′ standards. The first part is responsible for the language, while the other one for the country. At times, however, our locale will only contain a language. When using external libraries and bundles, we must be aware of the fact that the authors of the code we use do not always comply with norms or standards. Then, our locale may be non-compliant with these standards. Simple solutions, tempting as they may be, may result in adapting our design to others’ mistakes. No-one should be doing this but, as we know, everyone likes to cut corners sometimes.
It is now time to choose the default locale. In the app/config/config.yml or parameters.yml file (or, for Symfony4, config/services.yaml file) add a new parameter:
It will be responsible for default localization of the website.
The next step is adding a use to our newly added parameter. For app/config/config.yml (in Symfony 4, it would be: config/packages/translation.yaml):
These two settings are responsible for default localization.
Please remember that in Symfony 4 the translator will not be available by default, it will have to be prompted by: composer require translator.
Arranging basic translations
Once the default translator is set, it is time to choose the translation format and fill them into relevant files. For formats, I chose yaml files for us. Now, let us prepare a coherent concept of filling them. Any concept will be good if it is coherent, and if all project members know where to look for and add new translations.
I can propose the following arrangement:
his concept involves dividing the translations as per functionalities of our design. Translations here are sorted alphabetically, with a complete translation key, to help avoid incidental addition of similar translations. What I suggest, is creating a set of some generic translations shared by various functionalities of the design. I have no intention whatsoever to make any suggestions as to what, for translation purposes, should be considered an independent functionality, and what a dependent part of another functionality. I do not know your systems.
My intention is to make you more aware of the need to make translation names and values consistent in terms of the domain. You should try and make sure these translations and code feature names identical to those contained in the specification provided by your business. This may help you avoid certain misunderstandings and many lines of unnecessary code.
Using a translator
Once the translator is configured and the translations file is filled, it is time for our design to start applying these translations. We can now inject the translation service to all our classes and enjoy a nicely translated text. But will all of it be nicely translated? It’s advisable to set certain boundaries as to where a translator is indeed indispensable, and better not use it in all other parts.
I, personally, consider translation service as something very closely associated with the view. I would like my translator to be used solely in twig templates. This will not always be possible, however. If our app provides any APIs, even for the simplest Ajax, using a translator outside the template may sometimes be necessary.
A controller? Better not… I would rather use it in some handlers and, occasionally, in event listeners – but even there, it preferably should be avoided for performance reasons.
Over time, we’re bound to be faced with more and more translations, with some becoming out-of-date but not always deleted from the translations files. Soon, or even sooner, we will be facing chaos in these files. This is when Symfony comes to the rescue. Symfony’s documentation features 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
It may be helpful in organizing our translations but do not expect too much. The more complex your design is, and thus the more mistakes you anticipate in your translations, the larger the number of false alarms you will encounter on this command’s output.
Note also that in our case the app has two localizations in English. If our design is simple enough, the content of messages.en_US.yaml and messages.en_GB.yaml may be identical; what I suggest then, is to use a symlink instead of creating two files.
Another issue we face is the language – or rather localization – selector. The most classic version retains information on the localization in the session and attaches the listener to the onKernelRequest event. Remember, however, that we are adding a code to be executed with every request. If performance is our priority, it may come at high cost.
An example of a listener may be as follows:
Meanwhile, we’ll place in our services.yml file:
Make sure that the autowire and autoconfigure options are configured correctly.
Now, we need to create the controller action responsible for handling ‘locale’ change:
In the example above, localization is stored in a cookie. The most basic localization selector would look as follows:
We could try and send the current address to our controller action:
Then, our controller action would be able to direct us to this address and not to the home page.
Our locales can also be stored in a database, for each user separately. Appropriate modification of the controller should not pose major problems.
That is all for this part. In next we will read about translations of plural forms, exceptions and database content.
At Unity Group, we share our knowledge at internal meetings such as Unity Tech Talks or Coders. Should you wish to join us as a PHP programmer, feel free to read the following job offer.