Lorsqu’on crée un site c’est souvent dans une optique multi-langages. On parle alors d’internationalisation (i18n) et de localisation (L10n). L’internationalisation consiste à préparer une application pour la rendre potentiellement adaptée à différents langages.  La localisation consiste quant à elle à ajouter un composant spécifique à une langue.

C’est un sujet assez complexe qui ne se limite pas à la traduction des textes mais qui impacte aussi la représentation des dates, la gestion des pluriels, parfois la mise en page…

Qu’a à nous proposer Laravel dans ce domaine ? Nous allons le voir avec l’application d’exemple.

i18n parce qu’il y a 18 lettres entre le « i » et le « n » de internationalisation et L10n parce qu’il y a 10 lettres entre le « L » et le « n » de Localisation.

Le principe

La façade Lang et la classe Translator

Laravel est équipé de la façade Lang pour générer des chaînes de caractères dans plusieurs langues. Mais vous savez qu’une façade cache une classe.

Laquelle dans notre cas ?

Toutes les classes qui sont concernées par la traduction se trouvent dans le dossier Illuminate\Translation :

On y trouve très logiquement :

  • un service provider pour faire toutes les initialisations,
  • un fichier composer.json pour les dépendances,
  • une classe FileLoader avec son interface LoaderInterface pour gérer les fichiers de traduction,
  • une classe ArrayLoader pour charger les messages,
  • la classe principale Translator qui est mise en action par la façade.

Les fichiers de langage

On a déjà parlé des fichiers de langage. Lorsque Laravel est installé on a cette architecture :

Il n’est prévu au départ que la langue anglaise (en) avec 4 fichiers. Pour ajouter une langue il suffit de créer un nouveau dossier, par exemple fr pour le Français.

Il faut évidemment avoir le même contenu comme nous allons le voir.

Vous n’allez pas être obligé de faire toute cette traduction vous-même ! Certains s’en sont déjà chargés avec ce package. Nous l’avons déjà utilisé dans ce cours. 

L’application d’exemple embarque 3 langues : anglais, français et portugais (Brésil) :

Les fichiers présents sont tous constitués exactement de la même manière. Prenons le plus léger pagination.php, pour l’anglais on a :

return [
	'previous' => '« Previous',
	'next'     => 'Next »',
];

On voit qu’on se contente de renvoyer un tableau avec des clés et des valeurs. On ne peut pas faire plus simple ! Si on prend le même fichier en version française :

return [
    'previous' => '« Précédent',
    'next'     => 'Suivant »',
];

On retrouve évidemment les mêmes clés avec des valeurs adaptées au langage. Et c’est la même chose pour le portugais :

return [
    'previous' => '« Anterior',
    'next'     => 'Próxima »',
];

Le fonctionnement

On dispose de quelques méthodes pour tester et récupérer ces valeurs.

Avec :

Lang::get('pagination.previous');

je vais obtenir :

« Previous

Donc la version anglaise.

Comment faire pour obtenir la version française ?

Regardez dans le fichier config/app.php :

'locale' => 'en',

C’est ici qu’est fixé le langage en cours. Changez la valeur :

'locale' => 'fr',

Maintenant avec le même code vous allez obtenir :

« Précédent

Evidemment on va pouvoir changer cette valeur dans le code avec la méthode setLocale :

App::setLocale('fr');

Comme vous aurez de multiples endroits dans le code où vous allez récupérer des valeurs il existe un helper :

trans('pagination.previous');

On peut aussi vérifier qu’une clé existe avec la méthode has :

Lang::has('pagination.previous');

Ce sont les méthodes fondamentales qui seront suffisantes pour le présent chapitre, vous pouvez trouver des compléments d’information dans la documentation.

Un middleware ou un événement

Quand un utilisateur choisit une langue il faut s’en rappeler, on va donc utiliser la session pour mémoriser son choix.

Lorsqu’un utilisateur débarque sans avoir encore choisi il faut lui présenter une langue, mais laquelle ?

Il est possible d’aller chercher l’information de la langue utilisée au niveau de la requête.

Maintenant la question est : où allons nous placer le code pour gérer tout ça ?

Par exemple un middleware pourrait s’en occuper. Voici un schéma :

