Laravel and Vue translations

Laravel holds the strings. vue-i18n renders them. The instance is created once, so a locale change has to push new messages into it.

For your AI agent

Copies a prompt. Paste it into the AI agent you already have open.

Read https://laravel-vuejs.space/laravel-vue-i18n.md
Laravel holds the strings. vue-i18n renders them. The instance is created once, so a locale change has to push new messages into it.
Do not add a second translation package that syncs files.
Do not write $request->string('locale') straight into setLocale.
Show the diff. Do not commit.

Next: Laravel and Vue CORS errors

On this page · 7 sections
  1. The PHP strings
  2. vue-i18n
  3. Where this stops
  4. Recap
  5. FAQ
  6. Next steps
  7. Sources

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.locale and Lang::get('messages') from HandleInertiaRequests.
  • Switch with a POST to /locale that validates against an allow-list.
  • Register vue-i18n in the kit’s withApp (legacy: false); keep the kit’s other options.
  • Watch page.props.i18n in a persistent layout and merge the new messages.

What you'll learn

  • How to share Laravel translation strings with Vue
  • How to switch locale safely
  • How to keep vue-i18n in sync across visits

Who it's for: Laravel + Vue developers adding a second language

Applies to: Laravel 13 · Inertia 3 · Vue 3 · vue-i18n

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#

lang/en/messages.phpphp
return [
    'posts' => 'Posts',
    'saved' => 'Saved.',
];
lang/fr/messages.phpphp
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.

app/Http/Middleware/HandleInertiaRequests.phpphp
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'),
        ],
    ];
}
routes/web.phpphp
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#

bash
npm install vue-i18n

Heads 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.

resources/js/app.tstypescript
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.

resources/js/layouts/AppLayout.vuevue
<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.

resources/js/pages/posts/Index.vuevue
<script setup lang="ts">
import { useI18n } from "vue-i18n";

const { t } = useI18n();
</script>

<template>
  <h1>{{ t("posts") }}</h1>
</template>

Switch to French

resources/js/components/LocaleSwitch.vuevue
<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 lang files 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.

Next steps#

Sources#

Found this useful?

Ask questions, share what you built.

Join thousands of Laravel and Vue developers in our Facebook group, or get new posts in your Telegram feed. Sharing this article helps too.

Keep reading

Newsletter

Get the best of Laravel & Vue, once a month.

One short email with new guides, release notes worth reading and hand-picked community links. No spam, unsubscribe any time.