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

Publié le 18/04/2019 • Mis à jour 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 mon kiki ! 😎

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 4.3.0-BETA1
  • PHP 7.2

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 :

# config/services.yaml
parameters:
    locale: 'en'                          # default locale
    activated_locales: ['%locale%','fr']  # to get as an aray
    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 :

<?php declare(strict_types=1);

// src/Controller/BlogController.php

namespace App\Controller;

use App\Controller\Post\Post13Trait;
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\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%"})
 */
class BlogController extends AbstractController
{

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\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]);
    }

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 API
La langue courante {{ app.request.locale }} fr API
{{ 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);

// 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;
    }
}

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' %}

{% 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/>
    <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>

        <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>
        </div>
    </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 🇬🇧

Chose promise chose due, les deux fichiers de traduction utilisés pour la boite contenant le lien de changement de langue :

# 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/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 detectée ! 🇫🇷

read_in_your_lang_button: Lire en français 🇫🇷

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 . 👍

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


» A vous de jouer !

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

  • Me remonter des erreurs ou typos.
  • Me remonter des choses qui pourraient être améliorées.
  • Likez et retweetez !
  • Suivez moi sur Twitter
  • Inscrivez-vous au flux RSS.

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

COil