• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 11/05/2020

La localisation

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 en rendant notre petit blog disponible en français et en anglais.

Le principe

La façade Lang et la classe Translator

Laravel est équipé de la façadeLang pour générer des chaînes de caractères dans plusieurs langues. Mais maintenant 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 dossierIlluminate\Translation :

Le dossier pour les traductions

On y trouve très logiquement un provider pour faire toutes les initialisations. Un fichiercomposer.json pour les dépendances. Une classeFileLoader avec son interfaceLoaderInterface pour gérer les fichiers de traduction. Une classeArrayLoader pour charger les messages. Et enfin la classe principaleTranslator 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 :

Les dossiers de langage

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 exemplefr pour le Français. Et 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. Téléchargez le et copiez le dossier du français ici si vous ne l'avez pas encore ou si vous l'avez supprimé :

Le dossier du français

Vous disposez alors de deux versions linguistiques des messages de base de Laravel.

Voyons maintenant comment sont constitués ces fichiers. Prenons le plus léger (pagination.php) :

<?php

return [

	'previous' => '&laquo; Previous',
	'next'     => 'Next &raquo;',

];

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

<?php

return [

    'previous' => '&laquo; Précédent',
    'next'     => 'Suivant &raquo;',

];

On retrouve évidemment les mêmes clés avec des valeurs adaptées au langage.

Le fonctionnement

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

Avec :

<?php
Lang::get('pagination.previous');

Je vais obtenir :

« Previous

Donc la version anglaise. Comment faire pour obtenir la version française ? Regardez dans le fichierconfig/app.php :

<?php
'locale' => 'en',

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

<?php
'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éthodesetLocale :

<?php
App::setLocale('fr');

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

<?php
trans('pagination.previous');

On peut aussi vérifier qu'une clé existe avec la méthodehas :

<?php
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.

Le middleware

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 ? C'est un middleware qui va logiquement s'en occuper. Voici un schéma :

Gestion de la locale

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 à t'étape suivante.

Nous allons créer ce middleware avec Artisan :

php artisan make:middleware Locale

On le trouve dans le dossier des middlewares :

Le nouveau middleware

Je l'ai nomméLocale pour représenter sa fonction. Voici le code généré par défaut :

<?php

namespace App\Http\Middleware;

use Closure;

class Locale
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request);
    }
}

On va donc ajouter notre code :

<?php

namespace App\Http\Middleware;

use Closure;

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

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

        return $next($request);
    }
}

Le code reprend strictement le schéma vu ci dessus.

Il ne nous reste plus qu'à dire à Laravel de prendre en compte ce middleware dansapp/Http/Kernel.php :

<?php
protected $middlewareGroups = [
    'web' => [
        ...
        \App\Http\Middleware\Locale::class,
    ],

    ...
];

Les dates

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 formatjj/mm/aaaa alors que les Américains utilisent le formatmm/jj/aaaa.  Il va donc falloir faire quelque chose pour que 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èlePost. Mais pour généraliser la chose on va imaginer qu'on peut avoir besoin de la même chose pour d'autres 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 :

Le presenter pour les dates

Avec ce code :

<?php 

namespace App\Presenters;

use Carbon\Carbon;

trait DatePresenter
{

	public function getCreatedAtAttribute($date)
	{
		return $this->getDateFormated($date);
	}

	public function getUpdatedAtAttribute($date)
	{
		return $this->getDateFormated($date);
	}

	private function getDateFormated($date)
	{
		return Carbon::parse($date)->format(config('app.locale') == 'fr' ? 'd/m/Y' : 'm/d/Y');
	}

}

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

Il ne nous reste plus qu'à utiliser ce trait dans le modèlePost :

<?php 

namespace App;

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

class Post extends Model
{
	use DatePresenter;

	protected $fillable = ['titre','contenu','user_id'];

	public function user() 
	{
		return $this->belongsTo('App\User');
	}

	public function tags()
	{
		return $this->belongsToMany('App\Tag');
	} 

}

