Blog / Implémenter un système de traduction dans son projet Laravel avec Inertia et Vue

Image used for article Implémenter un système de traduction dans son projet Laravel avec Inertia et Vue

Implémenter un système de traduction dans son projet Laravel avec Inertia et Vue




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 CapsulesX 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' ],
  • Cette configuration nous sera utile lors de l’implémentation des boutons de changement de langue.




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"
  ]
]
  • Modifier la langue depuis 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>
  • la constante computed locales retourne les langues disponibles via Inertia.
  • la constante computed index représente l’index suivant la locale actuelle.
  • la constante computed 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 );
    }
}
  • la première condition vérifie si la locale fait partie des locales disponibles.
  • la deuxième condition vérifie si la locale donnée est différente de la locale actuelle.
  • 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.
  • Cette locale doit respecter where( [ 'locale' => '[a-zA-Z]{2}' ] ) , l’équivalent de deux lettres entre a et Z.
  • Les route '' 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.
  • Il est important de ne pas indiquer de nom à la route de changement de locale, ou une boucle infinie pourrait avoir lieu dans sa redirection.




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 !

v1.4.0

Icône XIcône Github