Blog / Implémenter un système de traduction dans son projet Laravel avec Inertia et Vue
8 min - 12 Feb 2024
TL;DR: Comment mettre en place un système de traduction rapidement dans un projet Laravel Inertia Vue.
Vous trouverez le code source via ce Github Repository. Découvrez-en plus sur Capsules, X ou Bluesky.
Le framework Laravel met à disposition un système de localisation par défaut, mais celui-ci nécessite quelques ajouts pour le bon fonctionnement d’un outil web utilisant les technologies Laravel Inertia et Vue. Cet article aborde ce sujet.
Partant d’un projet Laravel Inertia Vue Tailwind basique, celui-ci n’est pas encore adapté pour l’internationalisation. Preuve en est qu’il manque tout simplement le dossier lang
. Les étapes suivantes mettent en place les bases d’un outil mulltilingue.
Dans un premier temps, ajouter le dossier Laravel par défaut lang
dans le projet template
ainsi qu’un fichier de traduction. La langue de Molière, par exemple :
cd template
mkdir lang
lang/fr.json
{
"Hello world!" : "Bonjour le monde!",
"This is a translation" : "Ceci est une traduction",
"Maintenance mode activated" : "Le mode maintenance est activé"
}
Trois traductions sont accessibles. Les traductions anglaises, visibles dans les components Vue, sont les clefs, tandis que les traductions françaises sont les valeurs.
Ajouter les différentes langues qui feront partie du site dans le fichier config/app.php
. Dans cet article il est question du en
et du fr
.
config/app.php
/*
|--------------------------------------------------------------------------
| Application Available Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the available locales that will be used
| by the translation service provider. You are free to set this array
| to any of the locales which will be supported by the application.
|
*/
'available_locales' => [ 'en', 'fr' ],
Les nouvelles informations peuvent maintenant être injectées dans les données partagées Inertia dans le middleware HandleInertiaRequest
.
app/Http/Middleware/HandleInertiaRequests.php
<?php
namespace App\Http\Middleware;
use Inertia\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\File;
class HandleInertiaRequests extends Middleware
{
public function share( Request $request ) : array
{
$file = lang_path( App::currentLocale() . ".json" );
return array_merge( parent::share( $request ), [
'csrf' => csrf_token(),
'locale' => App::currentLocale(),
'locales' => config( 'app.available_locales' ),
'translations' => File::exists( $file ) ? File::json( $file ) : []
] );
}
}
locale
représente la langue actuelle.locales
représente les différentes langues disponibles, comme l’atteste config( 'app.available_locales' )
.translations
regroupe les traductions disponibles dans le fichier json situés dans le dossier lang
et lié à la langue actuelle. Si aucun fichier n’existe, le tableau de traduction retournée sera vide.Voici comment vérifier le contenu des données partagées avec le client :
routes/web.php
<?php
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
App::setLocale( 'fr' );
Route::get( '/', fn() => dd( Inertia::getShared() ) );
array:5 [▼ // routes/web.php:10
"errors" => Closure() {#307 ▶}
"csrf" => "QTGHRkM83KysIS7htTNEWfZ9sC6Cs7U20i6kSSeF"
"locale" => "fr"
"locales" => array:2 [▼
0 => "en"
1 => "fr"
]
"translations" => array:2 [▼
"Hello world!" => "Bonjour le monde!"
"This is a translation" => "Ceci est une traduction"
]
]
App::setLocale( 'fr' )
pour identifier les différentes traductions. Dans ce cas-ci, les autres possibilités retournerons un tableau vide au niveau des translations
.Le fichier web.php
peut être configuré correctement.
routes/web.php
<?php
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
App::setLocale( 'fr' );
Route::get( '/', fn() => Inertia::render( 'Welcome' ) )->name( 'welcome' );
Du côté du client, et plus précisément de Vue, il faut mettre en place un composable
qui prendra en compte la locale
actuelle pour afficher la bonne traduction se trouvant dans le tableau translations
transmis depuis le serveur.
mkdir resources/js/composables
cd resources/js/composables
resources/js/composables/trans.js
import { usePage } from '@inertiajs/vue3';
export function useTrans( value )
{
const array = usePage().props.translations;
return array[ value ] != null ? array[ value ] : value;
}
useTrans
retourne la traduction, si elle est présente, sinon elle retourne la phrase anglaise par défaut.Il est maintenant possible d’implémenter les traductions ajoutées au début de cet article dans le fichier Welcome.vue
en remplaçant Capsules Codes
par Hello world!
et en important useTrans
.
resources/js/pages/Welcome.vue
<script setup>
import { useTrans } from '/resources/js/composables/trans';
import logotype from '/public/assets/capsules-logotype.svg';
</script>
<template>
<div class="w-screen h-screen flex flex-col items-center justify-center text-center">
<img class="w-24 h-24" v-bind:src="logotype" v-bind:alt="'Capsules Codes Logotype'">
<h1 class="mt-4 text-6xl font-bold select-none header-mode" v-text="useTrans( 'Hello world!' )" />
</div>
</template>
Il est temps d’implémenter la barre de navigation, listant les différents choix de langue, directement depuis le fichier Welcome.vue
resources/js/pages/Welcome.vue
<script setup>
import { computed } from 'vue';
import { usePage } from '@inertiajs/vue3';
import { useTrans } from '/resources/js/composables/trans';
import logotype from '/public/assets/capsules-logotype.svg';
const locales = computed( () => usePage().props.locales );
const index = computed( () => locales.value.findIndex( value => value == usePage().props.locale ) + 1 );
const language = computed( () => locales.value[ index.value % locales.value.length ] );
</script>
<template>
<div class="absolute h-12 w-full flex items-center justify-center">
<a v-if=" locales.length > 1 " class="rounded-md outline-none hover:bg-slate-50 text-sm font-medium" v-bind:href="`/${language}`" v-text="`/ ${language}`" />
</div>
<div class="w-screen h-screen flex flex-col items-center justify-center text-center">
<img class="w-24 h-24" v-bind:src="logotype" v-bind:alt="'Capsules Codes Logotype'">
<h1 class="mt-4 text-6xl font-bold select-none header-mode" v-text="useTrans( 'Hello world!' )" />
</div>
</template>
locales
retourne les langues disponibles via Inertia.index
représente l’index suivant la locale actuelle.language
représente la langue suivant la langue actuelle. Dans ce cas-ci, si nous avons du fr
, language
représentera en
. S’il n’y a qu’une seule langue, rien n’est affiché. Si il y a trois langues, chaque langue défilera l’une après l’autre.La langue affichée dans la barre supérieure est, alors, la langue qui n’est pas utilisée sur la page. Le but est maintenant d’appliquer ce choix à la locale
du côté du serveur. Le tag <a>
envoie alors une requête GET
/fr
ou /en
en fonction de language
.
Pour permettre au serveur de comprendre cela et changer de locale via ce procédé, un middleware est nécessaire : SetLocale
app/Http/Middleware/SetLocale.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
class SetLocale
{
public function handle( Request $request, Closure $next ) : Response
{
if( in_array( $request->segment( 1 ), config( 'app.available_locales' ) ) && $request->segment( 1 ) !== App::currentLocale() ) Session::put( 'locale', $request->segment( 1 ) );
App::setLocale( Session::get( 'locale', App::currentLocale() ) );
URL::defaults( [ 'locale' => App::currentLocale() ] );
return $next( $request );
}
}
URL::defaults( [ 'locale' => App::currentLocale() ] );
permet d’ajouter la locale à l’url.Le rôle du middleware SetLocale
est d’initialiser la locale ou la changer, ainsi que d’ajouter celle-ci à l’url.
Ce middleware peut ensuite être ajouté dans le fichier Kernel
. La position du middleware est importante. Mais ne dépend que de son utilité. Il est utile de le placer avant le middleware de maintenance PreventRequestsDuringMaintenance
pour aussi bénéficier de la traduction au niveau de la page de maintenance durant celle-ci.
app/Http/Kernel.php
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
protected $middleware = [
...
\App\Http\Middleware\SetLocale::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
...
];
...
Un nouveau préfixe, une nouvelle route ainsi qu’un fallback sont nécessaires dans le fichier web.php
. La nouvelle route a pour but de rediriger vers la route précédente, si elle existe, sinon retour vers la route par défaut welcome
.
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::prefix( '{locale}' )->where( [ 'locale' => '[a-zA-Z]{2}' ] )->group( function()
{
Route::get( '', fn() => redirect()->route( Route::getRoutes()->match( Request::create( URL::previous() ) )->getName() ) ?? 'welcome' );
Route::get( 'welcome', fn() => Inertia::render( 'Welcome' ) )->name( 'welcome' );
} );
Route::fallback( fn() => redirect()->route( 'welcome' ) );
Route::prefix( '{locale}' )
comme son nom l’indique, ajoute un prefix à chaque route. Ici il s’agira de la locale.where( [ 'locale' => '[a-zA-Z]{2}' ] )
, l’équivalent de deux lettres entre a
et Z
.''
et '/'
étants les mêmes, il faut alors rediriger la route intiale welcome
vers 'welcome'
App::setLocale( 'fr' );
peut dorénavant disparaître.Route::fallback( fn() => redirect()->route( 'welcome' ) );
indique que si aucune route ne colle avec la requête donnée, elle redirigera vers la route welcome
. C’est une manière de gérer les erreurs et éviter une page 404
dans ce cas-ci.Le système de traduction est maintenant fonctionnel. 🎉
Pour éviter de devoir ajouter la locale à chaque référence href
entre autres méthodes, une autre fonction peut venir s’ajouter au composable trans.js
: useRoute
resources/js/compsables/trans.js
import { usePage } from '@inertiajs/vue3';
...
export function useRoute( value = null )
{
return `/${usePage().props.lang}${value ?? ''}`;
}
import { useRoute, useTrans } from '~/composables/trans';
<a v-bind:href="useRoute( `/welcome` )"><span v-text="useTrans( 'Welcome' )" />
Maintenant que les routes ont un préfixe, on peut y accéder depuis leur closure.
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::prefix( '{locale}' )->where( [ 'locale' => '[a-zA-Z]{2}' ] )->group( function()
{
...
Route::get( 'translate', fn( string $locale ) => dd( __( "This is a translation", [], $locale ) ) );
...
} );
...
"Ceci est une traduction" // routes/web.php:13
En cas de maintenance, comme indiqué plus haut, la locale
est bien assignée mais les traductions ne seront pas envoyées car le middleware PreventRequestDuringMaintenance
sera appelé avant le middleware HandleInertiaRequest
. Il faut donc les injecter soi-même dans le Handler
.
app/exceptions/handler.php
use Symfony\Component\HttpFoundation\Response;
use Inertia\Response as InertiaResponse;
use Inertia\Inertia;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\File;
public function render( $request, Throwable $exception ) : Response | InertiaResponse
{
$response = parent::render( $request, $exception );
if( $response->status() === 503 )
{
Inertia::share( 'locale', App::currentLocale() );
Inertia::share( 'translations', File::exists( lang_path( App::currentLocale() . ".json" ) ) ? File::json( lang_path( App::currentLocale() . ".json" ) ) : [] );
return Inertia::render( 'Error' )->toResponse( $request )->setStatusCode( $response->status() );
}
return $response;
}
resources/js/pages/Error.vue
<script setup>
import { useTrans } from '/resources/js/composables/trans';
</script>
<template>
<div class="w-screen h-screen flex items-center justify-center text-center space-y-8">
<h1 class="text-6xl font-bold select-none header-mode" v-text="useTrans( 'Maintenance mode activated' )" />
</div>
</template>
php artisan down
Ravi d’avoir pu aider !