Route et contrôleur

L'utilisateur doit pouvoir changer la langue s'il le désire. On va donc ajouter une route :

<?php
Route::get('language', 'PostController@language');

Et voici le contrôleur modifié :

<?php

namespace App\Http\Controllers;

use App\Repositories\PostRepository;
use App\Repositories\TagRepository;
use App\Http\Requests\PostRequest;

class PostController extends Controller
{

    protected $postRepository;

    protected $nbrPerPage = 4;

    public function __construct(PostRepository $postRepository)
	{
		$this->middleware('auth', ['except' => ['index', 'indexTag', 'language']]);
		$this->middleware('admin', ['only' => 'destroy']);

		$this->postRepository = $postRepository;
	}

	public function index()
	{
		$posts = $this->postRepository->getWithUserAndTagsPaginate($this->nbrPerPage);
		$links = $posts->render();

		return view('posts.liste', compact('posts', 'links'));
	}

	public function create()
	{
		return view('posts.add');
	}

	public function store(PostRequest $request, TagRepository $tagRepository)
	{
		$inputs = array_merge($request->all(), ['user_id' => $request->user()->id]);

		$post = $this->postRepository->store($inputs);

		if(isset($inputs['tags'])) 
		{
			$tagRepository->store($post, $inputs['tags']);
		}

		return redirect(route('post.index'));
	}

	public function destroy($id)
	{
		$this->postRepository->destroy($id);

		return redirect()->back();
	}

	public function indexTag($tag)
	{
		$posts = $this->postRepository->getWithUserAndTagsForTagPaginate($tag, $this->nbrPerPage);
		$links = $posts->render();

		return view('posts.liste', compact('posts', 'links'))
		->with('info', trans('blog.search') . $tag);
	}

	public function language()
	{
		session()->set('locale', session('locale') == 'fr' ? 'en' : 'fr');

		return redirect()->back();
	}

}

On voit une nouvelle fonctionlanguage. D'autre part la fonctionindexTag a été modifiée pour tenir compte des langues.

Donc avec l'url

.../language

On va pouvoir passer d'une langue à l'autre.

La localisation

Il nous faut aussi créer les fichiers de localisation :

Les fichiers de localisation

Avec ce contenu pourresources/lang/en/blog.php  :

<?php

return [
    'site' => 'My nice site',
	'title' => 'My nice blog',
	'creation' => 'Create an article',
	'login' => 'Login',
	'logout' => 'Logout',
	'delete' => 'Delete this post',
	'confirm' => 'Really delete this post ?',
	'on' => 'on',
	'search' => 'Results for tag : '
];

Et celui-ci pour resources/lang/fr/blog.php :

<?php

return [
    'site' => 'Mon joli site',
	'title' => 'Mon joli blog',
	'creation' => 'Créer un article',
	'login' => 'Se connecter',
	'logout' => 'Déconnexion',
	'delete' => 'Supprimer cet article',
	'confirm' => 'Vraiment supprimer cet article ?',
	'on' => 'le',
	'search' => 'Résultats pour la recherche du mot-clé : '
];

Les vues

Il ne nous reste plus qu'à modifier les vues pour que ça fonctionne. D'abord le template (views/template/php) :

<!DOCTYPE html>
<html lang="fr">
    <head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>{{ trans('blog.site') }}</title>
		{!! Html::style('https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css') !!}
		{!! Html::style('https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css') !!}
		<!--[if lt IE 9]>
			{{ Html::style('https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js') }}
			{{ Html::style('https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js') }}
		<![endif]-->
		<style> textarea { resize: none; } </style>
	</head>
	<body>
		<header class="jumbotron">
			<div class="container">
				<h1 class="page-header">{!! link_to_route('post.index', trans('blog.title')) !!}</h1>
				@yield('header')
			</div>
		</header>
		<div class="container">
			@yield('contenu')
		</div>
	</body>
</html>

Il est modifié pour la traduction du titre du site dans le header :

