7 ways Symfony helped us build an international site

Making an international site is hard, but Symfony made it easier.

A need to create an international site

Making an international site is hard. Even increasing the supported languages from one to two is a large burden on a software project.

At Pixo, we recently took on a site project whose audience is located in Western Europe. We needed to support French, German, Italian, Spanish, and English (but not the familiar U.S. English… we’re talking tea and crumpets English).

Depending on your software stack, this can have different degrees of difficulty. The software packages you use can hinder you or help you. The PHP framework Symfony has a reputation of being flexible and friendly to developers who use it. We’re already big fans of Symfony; after doing some research, we moved forward with it on our international project, and it ended up paying off big for us.

A screenshot of the Symfony Slack where members share what country they're from.
From the Symfony Slack. Look at all those international flags.

One benefit of Symfony for international work

One benefit of using Symfony for international work is that the community of developers who use it spans the globe. The project leader and primary maintainer of Symfony is Fabien Potencier, affectionately nicknamed FabPot by the community. FabPot is a French developer. Using software built by a French developer when you’re building a website for French users turns out to be a great advantage.

Here are seven things our team learned while working on this project:

1. Translations in templates, forms, and controllers

Our application has many forms, labels, buttons, and navigation text that appear to both admin users and anonymous site visitors. It turns out writing text for users who speak the same language as us is something we’ve taken for granted in the past.

Fortunately, Symfony has a translation module that makes it easier. You can define your translations in YAML files, a format easy to parse and edit by developers and non-developers. In your application code, assign each unique text an identifier that matches up in the translation files. Symfony automatically parses the translation and prints it in the template.

A screenshot of a Symfony translation template.

Debug toolbar

Symfony also sends information into the debug toolbar, which can notify you if translations are missing or a fallback is being used (such as en_GB falling back to en).

A screenshot of the Symfony debug toolbar.
We found a missing translation here.

The benefit of translation files

We ended up having about 80 lines for the front end and 150 lines for the admin area of the site. All of this is extra burden on the developers and has dubious value if you only need to support one language, but a benefit is that all the text that appears in the application can be reviewed and audited in one place: those translation files. Now your application text is abstracted away and no longer acts like many magic strings in your code.

2. Localized URL patterns

Symfony has a nice feature that allows you to customize the URL for different locales. For example, if you have a news story with the id 1, the URL for the story might look something like this for an English speaker:

https://example.com/en/news/1

And if you were French, the URL would look like this without modifications:

https://example.com/fr/news/1

The prefixed language has changed to “fr”, but the rest of the route pattern still has the English word “news”. That’s kind of icky for a French speaker who may copy and paste this URL to share somewhere else on the web.

In Symfony you can set up different route patterns for each locale, like:

https://example.com/en/news/1
https://example.com/fr/nouvelles/1
https://example.com/de/nachrichten/1

A screenshot of localized routing code.
Localized routing is awesome.

3. DoctrineExtensionsBundle

One of the most common Symfony bundles that gets installed along with the base Symfony project is the DoctrineExtensionsBundle, which adds many boilerplate features that are useful for CMSes and web applications. For example, the Timestampable extension is one of my favorites that I reuse all the time.

The one I discovered in this project was the Translatable extension, which creates a separate translation table in the SQL database. This table holds alternate text for each of your entity’s fields that can be translated. It also comes with code for swapping out the entity’s data with the translated text.

This turned out to be invaluable in a lot of ways. We could design our database like we normally would. Our entity tables would hold our content in a standard schema and in the default locale for the site (in our case French).

Then, an admin would enter German translations for a piece of content. The Translatable extension automatically puts this content in the translations table, leaving the original default entity untouched.

A screenshot of translation table code.
A peek at the translation table. Here we see some translations for Research and NewsStory entities.

Rendering a translated page

If a site visitor was looking at the German version (detectable by the /de_DE prefix), for example, we could use the Translatable extension to swap the entity’s content, then render like page as normal. The German speaker would see a page with the German translations shown.

