Implémenter un lien "Lire dans votre langue" avec Symfony

Publié le 18/04/2019 • Actualisé le 19/04/2019

Dans cet article nous allons voir comment ajouter dans vos pages un lien "Lire dans votre langue". Le but va être de détecter la langue de l'utilisateur et de lui proposer un lien si le contenu qu'il est en train de consulter est disponible dans sa langue. C'est parti ! 😎

» Publié dans "Une semaine Symfonique 642" (du 15 au 21 avril 2019).

Configuration

Le code que vous voyez dans cet article est utilisé par ce site web, vous êtes donc sûr que "ça marche ™" et que le contenu n'est pas obsolète. Ce projet utilise les composants suivants :

  • Symfony 6.4.15
  • PHP 8.3

Paramétrage i18n

Les articles que vous pouvez lire sur ce blog sont à la fois disponibles en français et en anglais. Tout d'abord, jetons un coup d'œil aux paramètres relatifs à l'i18n. (internationalisation) dans ce projet :

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

Nous avons dans le fichier config/services.yaml, trois paramètres relatifs. La langue par défaut qui est utilisée comme référence. Ensuite nous construisons deux paramètres qui vont nous permettre de récupérer la liste des langues disponibles. Maintenant, regardons le code du contrôleur principal du blog :


#[Route(path: '/{_locale}/blog', name: 'blog_', requirements: ['_locale' => '%locales_requirements%'])]

Dans l'annotation "route", nous pouvons voir deux choses. Premièrement, le slug de la langue /{_locale} va préfixer toutes les URLs du blog, il est ainsi facile d'identifier la langue d'une ressource. (on a donc deux arbres différents : /en et /fr) Nous avons ensuite une restriction pour les langues disponibles. Dans ce cas nous injectons le paramètre locales_requirements que nous avons vu dans la section précédente. Elle contient en|fr. Essayez d'accéder à /es (espagnol), vous aurez une erreur 404 puisque cette langue n'est pas disponible (Javier, si tu me lis! 😁). Maintenant, voyons comment créer le lien pour changer de langue.

Récupérer la langue préférée de l'utilisateur

La première chose à faire est de détecter la langue du navigateur de l'internaute. Symfony fournit déjà une fonction à ce sujet, elle est disponible par l'objet Request. Voyons comment le contrôleur gérant la racine de ce site fonctionne :

<?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]);

Comme vous pouvez le voir, nous récupérons la langue préférée de l'utilisateur. Nous passons en paramètre la liste des langues activées. Si la langue préférée de l'utilisateur correspond à une des langues disponibles elle est retournée sinon la langue par défaut est utilisée. Ensuite une redirection est faite vers la page d'accueil de la langue détectée, à savoir : /en ou /fr.
C'était un exemple pour illustrer le fonctionnement de la fonction getPreferredLanguage. Maintenant voyons comment utiliser tout ceci à l'intérieur des templates Twig. On a accès aux fonctions suivantes par l'intermédiaire de l'objet Request.

Variable Appel Twig Résultat Doc
La langue de l'utilisateur {{ app.request.preferredLanguage(activated_locales) }} en_US API
La langue courante {{ app.request.locale }} fr API
La langue par défaut {{ app.request.defaultLocale }} en API

Afin d'éviter d'avoir à appeler ces fonctions manuellement, nous introduisons une extension Twig qui va nous permettre de simplifier le code :

<?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,
        ];
    }
}

Nous pré-calculons plusieurs variables globales Twig : la route courante, la langue de la page courante, la langue alternative (ce sera l'opposé de la langue de la pages comme ce blog en gère uniquement deux) et finalement, la langue préférée de l'utilisateur (c'est le même appel que nous avons vu précédemment). On doit passer à la fonction la liste des langues disponibles. Désormais, ces variables Twig sont disponibles dans tous nos templates :

Globale Twig Appel Twig Résultat
La route courante {{ route }} blog_show
La langue courante {{ locale }} fr
La langue alternative {{ alt_locale }} en
La langue de l'utilisateur {{ user_locale }} en

Maintenant que nous avons à disposition ces variables, il est désormais facile de proposer le lien de changement de langue. Nous allons l'afficher si la langue alternative correspond à la langue de l'utilisateur. Regardons le code de plus près :

{# 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">&nbsp; {{ '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 %}

Quelques explications. Tout d'abord nous utilisons un domaine de traduction spécifique "locale". J'utilise cette valeur afin d'isoler les clés de traduction nécessaires au lien que nous allons afficher. De plus, cela me permettra d'afficher le contenu de ces deux fichiers de traduction sans qu'ils soient pollués par d'autres clés n'ayant rien à voir avec cet article. (voir ci-dessous)
Le test principal est explicite. Il y a une condition "ou" additionnelle pour permettre de forcer l'affichage du lien même si les conditions nécessaires ne sont par remplies. Ensuite nous construisons deux variables, la première va contenir le slug traduit de l'article et la seconde l'URL complète du contenu alternatif. Veuillez noter que l'on passe "alt_locale" comme argument à la fonction Twig trans() car nous ne voulons pas utiliser la langue de la page courante mais la langue de l'utilisateur afin de l'inciter à cliquer. Ensuite, nous créons le lien avec quelques textes d'explication.


English language detected! 🇬🇧

  We noticed that your browser is using English. Do you want to read this post in this language?

Read the english version 🇬🇧 Close

Chose promise chose due, les deux fichiers de traduction utilisés pour la boite contenant le lien de changement de langue (cliquez ici).
# 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

J'espère que cet article vous a plu et qu'il vous sera utile. Comme toujours, feedback, like et retweets sont les bienvenus. (voir la boîte ci-dessous) A bientôt ! COil. 😁

 La doc Symfony

Ils m'ont donné leurs retours et m'ont aidé à corriger des erreurs et typos dans cet article, un grand merci à : TimoBakx, Lynn, Pierstoval, jmsche. 👍

  Travaillez avec moi !


A vous de jouer !

Ces articles vous ont été utiles ? Vous pouvez m'aider à votre tour de plusieurs manières : (cf le tweet à droite pour me contacter )

  • Me remonter des erreurs ou typos.
  • Me remonter des choses qui pourraient être améliorées.
  • Aimez et retweetez !
  • Suivez moi sur Twitter
  • Inscrivez-vous au flux RSS.
  • Cliquez sur les boutons Plus sur Stackoverflow pour me faire gagner des badges "annonceur" 🏅.

Merci d'avoir tenu jusque ici et à très bientôt sur Strangebuzz ! 😉

COil