<title>{{ trans('blog.site') }}</title>

Et le titre/lien sur la page :

<h1 class="page-header">{!! link_to_route('post.index', trans('blog.title')) !!}</h1>

Et voici le blog transformé (views/posts/liste.blade.php) :

@extends('template')

@section('header')
    <div class="btn-group pull-right">
		{!! link_to('language', session('locale') == 'fr' ? 'English' : 'Français', ['class' => 'btn btn-primary']) !!}
    	@if(Auth::check())
    		{!! link_to_route('post.create', trans('blog.creation'), [], ['class' => 'btn btn-info']) !!}
			{!! link_to('logout', trans('blog.logout'), ['class' => 'btn btn-warning']) !!}
		@else
			{!! link_to('login', trans('blog.login'), ['class' => 'btn btn-info pull-right']) !!}
		@endif
	</div>
@endsection

@section('contenu')
	@if(isset($info))
		<div class="row alert alert-info">{{ $info }}</div>
	@endif
	{!! $links !!}
	@foreach($posts as $post)
		<article class="row bg-primary">
			<div class="col-md-12">
				<header>
					<h1>{{ $post->titre }}
						<div class="pull-right">
							@foreach($post->tags as $tag)
								{!! link_to('post/tag/' . $tag->tag_url, $tag->tag,	['class' => 'btn btn-xs btn-info']) !!}
							@endforeach
						</div>
					</h1>
				</header>
				<hr>
				<section>
					<p>{{ $post->contenu }}</p>
					@if(Auth::check() and Auth::user()->admin)
						{!! Form::open(['method' => 'DELETE', 'route' => ['post.destroy', $post->id]]) !!}
							{!! Form::submit(trans('blog.delete'), ['class' => 'btn btn-danger btn-xs ', 'onclick' => 'return confirm(\'' . trans('blog.confirm') . '\')']) !!}
						{!! Form::close() !!}
					@endif
					<em class="pull-right">
						<span class="glyphicon glyphicon-pencil"></span> {{ $post->user->name . ' ' . trans('blog.on') . ' ' . $post->created_at }}
					</em>
				</section>
			</div>
		</article>
		<br>
	@endforeach
	{!! $links !!}
@endsection

Les modifications portent sur tous les textes. Je vous laisse analyser le code. Le résultat est qu'en français rien ne change par rapport à ce que nous avions. Par contre quand on passe en anglais en cliquant sur le bouton "English" :

Le bouton de changement de langue

Déjà les boutons changent d'aspect :

Les boutons en anglais
Les boutons en anglais

Le titre de la page est en anglais :

Le titre de la page en anglais
Le titre de la page en anglais

Le titre/lien est en anglais :

Le titre en anglais
Le titre en anglais

Les dates sont formatées correctement :

Date formatée
Date formatée

Le bouton de suppression est aussi en anglais :

Le bouton de suppression
Le bouton de suppression

De même que le message :

Le message d'alerte en anglais
Le message d'alerte en anglais

Le message de la recherche par tag est aussi adapté :

Le message de la recherche par tag
Le message de la recherche par tag

Noms des contrôles

Il faudrait aussi traiter la vue de création d'un article mais c'est exactement le même principe. Il y a toutefois un élément à prendre en compte, c'est le nom des contrôles de saisie. Vous vous rappelez qu'on les a appeléstitre,contenu ettags. 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 anglaise "The titre field is required.". Alors comment faire ? 

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

<?php
'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. Donc dans notre situation, étant donné que nous avons francisé le nom des contrôles, il faut ajouter la version anglaise dans le fichier resources/lang/en/validation.php :

<?php
'attributes' => [
    "titre" => "Title",
    "contenu" => "Content",
],

Evidemment si on avait prévu des noms anglais, ce que l'on fait en général, il aurait fallu faire l'inverse, et prévoir des appellations françaises dans le fichier resources/lang/fr/validation.php.

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

Exemple de certificat de réussite
Exemple de certificat de réussite