Support the ongoing development of Laravel.io →

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

12 Feb, 2024 9 min read

Photo by Ave Calvar on Unsplash

How to quickly set up a translation system in a Laravel project with Inertia and Vue.

Capsules Translations Image 0

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 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"
  ]
]
  • 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' );

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>

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>
  • 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 );
    }
}
  • 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 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 ?? ''}`;
}
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

Capsules Translations Image 2

Glad this helped.

Last updated 3 weeks ago.

driesvints, fanatp, antoniputra liked this article

3
Like this article? Let the author know and give them a clap!
mho (MHO) Full time side project full stack web developer | designer Work @ http://capsules.codes

Other articles you might like

November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
November 11th 2024

🍣 Sushi — Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.