Blog / Implement a translation system into your Laravel project with Inertia and Vue
8 min - 12 Feb 2024
TL;DR: How to quickly set up a translation system in a Laravel project with Inertia and Vue.
You will find the source code via this Github Repository. Find out more on Capsules, X or Bluesky.
The Laravel framework provides a default localization system, but it requires some additions for the proper functioning of a web tool using the Laravel Inertia and Vue technologies. This article addresses this topic.
Starting from a basic Laravel Inertia Vue Tailwind project, it is not yet adapted for internationalization. Proof of this is simply that the lang
folder is missing. The following steps establish the foundations of a multilingual tool.
First, add the default Laravel lang
folder to the template
project along with a translation file. For example, the language of Molière :
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é"
}
Three translations are accessible. The English translations, visible in the Vue components, are the keys, while the French translations are the values.
Add the different languages that will be part of the site in the file config/app.php
. In this article, it concerns en
and 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' ],
The new informations can now be injected into the shared data in Inertia's HandleInertiaRequest
middleware.
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
represents the current language.locales
represents the different available languages, as evidenced by config( 'app.available_locales' )
.translations
groups the available translations from the JSON files located in the lang
directory and linked to the current language. If no file exists, the returned translation array will be empty.Here's how to check the content of the shared data with the 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')
to identify the different translations. In this case, the other possibilities will return an empty array for the translations
.The web.php
file can be configured correctly now.
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' );
On the client side, specifically in Vue, you need to set up a composable
that takes into account the current locale
to display the correct translation found in the translations
array transmitted from the server.
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
returns the translation if it exists, otherwise, it returns the default English phrase.It is now possible to implement the translations added at the beginning of this article in the Welcome.vue
file by replacing "Capsules Codes" with "Hello world!" and importing 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>
It's time to implement the navigation bar, listing the different language choices, directly from the Welcome.vue
file.
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
returns the available languages via Inertia.index
represents the index following the current locale.language
represents the language following the current language. In this case, if we have fr
, language
will represent en
. If there is only one language, nothing is displayed. If there are three languages, each language will scroll one after the other.The language displayed in the top bar is, then, the language that is not used on the page. The goal now is to apply this choice to the server-side locale. The <a>
tag then sends a GET request to /fr
or /en
depending on the language
.
To allow the server to understand this and change the locale
via this process, a middleware is necessary : 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() ] );
allows adding the locale to the URL.The role of the SetLocale
middleware is to initialize or change the locale, as well as to add it to the URL.
This middleware can then be added to the Kernel
file. The position of the middleware is important but depends only on its usefulness. It is useful to place it before the PreventRequestsDuringMaintenance
maintenance middleware to also benefit from translation on the maintenance page during maintenance.
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,
...
];
...
A new prefix, a new route, and a fallback are necessary in the web.php
file. The new route aims to redirect to the previous route if it exists. Otherwise, it returns to the default route, 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}' )
as its name indicates, adds a prefix to each route. Here, it will be the locale.where(['locale' => '[a-zA-Z]{2}'])
, which is equivalent to two letters between a
and Z
.''
and '/'
are the same, it is necessary to redirect the initial route welcome
to 'welcome'
.App::setLocale( 'fr' );
can now be removed.Route::fallback( fn() => redirect()->route( 'welcome' ) );
indicates that if no route matches the given request, it will redirect to the 'welcome'
route. This is a way to handle errors and avoid a 404
page in this case.The translation system is now functional. 🎉
To avoid having to add the locale to every href reference, among other methods, another function can be added to the 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' )" />
Now that the routes have a prefix, they can be accessed from their 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
In case of maintenance, as indicated earlier, the locale is indeed assigned, but the translations will not be sent because the PreventRequestDuringMaintenance
middleware will be called before the HandleInertiaRequest
middleware. Therefore, you need to inject them manually into the 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
Glad this helped.