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! π
» Published in "A week of Symfony 642" (15-21 April 2019).
Configuration
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 6.4.15
- PHP 8.3
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:
parameters:
# i18n βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
locale: 'en' # default locale
activated_locales: [] # This parameter is dynamically injected in src/Kernel.php to get the value of the "framework.translator.enabled_locales" parameter.
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:
#[Route(path: '/{_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\Repository\ArticleRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* App generic actions and locales root handling.
*/
final class AppController extends AbstractController
{
public function __construct(
public readonly ArticleRepository $articleRepository,
) {
}
#[Route(path: '/', 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:
To avoid manually calling these functions, we will introduce a Twig extension to ease the job:
<?php
declare(strict_types=1);
namespace App\Twig\Extension;
use App\Enum\Locale;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
/**
* Locale pre-computed values.
*/
final class LangExtension extends AbstractExtension implements GlobalsInterface
{
private RequestStack $requestStack;
/**
* @var array<int,string>
*/
private array $activatedLocales;
private string $defaultLocale;
/**
* @param array<int,string> $activatedLocales
*/
public function __construct(RequestStack $requestStack, array $activatedLocales)
{
$this->requestStack = $requestStack;
$this->activatedLocales = $activatedLocales;
$this->defaultLocale = $this->activatedLocales[0];
}
/**
* @return array<string,mixed>
*/
public function getGlobals(): array
{
$request = $this->requestStack->getMainRequest();
if (!$request instanceof Request) {
return [];
}
$route = $request->attributes->get('_route');
$locale = $request->getLocale();
$userLocale = $request->getPreferredLanguage($this->activatedLocales);
return [
'route' => $route,
'locale' => $locale,
'alt_locale' => $locale === $this->defaultLocale ? Locale::FR->value : $this->defaultLocale,
'user_locale' => $userLocale,
];
}
}
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' %}
<a id="alt-box"></a>
{% if (article.id == 21 or (not strike)) and article.hasLanguage(alt_locale) %}
<div v-show="showLangSwitcher" style="display:none">
{% 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) %}
<br/>
<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>
<div class="card-body">
<p class="card-title h4"> {{ 'read_in_your_lang'|trans({}, 'locale', alt_locale)|raw }}</p>
<a href="{{ alternate_url }}" class="btn btn-primary">{{ 'read_in_your_lang_button'|trans({}, 'locale', alt_locale) }}</a>
<a v-on:click.prevent="disableLangSwitcher" class="btn" href="#">{{ 'close'|trans({}, 'locale', alt_locale) }}</a>
</div>
</div>
{% endif %}
</div>
{% 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 user's 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.
You don't see the link box because your lang is en and the current article locale in en. (or you don't have the ?force=1
get parameter in the url). To see it, click on the link below.
And as promised the two translations files used in this post (click here to see).
# 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 π¬π§
close: Close
# translations/locale.fr.yaml
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 dΓ©tectΓ©e ! π«π·
read_in_your_lang_button: Lire en franΓ§ais π«π·
close: Fermer
That's it! I hope you like it. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. π
They gave feedback and helped me to fix errors and typos in this article, many thanks to TimoBakx, Lynn, Pierstoval, jmsche. π
Call to action
Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment or to contact me )
- Report any error/typo.
- Report something that could be improved.
- Like and retweet!
- Follow me on Twitter Follow me on Twitter
- Subscribe to the RSS feed.
- Click on the More on Stackoverflow buttons to make me win "Announcer" badges π .
Thank you for reading! And see you soon on Strangebuzz! π
[π¬π§] This is my 3rd #Symfony blog post of the year. This time we are talking about #i18n and we look how to use the detected user preferred language. π«π·π¬π§ https://t.co/9Nn015JKhR Comments and retweets are welcome! π Annual goal: 3/12 (25%) #php #blog #blogging
— Vernet LoΓ―c (@C0il) 18 avril 2019