Implement a translation system into your Laravel project with Inertia and Vue
Photo by Ave Calvar on Unsplash
How to quickly set up a translation system in a Laravel project with Inertia and Vue.
A sample Laravel project can be found on this Github Repository. Find out more on Capsules or X.
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' ],
- This configuration will be useful to us during the implementation of the language change buttons.
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 byconfig( 'app.available_locales' )
. -
translations
groups the available translations from the JSON files located in thelang
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"
]
]
- Modify the language using
App::setLocale('fr')
to identify the different translations. In this case, the other possibilities will return an empty array for thetranslations
.
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>
- The computed constant
locales
returns the available languages via Inertia. - The computed constant
index
represents the index following the current locale. - The computed constant
language
represents the language following the current language. In this case, if we havefr
,language
will representen
. 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 );
}
}
- The first condition checks if the locale is among the available locales.
- The second condition checks if the given locale is different from the current locale.
-
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. - This locale must adhere to
where(['locale' => '[a-zA-Z]{2}'])
, which is equivalent to two letters betweena
andZ
. - As the routes
''
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 a404
page in this case. - It is important not to specify a name for the route for changing the locale, or an infinite loop could occur in its redirection.
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.
driesvints, fanatp, antoniputra liked this article