Have you taken a look at the docs?
Translations with Laravel and Inertia.
I am searching for the best way to make my Laravel/Inertia app multi language. Preferably without using a 3rd party package.
I have found two different explanations but they seem to differ from one and other.
It seems one of the major differences is that the first one uses a blade component to prevent props that rarely change be sent with every request. However the second one also explains how to build a language switcher.
Now I am wondering which one I should follow. Any opinions? Or maybe a third one?
Personally I generate a json file with all the translations (from laravel localization) by running a command. Then I load this file directly. Works like a charm
@Sinnbeck I know this is an old thread, but I was hoping that you could share in more detail your approach at handling translations in Inertia. Any suggestions are much appreciated.
@DanielRønfeldt Sure. I use langjs
https://github.com/rmariuzzo/Lang.js/
and set it up like this in javascript
import Lang from 'lang.js'
import messages from '@/i18n.json'
export const lang = new Lang({messages, fallback: 'da'})
I also added some helper functions to make it more laravel like
import {lang} from '@/helpers/lang'
export function trans(key, params = null) {
return lang.get(key, params)
}
export function trans_choice(key, count, params = null) {
return lang.choice(key, count, params)
}
To generate the json file I use this package from the same author https://github.com/rmariuzzo/Laravel-JS-Localization
And run
php artisan lang:js --json
For this to work you need to set up the path in the config file for the package
'path' => resource_path('js/i18n.json'),
I have created an example for a multi-language website using Laravel, Vue, and InertiaJS. Take a look https://github.com/GankCC/multilang
@GankSt That's fine, but I don't think it's really useful for a large website with a lot of text to translate. Ideally make switch from locale and use the translations that already exist from Laravel, and just include additional files with your own texts, which is different than including the texts inside each Vue component. It is much easier to maintain and scale.
I built a dynamic blade directive to inject a global translations helper like the one laravel uses to load its lang files __(key, paramerers)
Just load this service provider and include the directive within your layout file in the head section and enjoy the translation will be updated once you add new tokens to it.
Source: https://gist.github.com/nadyshalaby/669e6f251b942e7d91576f49cda6e879
@taekunger isnt this something you would want to cache, or have as an artisan command you run when there are new translations?
I have a package that handles Laravel style translations to Javascript.
https://github.com/jetstreamlabs/zora
It works very much like Ziggy but without the Blade directive.
It will load all your php or json languages, then compile them into a javascript file which you then use in your Inertia app.
It requires 2 aliases in vite config:
zora: resolve(__dirname, 'vendor/jetstreamlabs/zora/dist/vue.js'),
'zora-js': resolve(__dirname, 'vendor/jetstreamlabs/zora/dist/index.js'),
Compile the translations to resources/js in your dev and build steps in package.json:
"build:assets": "php artisan zora:generate",
Import zora and your compiled translations in app.js or wherever you implement your Inertia setup:
import { ZoraVue } from 'zora'
import { Zora } from '../zora.js'
Then use it in your app:
...
.use(ZoraVue, Zora)
...
Then in your Vue components you can use {{ __('Dashboard') }} or {{ trans('app.whatever') }}
I also have a composable for use in the script portion of components if you need a translation there:
// composables/useTrans.js
import { trans } from 'zora-js'
import { Zora } from '../zora.js'
export default function useTrans(key, replace) {
return trans(key, replace, Zora)
}
I hope you find it useful.
@secondman awesome library. Good to mention you should also add
<script>
window.locale = '{{ app()->getLocale() }}';
</script>
to your app.blade.php so that lib trans function will catch current locale.
Also I released a separate version of that package:
Try it out https://github.com/cyberwolf-studio/lingua
Readme has all instructions how to install it and how to use
@mikield Doesn't seem to work properly.
The keyed translations seem to work, but translations in individual json files don't.
I have en.json placed at lang/en.json.
The file loaded in the browser doesn't have it's contents, hence it is working as if there is no translation for the object:
const Lingua = {
translations: {
"en": {
"php": {
...
"en": {
"json": "en.json"
},
...
},
"json": []
}
}
}
export {Lingua}
I just share translation lang/en.json
<?php
'translation' => function () {
return json_decode(file_get_contents(base_path('lang/'. app()->getLocale() .'.json')), true);
},
Hi all! I just released this one today. Hope that can help you all, and I appreciate if you can give a feedback.
https://github.com/linguijs/i18n
Note that you have a Laravel package to export your translations: https://github.com/linguijs/lingui-laravel
Plus a vite-plugin responsible to listen and re-generate your translations when you modify/add/remove it: https://github.com/linguijs/vite-plugin
I’m trying to build a complete solution for i18n on JavaScript side of Laravel.
I’m updating the docs to have some examples, but for now you can use a i18n.on(“changedLocale”, (locale) => {}) to change the App::setLocale on your back-side from a router.
I have developed a multi-language application with Laravel and VueJS.
Here is what I have done.
You will necessarily need a 3rd party package for the internationalization.
npm install vue-i18n
Then I have created these files to declare the different languages.
// languages/index.js
import action from './action.js';
import branch from './branch.js';
import user from './user.js';
const languages = {
en: {
action: action.en,
branch: branch.en,
user: user.en,
},
fr: {
action: action.fr,
branch: branch.fr,
user: user.fr,
}
};
export default languages;
And the translations files (here example with user.js).
export default {
en: {
change_password: 'Change password',
choose: 'Choose a user',
comments: 'Comments',
confirm_delete: 'Do you really want to delete this user ?',
confirm_reset_password: 'Do you really want to reset the password of this user ?',
create: 'Create a user',
current_password: 'Current password',
delete: 'Delete the user',
edit: 'Edit the user',
email: 'Email address',
name: 'Name',
new_password: 'New password',
new_password_confirmation: 'New password confirmation',
password_to_update_profile: 'You have to enter your password to update your profile',
update: 'Update the user',
user: 'User',
users: 'Users',
users: 'Users',
current_password_required: 'You have to enter your current password.',
name_required: 'You have to enter your name or nickname.',
name_unique: 'This name or nickname is already used by another user.',
name_max: 'The name cannot contain more than 255 caracters.',
email_required: 'You have to enter an email address.',
email_unique: 'This email address is already used by another user.',
email_email: 'You have to enter a valid email address.',
email_max: 'The email address cannot contain more than 255 caracters.',
new_password_required: 'You have to enter a new password.',
new_password_confirmation_required: 'You have to confirm your new password.',
new_password_confirmation_same: 'The new password and its confirmation must be the same.',
password_required: 'You have to enter your password.',
password_right_password: 'You entered the wrong password.',
},
fr: {
change_password: 'Changer le mot de passe',
choose: 'Choisir un utilisateur',
comments: 'Commentaires',
confirm_delete: 'Voulez-vous vraiment supprimer cet utilisateur ?',
confirm_reset_password: 'Voulez-vous vraiment réinitialiser le mot de passe de cet utilisateur ?',
create: 'Créer un utilisateur',
current_password: 'Mot de passe actuel',
delete: 'Supprimer l\'utilisateur',
edit: 'Editer l\'utilisateur',
email: 'Adresse mail',
name: 'Nom',
new_password: 'Nouveau mot de passe',
new_password_confirmation: 'Confirmation du nouveau mot de passe',
password_to_update_profile: 'Vous devez saisir votre mot de passe pour mettre à jour votre profil',
update: 'Mettre à jour l\'utilisateur',
user: 'Utilisateur',
users: 'Utilisateurs',
current_password_required: 'Vous devez saisir votre mot de passe actuel.',
name_required: 'Vous devez saisir votre nom ou pseudo.',
name_unique: 'Ce nom / pseudo est déjà utilisé pour un autre utilisateur.',
name_max: 'Le nom ne peut pas contenir plus de 255 caractères.',
email_required: 'Vous devez saisir une adresse mail.',
email_unique: 'Cette adresse mail est déjà utilisée pour un autre utilisateur.',
email_email: 'Vous devez saisir une adresse mail valide.',
email_max: 'L\'adresse mail ne peut pas contenir plus de 255 caractères.',
new_password_required: 'Vous devez saisir un nouveau mot de passe.',
new_password_confirmation_required: 'Vous devez confirmer votre nouveau mot de passe.',
new_password_confirmation_same: 'Le nouveau mot de passe et sa confirmation doivent être identiques.',
password_required: 'Vous devez saisir votre mot de passe.',
password_right_password: 'Vous vous êtes trompé de mot de passe.',
},
}
You have to use this package in your Vue application and load the languages.
import { createI18n } from 'vue-i18n';
import languages from './languages';
let locale = localStorage.getItem('locale');
if (!locale) {
localStorage.setItem('locale', 'fr');
locale = 'fr';
}
const i18n = createI18n({
locale: locale,
fallbackLocale: 'fr',
messages: languages,
});
// Application
app.use(i18n);
And I have created a LanguageComponent to switch from one language to another.
<template>
<form>
<select class="outline-none appearence-none rounded px-2 py-1" id="language" v-model="$i18n.locale">
<option value="en">{{ $t('language.english') }}</option>
<option value="fr">{{ $t('language.french') }}</option>
</select>
</form>
</template>
<script setup>
</script>
And you can then use this in your different templates.
For example, this refers to the user.js file and the name property. The locale is selected via the global configuration of $i18n.locale.
<div>{{ $t('user.name') }}</div>
🌍 Reactive i18n Integration with Inertia.js, Vue 3, and Laravel
Hey everyone 👋
I’d like to share how I’m using vue-i18n in a reactive way within an Inertia.js + Vue 3 + Laravel stack.
The key is that usePage() from Inertia is reactive by default, and we use it to sync the frontend’s language and translations with Laravel’s backend automatically — no page reloads, no remounts.
⚙️ Laravel:
HandleInertiaRequests.php
Below is the critical part of the middleware that shares locale and translation data reactively with Inertia.
public function share(Request $request): array
{
return [
...parent::share($request),
// 🧑💻 Auth user info
'auth' => [
'user' => $request->user() ? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'email' => $request->user()->email,
'email_verified_at' => $request->user()->email_verified_at?->toISOString(),
'created_at' => $request->user()->created_at->toISOString(),
'updated_at' => $request->user()->updated_at->toISOString(),
] : null,
],
// ⚡ Reactive flash messages
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
'warning' => fn () => $request->session()->get('warning'),
'info' => fn () => $request->session()->get('info'),
],
// 🌍 --- MOST IMPORTANT PART ---
// These three lines make i18n fully reactive with Inertia
'locale' => fn () => app()->getLocale(), // ← line 58
'languages' => fn () => LanguageResource::collection(Lang::cases())->resolve(), // ← line 59
'translations' => fn () => $this->getTranslations($request), // ← line 60
];
}
🧠 What happens here
-
locale sends the current Laravel locale to Vue.
-
languages provides all supported language options.
-
translations dynamically calls getTranslations() to load the proper language files for the current page.
Together, these three lines keep the backend and frontend perfectly synchronized.
When Laravel’s locale changes, Vue detects it reactively via usePage() and updates all translations automatically.
🔍 The getTranslations() Method
This method dynamically loads the right translation files for each Inertia page.
private function getTranslations(Request $request): array
{
$locale = app()->getLocale();
// Detect current Inertia page component
$component = $request->header('X-Inertia-Component');
if (! $component) {
$routeName = $request->route()?->getName();
$component = match ($routeName) {
'home' => 'Home',
'about' => 'About',
default => '',
};
}
// Extract base page name (e.g. "Home" → "home", "Tours/Index" → "tours")
$page = $component ? strtolower(explode('/', $component)[0]) : '';
// 🗂️ Build paths dynamically
$validationPath = lang_path("{$locale}/validation.php");
$commonPath = lang_path("{$locale}/common.php");
$pagePath = $page ? lang_path("{$locale}/{$page}.php") : '';
// ✅ Return grouped translation arrays
return [
'validation' => file_exists($validationPath) ? include $validationPath : [],
'common' => file_exists($commonPath) ? include $commonPath : [],
'page' => ($pagePath && file_exists($pagePath)) ? include $pagePath : [],
];
}
🧩 Frontend: useI18nSetup.ts
Here’s how the frontend picks up and reacts to those shared props from Laravel using vue-i18n and Inertia’s usePage().
import type { SharedProps } from '@/types';
import { usePage } from '@inertiajs/vue3';
import type { WritableComputedRef } from 'vue';
import { watch } from 'vue';
import { createI18n, type I18n } from 'vue-i18n';
export function useI18nSetup(initialPage: any) {
const locale = initialPage.props?.locale || 'en';
// Initialize i18n with placeholder objects for ALL supported locales
const i18n = createI18n({
legacy: false,
locale,
fallbackLocale: 'en',
messages: {
en: { validation: {}, common: {}, page: {} },
es: { validation: {}, common: {}, page: {} },
},
});
// Set initial translations for current locale
const initialTranslations = initialPage.props?.translations || {};
i18n.global.setLocaleMessage(locale, {
validation: initialTranslations.validation || {},
common: initialTranslations.common || {},
page: initialTranslations.page || {},
});
return i18n;
}
export function setupI18nSync(i18n: I18n) {
const page = usePage<SharedProps>();
watch(
() => [page.props.locale, page.props.translations] as const,
([newLocale, newTranslations]) => {
const currentLocale = i18n.global.locale as WritableComputedRef<string>;
if (newLocale) {
// Always update translations (especially page-specific ones)
i18n.global.setLocaleMessage(newLocale, {
validation: newTranslations?.validation || {},
common: newTranslations?.common || {},
page: newTranslations?.page || {},
});
// Update locale if it changed
if (newLocale !== currentLocale.value) {
currentLocale.value = newLocale;
}
}
},
{ deep: true, immediate: true },
);
}
🚀 app.ts
Finally, here’s how it’s initialized and made reactive after Inertia mounts.
import { createInertiaApp, Head, Link } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createPinia } from 'pinia';
import type { DefineComponent } from 'vue';
import { createApp, h } from 'vue';
import './bootstrap.ts';
import { setupI18nSync, useI18nSetup } from './composables/useI18nSetup';
const appName = import.meta.env.VITE_APP_NAME || 'Define App Name .env file';
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
resolve: (name) =>
resolvePageComponent(`./pages/${name}.vue`, import.meta.glob<DefineComponent>('./pages/**/*.vue')),
setup({ el, App, props, plugin }) {
const pinia = createPinia();
const i18n = useI18nSetup(props.initialPage);
const app = createApp({ render: () => h(App, props) });
app.use(plugin);
app.use(pinia);
app.use(i18n);
app.component('Link', Link);
app.component('Head', Head);
// Mount the app
app.mount(el);
// Setup reactive i18n sync
setupI18nSync(i18n);
},
progress: {
color: '#387CD0',
showSpinner: true,
},
});
💡 Result
-
When Laravel sends a new locale or updated translations through Inertia,
Vue automatically detects the change via usePage() and updates all translated content.
-
The connection between backend and frontend stays fully reactive and seamless.
-
No page reloads, no re-mounting components — just pure reactivity.
✅ In summary:
The combination of
'locale', 'languages', 'translations'
(lines 58–60 in HandleInertiaRequests.php)
and the watch() in useI18nSetup.ts makes vue-i18n + Inertia.js fully reactive across Laravel and Vue.
Please or to participate in this conversation.