4. Language and locale names with the Intl package

Symfony has an Intl package that can provide names for locales and languages. This was so useful for the developers in a lot of ways. We kept running into instances where we needed the French word for Germany and the German word for France and every other combination we could think of. All these different names were provided by the Intl package with static methods.

We made a “locale switcher” on the front end of the site for users to identify what locale they live in and language they wanted to read. Best practices told us that using text labels for locales and languages is better than using something visual like a flag. (A French flag might seem okay for the French language, but what about Switzerland or Canada, which both have a lot of French speakers?)

We could also print a locale’s name in the locale switcher in the language of that locale. This means that “German (Germany)” in the locale switcher always appears as “Deutsch (Deutschland)”. This will make sure that the menu option looks familiar to a German speaker even if they are on the French section of the site.

5. HttpFoundation, the Accept-Language header, and the Request object

Symfony’s HttpFoundation package is so useful for a lot of reasons but we have found another one while working on this project.

If it’s unclear what section of the site the visitor wants to see (for example, they hit the / route) we can use the header utilities provided in the HttpFoundation package to read the Accept-Language header and try to redirect them to matching section.

If not, we can set a default. For us, the default can be set by the admin. We can also do some smart checking, where a user looking for the English language can be redirected to English (Great Britain).

Another key thing about the Request object that might be kind of obscure: there is a method called “setLocale” on the object. If you set a locale here, then Symfony will treat that locale as the desired locale for the rest of the Request. The means rendering, translation, and other functions depending on a locale will read from whatever the developer has set in the Request object.

In the documentation, it’s carefully outlined how to set an Event Listener in your app to set the locale in the Request object early enough so that everything works as expected. Symfony provides a special attribute called “_locale” which you can use in your routes. We found it extremely useful.

public function onKernelRequest(RequestEvent $event)
{
    $request = $event->getRequest();

    if ($locale = $request->attributes->get('_locale'))
        $request->setLocale($locale);
}

Using RequestContext to find the current locale

We strongly recommend not setting the locale in the user’s session, but instead to rely on URL patterns and the _locale attribute.

We wrote many custom services that relied on “knowing” what locale the visitor wanted — but it ended up being trivial. There are two ways to get the current locale for the request: RequestStack and RequestContext. It seems like RequestContext is a bit safer. Inject it like below, and pull the _locale out using getParameter.

public function __construct(RequestContext $requestContext)
{
    $requestContext->getParameter('_locale');
}

6. SonataTranslationBundle

We used the popular SonataAdminBundle to create an admin dashboard for the maintainers of the site. Sonata has a plugin that integrates with the DoctrineExtensionsBundle to create forms for entering translations.

We found that it worked pretty well, but we wanted to make several changes to make the experience better for admins. The part we enjoy most about Sonata is how extensible and customizable it is. We changed the flags in the locale switcher to be bigger and use SVGs instead of bitmap images that come with Sonata. It was also easy to add some helpful text and tooltips when needed.

A screenshot of Sonata's list view and locale switcher.
Sonata’s list view includes a locale switcher.

7. IntlDateFormatter

This is more of a PHP feature but we are including it anyway. Dates are tough. Each region and culture could have their own way of formatting dates. Did you know that months are rarely capitalized in France? As a bunch of Americans, we had no idea.

We relied on using standard tools for internationalization. One of them, provided by PHP’s Intl extension, is a date formatter. Give it a DateTime object and ask it to format it for the given locale, and it will give you the standard format for that locale. It removes the headache of us having to research, create, and debug different formats for each of our supported locales.

Using Symfony for custom cases

While not every feature of Symfony is specific to internalization, we did run into many cases where we needed to do something custom or configure the framework to handle a use case differently, and we generally had a much easier time doing that using Symfony.

There are lots of other little things that Symfony does that makes internationalization easier. In general, Symfony follows best object-oriented practices in their code, such as the SOLID Principles.

To learn more, we strongly recommend reading the docs for the Translation Component.