
Implementing i18n in Laravel and Vue.js with Inertia
Ever tried adding internationalization to your Laravel and Vue.js app? It can be a real headache! But don't worry, we've got you covered. In this guide, we'll walk through implementing i18n in your Laravel, Vue.js, and Inertia stack – and we'll make it painless and even fun!
Hey there! Ever tried to add internationalization to your Laravel and Vue.js app? If you're anything like me, you probably went through the five stages of i18n grief:
-
Denial: "It can't be that hard, right?"
-
Anger: "Why won't this *@#$ing translation work?!"
-
Bargaining: "Maybe if I just use Google Translate for everything..."
-
Depression: "I guess my app will only ever speak one language." 😢
-
Acceptance: "Okay, let's figure this out once and for all."
Well, my friend, it's time to skip straight to acceptance because I've got a treat for you today. We're going to implement i18n in our Laravel, Vue.js, and Inertia stack, and we're going to do it with style. 😎
What We Need

Before we dive in, let's gather our ingredients:
-
vue-i18n package
-
Language files in JSON format
-
An endpoint to set the current language
-
A way to store the current locale
-
The HandleInertia middleware
Got everything? Great! Let's get cooking.
Step 1: Setting Up vue-i18n
First things first, let's add vue-i18n to our project. Open up your terminal and type:
npm install vue-i18n@10
Step 2: Creating Our Language Files
Now, let's create some language files. We'll put these in a resources/js/lang directory. Here's an example structure:
resources/js/lang/
├── en.json
├── es.json
└── fr.json
Let's fill en.json with some sample translations:
{
"welcome": "Welcome to our awesome app!",
"login": "Log in",
"register": "Sign up"
}
And es.json:
{
"welcome": "¡Bienvenido a nuestra increíble aplicación!",
"login": "Iniciar sesión",
"register": "Registrarse"
}
Step 3: Setting Up Our Language Endpoint
We need a way to tell our backend which language to use. Let's create a simple endpoint in our web.php routes file:
Route::post('/language', function (Request $request) {
$locale = $request->input('locale');
if (in_array($locale, config('app.available_locales'))) {
session()->put('locale', $locale);
}
return redirect()->back();
});
Step 4: Storing the Current Locale
We're using the session to store the current locale in the example above. If you want to store it in the user's database record instead, you'd need to modify this to update the user's record when they're logged in.
Step 5: Updating the HandleInertia Middleware
Now, let's modify our HandleInertiaRequests middleware to provide the frontend with available locales and the current locale:
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request)
{
return array_merge(parent::share($request), [
'locale' => session('locale', config('app.locale')),
'available_locales' => config('app.available_locales'),
]);
}
}
Step 6: Setting Up app.js
Now, let's set up our app.js to register the languages:
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import { ZiggyVue } from '../../vendor/tightenco/ziggy'
import { createI18n } from 'vue-i18n'
import messages from './lang'
const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, App, props, plugin }) {
const i18n = createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: props.initialPage.props.locale || 'fr',
fallbackLocale:'en',
messages: messages,
globalInjection:true
});
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(i18n)
.use(ZiggyVue, Ziggy)
.mount(el);
},
progress: {
color: '#4B5563',
},
});
Step 7: Creating Our lang/index.js
Let's create a lang/index.js file to import all our language files:
import en from './en.json'
import es from './es.json'
import fr from './fr.json'
export default {
en,
es,
fr
}
Step 8: Usage Example
Now for the fun part! Let's use our translations in a Vue component, with a small language switcher that posts to the endpoint from step 3:
<script setup>
import { router, usePage } from '@inertiajs/vue3'
const page = usePage()
function switchLanguage(locale) {
router.post('/language', { locale }, { preserveScroll: true })
}
</script>
<template>
<h1>{{ $t('welcome') }}</h1>
<select :value="page.props.locale" @change="switchLanguage($event.target.value)">
<option v-for="locale in page.props.available_locales" :key="locale" :value="locale">
{{ locale.toUpperCase() }}
</option>
</select>
</template>
One thing the setup above doesn't do on its own: after the switch, Laravel redirects back and Inertia updates page.props.locale, but vue-i18n was created once with the initial locale. Keep them in sync by listening to Inertia's navigate event in app.js, right after creating the i18n instance:
import { router } from '@inertiajs/vue3'
router.on('navigate', (event) => {
const locale = event.detail.page.props.locale
if (locale) i18n.global.locale.value = locale // `legacy: false` makes this a ref
})
Bonus: Naming Convention
Now, you might be wondering: should I use full strings or keys for my translations? Well, it depends on your preference and project needs. Here's a quick comparison:
Full Strings:
{
"Welcome to our awesome app!": "¡Bienvenido a nuestra increíble aplicación!"
}
Keys:
{
"welcome_message": "Welcome to our awesome app!"
}
Full strings can be easier to manage for smaller projects, while keys can be more organized for larger ones. Choose what works best for you!
The Missing Piece: Language Middleware
Oh, wait! We almost forgot one crucial piece: the middleware to set the language for other requests. Let's create a new middleware called SetLanguage:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SetLanguage
{
public function handle(Request $request, Closure $next)
{
$locale = session('locale', config('app.locale'));
app()->setLocale($locale);
return $next($request);
}
}
Don't forget to register this middleware. On Laravel 11 and later there's no Kernel.php any more; middleware is registered in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\SetLanguage::class,
]);
})
On Laravel 10 and earlier, add it to the web group in app/Http/Kernel.php:
protected $middlewareGroups = [
'web' => [
// ... other middleware
\App\Http\Middleware\SetLanguage::class,
],
];
Arabic and other right-to-left languages
Translating strings is half the job for Arabic, Hebrew, Persian or Urdu: the whole layout has to flip. I've shipped Arabic/French storefronts on this kind of stack, and these are the parts that matter.
1. Set lang and dir on <html>, on the server and on every switch. In resources/views/app.blade.php:
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"
dir="{{ in_array(app()->getLocale(), ['ar', 'he', 'fa', 'ur']) ? 'rtl' : 'ltr' }}">
Because switching language is an Inertia visit, not a full page load, the Blade template doesn't re-render. Update the attributes in the same navigate listener that syncs vue-i18n:
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur']
router.on('navigate', (event) => {
const locale = event.detail.page.props.locale
if (!locale) return
i18n.global.locale.value = locale
document.documentElement.lang = locale
document.documentElement.dir = RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr'
})
2. Write direction-agnostic CSS. With Tailwind, use logical utilities instead of left/right ones: ms-4/me-4 instead of ml-4/mr-4, ps-/pe- for padding, start-0/end-0 for positioning, text-start instead of text-left. They flip automatically with dir. For the few things that don't, use the rtl: variant, for example rtl:-scale-x-100 on an arrow icon.
3. Don't mirror everything. Logos, product photos, code, phone numbers and most charts stay as they are. Arrows that mean "next" and "back" flip; a play button doesn't.
4. Use a font that has Arabic glyphs. Many Latin UI fonts fall back to a system Arabic font that looks out of place. Load an Arabic font and apply it only to RTL pages, e.g. [dir="rtl"] body { font-family: "Your Arabic Font", sans-serif; }.
5. Format numbers and dates with Intl. new Intl.NumberFormat('ar-DZ') and Intl.DateTimeFormat handle separators and month names for you. Check which digits your audience expects before hard-coding a locale.
6. Test with real text. Arabic strings are often shorter or longer than English, and mixed Arabic/Latin text (a product name inside an Arabic sentence) is where bugs hide. Put a real Arabic translation in before calling a screen done.
And there you have it, folks! You've just implemented i18n in your Laravel, Vue.js, and Inertia app. Your app is now a polyglot, ready to charm users from all corners of the globe. 🌍🗣️
Remember, the key to mastering i18n is practice and patience. Don't get discouraged if things don't work perfectly right away. Keep at it, and soon you'll be juggling languages like a linguistic circus performer!
Happy coding, and may your apps speak in tongues (the good kind)! 😄🚀
Vous avez un projet similaire ?
Expliquez-moi où vous en êtes et ce qui vous bloque. Réponse rapide sur WhatsApp, devis gratuit.