Your Laravel validation messages are in French and your Vue buttons are still in English. Two translation systems, zero shared strings. Here is how to send Laravel’s lang files to vue-i18n as Inertia props, and switch language without a reload of the app.
TL;DR
- Share
i18n.localeandLang::get('messages')fromHandleInertiaRequests. - Switch with a POST to
/localethat validates against an allow-list. - Register vue-i18n in the kit’s
withApp(legacy: false); keep the kit’s other options. - Watch
page.props.i18nin a persistent layout and merge the new messages.
Laravel already has a lang directory. Vue does not read it. The job is to share the locale and the strings as Inertia props, then let vue-i18n render them. Do not add a second translation package that syncs files. The stack around this page is Inertia, a SPA, or Nuxt.
The PHP strings#
return [
'posts' => 'Posts',
'saved' => 'Saved.',
];return [
'posts' => 'Articles',
'saved' => 'Sauvegarde.',
];Gotcha: set the locale before loading strings
Share the active file from HandleInertiaRequests. Lang::get('messages') returns the array for that locale. Set the locale from the session before you load the file, or the French page still receives the English array.
use Illuminate\Support\Facades\Lang;
public function share(Request $request): array
{
$locale = $request->session()->get('locale', config('app.locale'));
app()->setLocale($locale);
return [
...parent::share($request),
'i18n' => [
'locale' => $locale,
'messages' => Lang::get('messages'),
],
];
}Route::post('/locale', function (Request $request) {
$locale = $request->validate([
'locale' => ['required', 'in:en,fr'],
])['locale'];
$request->session()->put('locale', $locale);
return back();
});Gotcha: never trust the raw locale
The allow-list is the point. Do not write $request->string('locale') straight into setLocale.
vue-i18n#
npm install vue-i18nHeads up: keep the kit’s options
Register it in the starter kit’s withApp callback in resources/js/app.ts. Keep the title, layout, and progress options the kit already passes. A manual setup callback turns off the Vite plugin’s SSR wiring. legacy: false is the Composition API mode. The shared props are page.props.
import { createI18n } from "vue-i18n";
// inside the existing createInertiaApp({ ... }) call
withApp(app, { page }) {
const i18n = createI18n({
legacy: false,
locale: page.props.i18n.locale,
messages: {
[page.props.i18n.locale]: page.props.i18n.messages,
},
});
app.use(i18n);
},Gotcha: created once, so watch for changes
That instance is created once. A later visit can change page.props.i18n without rebuilding it. Update the instance when the shared prop changes. A layout that is always mounted is the right place.
<script setup lang="ts">
import { watch } from "vue";
import { useI18n } from "vue-i18n";
import { usePage } from "@inertiajs/vue3";
const page = usePage();
const { locale, mergeLocaleMessage } = useI18n();
watch(
() => page.props.i18n,
(i18n) => {
mergeLocaleMessage(i18n.locale, i18n.messages);
locale.value = i18n.locale;
},
);
</script>In a page, t reads the key you put in the PHP file.
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t } = useI18n();
</script>
<template>
<h1>{{ t("posts") }}</h1>
</template>Switch to French
<Form action="/locale" method="post">
<button name="locale" value="fr">Français</button>
</Form>Click it: the heading changes to “Articles” with no full reload.
The locale switch is a normal Inertia form posting to /locale. back() loads the same page again, the middleware shares the other file, and the watcher swaps t("posts").
Where this stops#
Dates and money still use Intl in the browser or Carbon on the server. This page does not format them. Client-only drafts stay in Pinia. Pinia is the wrong place to copy the language files.
Recap#
- Laravel’s
langfiles stay the source of truth. - Share the active locale’s strings as a prop.
- Allow-list the locale before
setLocale. - Create vue-i18n once; merge new messages on each visit.
FAQ#
How do I use Laravel translations in Vue with Inertia?
Share Lang::get('messages') and the locale from HandleInertiaRequests, then load them into vue-i18n.
Why does vue-i18n not change language after switching locale?
The i18n instance is created once. Watch page.props.i18n in a persistent layout and call mergeLocaleMessage and set locale.
Do I need a package to sync Laravel lang files to Vue?
No. Sharing the strings as Inertia props is enough.



