Implement a "Read in your language" link with Symfony

Published on 2019-04-18 • Modified on 2019-04-19

In this post, we will see how to implement a "Read in your language" link in your pages. The goal will be to detect the user browser preferred language and show him a link if the current page is available in his language. Let's go! 😎


The code snippets you see in this post are used by this website, so you are sure "It works ™". This project uses the following components:

  • Symfony 4.3.4
  • PHP 7.2

i18n setup

The posts you are reading on this blog are all available in both English and French. First, let's have a look at the parameters related to i18n (internationalization) in this project:

# config/services.yaml
    locale: 'en'                         # default locale
    activated_locales: ['%locale%','fr'] # to get as an array
    locales_requirements: '%locale%|fr'  # to inject in controller routes requirements

We have three related parameters in our config/services.yaml file. The default locale that we will use as the reference. Then we build two parameters that will help us to get the list of available locales. Now, let's have a look at the main blog controller:

<?php declare(strict_types=1);

// src/Controller/BlogController.php

namespace App\Controller;

use App\Controller\Post\Post13Trait;
use App\Controller\Post\Post26Trait;
use App\Data\ArticleData;
use App\Entity\Article;
use App\Twig\Extension\SlugExtension;
use App\Utility\BreadcrumbsHelper;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;

 * @Route("/{_locale}/blog", name="blog_", requirements={"_locale"="%locales_requirements%"})

In the route annotation, we can see two main things. First the locale slug /{_locale} will prefix all URLs of the blog so it's easy to identify the locale of a given resource. (we have two separate trees: /en and /fr) Then, we have a requirement that limits the available locales. So in this case we inject the locales_requirements parameter we have seen in the previous section. It contains: en|fr. Try to access /es (spanish), you will get a 404 error as this locale is not available. (Javier, if you read me! 😁) Now, let's see how to build our switch language link.

Getting the user preferred language

The first thing to do is to detect the user language. Symfony already provides a function for that, it can be accessed through the Request object. Let's see the main application controller that handles the root URL of this website:

<?php declare(strict_types=1);

namespace App\Controller;

use App\Entity\ArticleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

 * App generic actions and locales root handling.
class AppController extends AbstractController
     * @Route("/", name="root")
    public function root(Request $request): Response
        $locale = $request->getPreferredLanguage($this->getParameter('activated_locales'));

        return $this->redirectToRoute('homepage', ['_locale' => $locale]);

As you can see, we are getting the preferred language of the user. We are passing the activated locales as an argument. If the user preferred language matches one of theses values then it will return it, otherwise it will return the default locale. Then a redirection is made to the locale homepage: /en or /fr.
This was an example to show how the preferred language function works. Now let's have a look on how to use this inside templates. You can use the following functions directly on the request object:

Variable Twig call Result Doc
The user preferred locale {{ app.request.preferredLanguage(activated_locales) }} en API
The current locale {{ app.request.locale }} en API
{{ app.request.defaultLocale }} en API

To avoid manually calling these functions, we will introduce a Twig extension to ease the job:

<?php declare(strict_types=1);

// src/Twig/Extension/LangExtension.php

namespace App\Twig\Extension;

use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;

 * Locale pre-computed values.
class LangExtension extends AbstractExtension implements GlobalsInterface
    private const LANG_FR = 'fr';

    protected $requestStack;
    protected $activatedLocales;
    protected $defaultLocale;

    public function __construct(RequestStack $requestStack, array $activatedLocales)
        $this->requestStack = $requestStack;
        $this->activatedLocales = $activatedLocales;
        $this->defaultLocale = $this->activatedLocales[0];

    public function getGlobals(): array
        $request = $this->requestStack->getMasterRequest();
        $route = $request ? $request->attributes->get('_route') : '';
        $locale = $request ? $request->getLocale() : $this->defaultLocale;
        $userLocale = $request ? $request->getPreferredLanguage($this->activatedLocales) : $this->defaultLocale;

        $globals = [
            'route' => $route,
            'locale' => $locale,
            'alt_locale' => $locale === $this->defaultLocale ? self::LANG_FR : $this->defaultLocale,
            'user_locale' => $userLocale,

        return $globals;

We introduce several pre-computed global variables: the current route, the current page locale, the alternative locale and the user preferred locale. The alternative locale variable will be the opposite of the current language as the blog only handles two. The user preferred locale uses the same call we used before, we have to pass the activated locales argument. Now, we can call these global variables in all our templates:

Twig global Twig call Result
The current route {{ route }} blog_show
The current locale {{ locale }} en
The alternate locale {{ alt_locale }} fr
The user preferred locale {{ user_locale }} en

Now that we have these variables, we can check if we should propose a link. It fact we will show it if the alternate language matches the user's locale. Let's look at the code:

{# templates/_read_in_your_lang.html.twig #}

{% trans_default_domain 'locale' %}

{% if alt_locale == user_locale or app.request.get('force') %}
    {% set alt_slug = ('slug_'~slug)|trans({}, 'blog', alt_locale) %}
    {% set alternate_url = path('blog_show', {'slug': alt_slug, '_locale': alt_locale}, alt_locale) %}
    <a name="alt-box"></a>
    <div class="card card-nav-tabs text-center">
        <div class="card-header card-header-primary">
            <p class="h4">{{ 'alt_lang_detected'|trans({}, 'locale', alt_locale) }}</p>

        <div class="card-body">
            <h4 class="card-title">&nbsp; {{ 'read_in_your_lang'|trans({}, 'locale', alt_locale)|raw }}</h4>
            <a href="{{ alternate_url }}" class="btn btn-primary">{{ 'read_in_your_lang_button'|trans({}, 'locale', alt_locale) }}</a>
{% endif %}

Some explanations. First we use a specific translation domain "locale". I am using this so I can show you the content of two translation files I am using. (see below). The main test is explicit. I have added an "or" condition to force the box to be displayed with a get parameter. After that, we set two variables: the first one will contain the translated slug of the article and the second one will contain the alternate URL. Note that we pass "alt_locale" as an argument to the trans Twig function because we don't want to use the current locale of the page but the users preferred one to encourage them to click. Then we create the link with some explanation texts. There are two cases: you can see the box, or (if you don’t see it) click on the link below. It will add the special get parameter to force its display.

Langue française detectée ! 🇫🇷

  Nous avons remarqué que votre navigateur utilise le français, voulez-vous lire cet article dans cette langue ?

Lire en français 🇫🇷

And as promised the two translations files used in this post:

# translations/locale.en.yaml
read_in_your_lang: >
  We noticed that your browser is using English.
  Do you want to read this post in this language?

alt_lang_detected: English language detected! 🇬🇧

read_in_your_lang_button: Read the english version 🇬🇧
# translations/
read_in_your_lang: >
  Nous avons remarqué que votre navigateur utilise le français,
  voulez-vous lire cet article dans cette langue ?

alt_lang_detected: Langue française detectée ! 🇫🇷

read_in_your_lang_button: Lire en français 🇫🇷

That's it! I hope you like it. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. 😁

 The Symfony doc

They gave feedback and helped me to fix errors and typos in this article, many thanks to: TimoBakx, Lynn, Pierstoval jmsche . 👍

» Published in "A week of Symfony 642" (15-21 April 2019).

» Call to action

Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment/contact me )

  • Report any error/typo.
  • Report something that could be improved.
  • Like and retweet!
  • Follow me on Twitter
  • Subscribe to the RSS feed.

Thank you for reading! And see you soon on Strangebuzz! 😉