Lorsque la requête arrive elle est prise en charge par le middleware. Là on va regarder si l’utilisateur a une session active avec une localisation, si c’est le cas on applique cette locale et on envoie la requête à l’étape suivante. Si ce n’est pas le cas on regarde dans le header de la requête la langue envoyée par le navigateur, on la mémorise dans la session et on l’applique. On envoie alors la requête à l’étape suivante.

Ce middleware aurait ce code :

<?php

namespace App\Http\Middleware;

use Closure;

class Locale
{
    protected $languages = ['en','fr','pt-BR'];
    
    public function handle($request, Closure $next)
    {
        if(!session()->has('locale')) {
            session()->put('locale', $request->getPreferredLanguage($this->languages));
        }

        app()->setLocale(session('locale'));

        return $next($request);
    }
}

Ce n’est pas ce qui a été réalisé dans l’application d’exemple. On y utilise un événement (nous verrons les événements dans un prochain chapitre) pour détecter l’arrivée d’un utilisateur et on utilise alors un service :

On a dans ce service un code proche de celui vu ci-dessus pour le middleware :

<?php

namespace App\Services;

use Request;

class Locale
{
    /**
     * Set the locale.
     *
     * @return void
     */
    public static function setLocale()
    {
        if (!session()->has('locale')) {
            session()->put('locale', Request::getPreferredLanguage(config('app.languages')));
        }

        app()->setLocale(session('locale'));
    }
}

La différence c’est que les langues sont définies dans la configuration, ce qui est plus logique :

'languages' => ['en', 'fr', 'pt-BR'],

Le changement de langue

Dans l’application d’exemple on peut choisir librement sa langue dans le menu si le fonctionnement automatique ne convient pas :

La liste déroulante est générée avec ce code :

<li class="dropdown">
    <a data-toggle="dropdown" class="dropdown-toggle" href="#"><img width="32" height="32" alt="{{ session('locale') }}"  src="{!! asset('img/' . session('locale') . '-flag.png') !!}" />  <b class="caret"></b></a>
    <ul class="dropdown-menu">
    @foreach ( config('app.languages') as $user)
        @if($user !== config('app.locale'))
            <li><a href="{!! url('language') !!}/{{ $user }}"><img width="32" height="32" alt="{{ $user }}" src="{!! asset('img/' . $user . '-flag.png') !!}"></a></li>
        @endif
    @endforeach
    </ul>
</li>

Et la route est celle-ci :

Route::get('language/{lang}', 'LanguageController')
->where('lang', implode('|', config('app.languages')));

Au passage remarquez la contrainte de route pour le paramètre qui doit être l’une des langues présentes dans la configuration.

On aboutit dans le contrôleur LanguageController :

<?php

namespace App\Http\Controllers;

class LanguageController extends Controller
{
    /**
     * Change language.
     *
     * @param  string $lang
     * @return \Illuminate\Http\Response
     */
    public function __invoke($lang)
    {
        $language = in_array($lang, config('app.languages')) ? $lang : config('app.fallback_locale');
        
        session()->set('locale', $language);

        return back();
    }
}

Ici on mémorise en session la nouvelle langue

Remarquez l’utilisation de la méthode __invoque pour un contrôleur qui ne possède qu’une méthode.

Les dates

Le presenter

Il ne vous a sans doute pas échappé que les dates ne sont pas présentées de la même manière selon les langues utilisées. En particulier entre le français et l’anglais.

En France nous utilisons le format jj/mm/aaaa alors que les Américains utilisent le format mm/jj/aaaa.

Il va faut donc faire quelque chose pour que par exemple les dates de création des articles apparaissent au bon format dans chaque cas.

Comme ce formatage doit être appliqué pour toutes les dates la façon la plus simple et élégante de procéder est de créer un « accessor » sur la propriété. Ainsi chaque fois qu’on va extraire une date à partir du modèle on va le formater au passage. On pourrait faire ceci directement dans notre modèle Post. Mais on va  généraliser la chose, parce qu’on va aussi en avoir besoin dans d’autre modèles.

Dans ce genre de situation la création d’un trait est pertinente pour introduire une fonctionnalité sans passer par l’héritage.

On va utiliser une version simplifiée du Design pattern Décorateur (decorator). Dans Laravel on va appeler cela un « Model Presenter ».

On va créer un dossier pour les presenters et créer celui dont nous avons besoin :

Avec ce code :

<?php

namespace App\Presenters;

use Carbon\Carbon;

