DEV Community

Cover image for Implement a translation system into your Laravel project with Inertia and Vue
Capsules Codes
Capsules Codes

Posted on • Originally published at capsules.codes

Implement a translation system into your Laravel project with Inertia and Vue

TL;DR: 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
Enter fullscreen mode Exit fullscreen mode

 

lang/fr.json

{
    "Hello world!" : "Bonjour le monde!",
    "This is a translation" : "Ceci est une traduction",
    "Maintenance mode activated" : "Le mode maintenance est activé"
}
Enter fullscreen mode Exit fullscreen mode

 
 

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' ],
Enter fullscreen mode Exit fullscreen mode
  • 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 ) : []
        ] );
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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() ) );
Enter fullscreen mode Exit fullscreen mode
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"
  ]
]
Enter fullscreen mode Exit fullscreen mode
  • Modify the language using 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' );
Enter fullscreen mode Exit fullscreen mode

 
 

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
Enter fullscreen mode Exit fullscreen mode

 
 

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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

 
 

Capsules Translations Image 1

 
 

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>
Enter fullscreen mode Exit fullscreen mode
  • 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 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 );
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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,
        ...
    ];
    ...
Enter fullscreen mode Exit fullscreen mode

 
 

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' ) );
Enter fullscreen mode Exit fullscreen mode
  • 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 between a and Z.
  • 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 a 404 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 ?? ''}`;
}
Enter fullscreen mode Exit fullscreen mode

 

import { useRoute, useTrans } from '~/composables/trans';

<a v-bind:href="useRoute( `/welcome` )"><span v-text="useTrans( 'Welcome' )" />
Enter fullscreen mode Exit fullscreen mode

 
 

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 ) ) );

    ...
} );

...
Enter fullscreen mode Exit fullscreen mode

 

"Ceci est une traduction" // routes/web.php:13
Enter fullscreen mode Exit fullscreen mode

 
 

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;
}
Enter fullscreen mode Exit fullscreen mode

 
 

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>
Enter fullscreen mode Exit fullscreen mode

 
 

php artisan down
Enter fullscreen mode Exit fullscreen mode

 
 

Capsules Translations Image 2

 
 

Glad this helped.

Top comments (2)

Collapse
 
mreduar profile image
Eduar Bastidas

From my point of view this is inefficient because the middleware is going to run for each request of the application, if you have millions of requests per second searching and reading the file is very slow and the application will become slow.

Collapse
 
capsulescodes profile image
Capsules Codes

Do you have any suggestion on how to improve the behavior ? I suppose separating translations by view could be a solution.