trait DatePresenter
{
    /**
     * Format created_at attribute
     *
     * @param \Carbon\Carbon  $date
     * @return string
     */
    public function getCreatedAtAttribute($date)
    {
        return $this->getDateTimeFormated($date);
    }

    /**
     * Format updated_at attribute
     *
     * @param \Carbon\Carbon  $date
     * @return string
     */
    public function getUpdatedAtAttribute($date)
    {
        return $this->getDateTimeFormated($date);
    }

    /**
     * Format date
     *
     * @param \Carbon\Carbon  $date
     * @return string
     */
    protected function getDateTimeFormated($date)
    {
        return Carbon::parse($date)->format(config('app.locale') != 'en' ? 'd/m/Y H:i:s' : 'm/d/Y H:i:s');
    }
}

On voit que selon la valeur de la locale on va appliquer le formatage adapté de la date extraite.

Remarquez qu’on utilise la date et aussi l’heure, selon les vues on aura besoin seulement de la date, sans l’heure, il faudra alors en tenir compte.

Il ne nous reste plus qu’à utiliser ce trait dans les modèles concernés, par exemple Post :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Presenters\DatePresenter;
...

class Post extends Model
{
    use DatePresenter;

Les vues

Une fois que tout ça est en place l’utilisation dans les vues est simple. Prenons par exemple la vue pour la gestion des articles :

Le contenu du tableau est généré par la vue resources/views/back/blog/table.blade.php.

Les boutons doivent avoir un texte qui correspond à la langue. Par exemple pour le bouton d’édition on a :

<td>{!! link_to_route('blog.edit', trans('back/blog.edit'), [$post->id], ['class' => 'btn btn-warning btn-block']) !!}</td>

On utilise l’helper trans pour traduite le titre du bouton qui sera dans la langue actuelle de l’utilisateur.

D’autre part la date doit avoir le bon format, ici elle est au format anglais, voici le code :

<td>{{ $post->created_at }}</td>

Vous voyez qu’on a rien de particulier à faire puisque tout se passe dans le modèle avec le trait mis en place.

Si on change la langue pour le français les textes et les dates s’adaptent automatiquement :

Les noms pour la validation

Il y a un autre élément à prendre en compte pour la traduction, c’est le nom des contrôles de saisie. Ces noms ne sont pas visibles tant qu’il n’y a pas de souci de validation, ils sont alors transmis dans le texte de l’erreur.

‌On voit mal arriver en langue française « Le champ name est obligatoire.. ». Alors comment faire ?

Si vous regardez dans le fichier resources/lang/fr/validation.php vous trouvez ce tableau :

'attributes' => [
    "name" => "Nom",
    "username" => "Pseudo",
    ...
],

Ici on fait correspondre le nom d’un attribut avec sa version linguistique pour justement avoir quelque chose de cohérent au niveau du message d’erreur.

Le but de ce chapitre est de montrer comment localiser l’interface d’une page web. S’il s’agit d’adapter le contenu selon la langue c’est une autre histoire et il faut alors intervenir au niveau de l’url. En effet un principe fondamental du web veut que pour une certaine url on renvoie toujours le même contenu.

En résumé

  • Laravel possède les outils de base pour la localisation.
  • Il faut créer autant de fichiers de localisation qu’on a de langues à traiter.
  • La localisation peut s’effectuer au niveau d’un middleware.
  • Le formatage des dates peut s’effectuer facilement avec un « presenter ».
  • Il faut prévoir la version linguistique des attributs.
  1. tamplan

    Bonjour,

    Tout d’abord merci pour votre site très complet sur Laravel, bon boulot !

    Je me pose une question concernant la gestion des formats de date selon la langue.
    Pourquoi ne pas utiliser un fichier de langue nommé par exemple ‘date.php’ dans chaque dossier de langue (en, fr …) contenant les clés ‘date’ et ‘date_time’ :

    Pour fr
    ‘date’ => ‘d/m/Y’,
    ‘date_time’ => ‘d/m/Y H:i:s’,

    Pour en
    ‘date’ => ‘m/d/Y’,
    ‘date_time’ => ‘m/d/Y H:i:s’,

    L’a prise en compte d’une nouvelle langue n’obligerait pas à adapter le code de la fonction getDateTimeFormated.

    Voyez-vous une raison particulière de ne pas utiliser ce genre de solution ?

    D’avance merci pour votre réponse.

    Cordialement

Laisser un commentaire