inertia start
Features

Localization

Go global with built-in multi-language support.

Inertia Start makes it easy to adapt your application to every market.

It builds on Laravel's native localization system, so everything you already know about translation files and the __() helper still applies. On top of that, Inertia Start adds the pieces you'd otherwise have to build yourself.

What you get

  • automatic detection of the user's preferred language, and persistence of their choice
  • a ready-to-use language switcher component
  • translations available in your Vue components, not just in Blade
  • dates automatically formatted for the active language
  • locale-aware emails (users receive notifications in their own language)
  • SEO-friendly localized URLs with Route::localized(...), including automatic hreflang tags
  • translated URL segments with Lang::uri(...)

Included translations

Out of the box, Inertia Start ships translations for:

  • English (en)
  • French (fr)
  • Spanish (es)

Adding more languages is straightforward and covered below.

Getting started

How you set things up depends on whether your app needs one language or several.

One language only

If your app only ever needs a single language, you don't have to enable anything. Just set the APP_LOCALE environment variable:

.env
APP_LOCALE=fr
APP_FALLBACK_LOCALE=en

APP_FALLBACK_LOCALE is the language used when a translation is missing in the active language.

That's it. You can still use Laravel's translation files and the __() helper everywhere, but no language switcher or auto-detection will be active.

Multiple languages

If you want to support several languages and let users pick their own, enable the localization feature and follow these steps.

Enable localization

.env
I_S_ENABLE_LOCALIZATION=true

While localization is disabled, the localization-only routes and the settings.updateLocale endpoint return a 404, and the language switcher is hidden.

Declare your supported locales

Create a config/env/locales.json file (you can copy the provided config/env/locales.json.example) and list every language you want to offer. The key is the locale identifier; name and native are the labels shown in the language switcher.

config/env/locales.json
{
    "en": {
        "name": "English",
        "native": "English"
    },
    "es": {
        "name": "Spanish",
        "native": "Español"
    },
    "fr": {
        "name": "French",
        "native": "Français"
    }
}

Each entry may also include an optional slug or domain if you want custom route slugs or a dedicated domain per language (see Localized URLs).

Add your translations

Translations live in Laravel's lang/ directory. You can use short keys grouped in PHP files, or full sentences as keys in a JSON file.

lang/en/messages.php
<?php

return [
    'welcome' => 'Welcome!',
];
lang/fr/messages.php
<?php

return [
    'welcome' => 'Bienvenue !',
];

You then reference these with the messages.welcome key.

lang/fr.json
{
    "Welcome!": "Bienvenue !"
}

You then reference these with the full English sentence as the key: __('Welcome!').

See Laravel's guide on defining translation strings for the full details.

Compile the translations for the frontend

Your Vue components don't read the lang/ files directly. Inertia Start compiles them into a single resources/js/translations.json file that is bundled with your frontend.

While the npm run dev server is running, you don't need to do anything: it watches the lang/ directory, regenerates that file automatically, and hot-reloads the page as soon as you save a translation.

Outside of the dev server, regenerate it manually:

npm run prepare

npm run prepare regenerates both the JS translations (resources/js/translations.json) and the route definitions (resources/js/routes.js). It also runs as part of npm run build, so production builds always pick up your latest translations. So you only need to run it by hand when you've changed translations without the dev server running.

Display translations in your UI

In Blade views, use Laravel's __() helper as usual:

<div>{{ __('messages.welcome') }}</div>

In Vue components, the same __() helper is available globally in templates — no import needed:

<template>
  <div>{{ __('messages.welcome') }}</div>
</template>

See Translating in Vue components for how to translate inside <script setup> and for pluralization.

Let users switch language

The LanguageSwitcher component is already included in the built-in layouts and on the account settings page. You can drop it anywhere else too:

<script setup lang="ts">
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
</script>

<template>
  <LanguageSwitcher />
</template>

When the user picks a language, Inertia Start persists the choice automatically: for an authenticated user it's saved to their account, and for a guest it's stored in the session. On localized URLs, the switcher navigates to the matching language URL instead.

Translating in Vue components

In a template, the global __() helper (an alias of trans) works without any import:

<template>
  <h1>{{ __('messages.welcome') }}</h1>
</template>

Inside <script setup> (where global helpers aren't available), use the useTrans composable:

<script setup lang="ts">
import { useTrans } from '@/plugins/i18n';

const { __, trans, transChoice } = useTrans();

const title = __('messages.welcome');
</script>

useTrans exposes:

  • trans(key, replacements?) — get a translation. __ is an alias of trans.
  • transChoice(key, count, replacements?) — get a translation with pluralization.

Replacing placeholders. Pass an object as the second argument to fill :placeholders in your strings:

lang/en/messages.php
return [
    'greeting' => 'Hello, :name!',
];
<template>
  <p>{{ __('messages.greeting', { name: 'Anthony' }) }}</p>
</template>

Pluralization. Use transChoice (or the global trans_choice) with a count:

lang/en/messages.php
return [
    'apples' => 'There is one apple|There are many apples',
];
<script setup lang="ts">
import { useTrans } from '@/plugins/i18n';

const { transChoice } = useTrans();
</script>

<template>
  <p>{{ transChoice('messages.apples', 5) }}</p>
</template>

Dates are localized for you

When the active language changes, Inertia Start also updates the date-fns locale, so any date you format on the frontend follows the conventions of the current language automatically.

How the language is chosen

When localization is enabled, Inertia Start automatically resolves the most appropriate language on each request. It checks the following sources in order and uses the first supported locale it finds:

  1. URL — the locale prefix in the path (e.g. /fr/...) or a locale-specific domain.
  2. User account — the authenticated user's saved locale.
  3. Session — the locale stored in the current session (used for guests).
  4. Cookie — a previously stored locale cookie (only if you enable cookie storage).
  5. Browser — the Accept-Language header sent by the browser.
  6. Application default — falls back to app.locale if nothing else matches.

Once resolved, the locale is persisted so it sticks across requests — by default in the session (and in the user's account when they explicitly switch).

You can fully customize this behavior in config/inertia-start.php:

  • reorder, add, or remove detectors under localization.detection
  • choose where the locale is persisted under localization.storage (session by default; uncomment the CookieStore to also persist it in a cookie)

These settings apply globally, whether or not you use localized URLs.

Localized URLs

Localized URLs give each language its own unique address (great for SEO, since search engines can index every language version separately). Register them with the Route::localized(...) macro:

routes/web.php
Route::localized(function () {
    Route::get('/', function () {
        return Inertia::render('Home');
    })->name('home');
});

The macro prefixes each URL with its locale, based on localization.locales and localization.localized_urls in config/inertia-start.php. By default en is the omitted locale, so English URLs stay unprefixed while the others get a prefix:

  • /login → English
  • /fr/login → French
  • /es/login → Spanish

If English isn't your primary language, update both localization.localized_urls.fallback_locale and localization.localized_urls.omitted_locale in config/inertia-start.php.

When I_S_ENABLE_LOCALIZATION is false, Route::localized(...) still registers your routes — it simply skips the locale prefixes. This means the macro is safe to use even in a single-language app.

Which routes should be localized?

In the built-in routes, only publicly accessible guest routes (such as the home, login, and signup pages) are wrapped in Route::localized(...), because those benefit from SEO and indexing.

Routes behind the auth middleware are intentionally not localized: they aren't indexed by search engines, and every language version can safely share the same URL, so a locale prefix would add no value.

Generating localized URLs

On the frontend, use the global route() helper or the useRoute composable. Both automatically try the localized route name for the current language first (e.g. fr.home) and fall back to the raw name (home) if the route isn't localized — so you can always just write the plain route name.

<script setup lang="ts">
import { useRoute } from '@/composables/useRoute';

const { route } = useRoute();
</script>

<template>
  <!-- resolves to /fr/login when the active language is French -->
  <Link :href="route('login')">Login</Link>
</template>

On the backend, Route::localizedUrl(...) generates the URL of the current page in a given locale — this is what powers the language switcher and the alternate links below.

For localized pages, Inertia Start automatically injects <link rel="alternate" hreflang="..."> tags (including an x-default pointing at your fallback locale) via the AlternateLinks component, which is already wired into the layouts. This tells search engines about every language version of a page — no setup required.

Redirecting URLs without a locale prefix

You can optionally redirect visitors who land on a non-localized URL to the correct localized version. Enable it in config/inertia-start.php:

config/inertia-start.php
'localized_urls' => [
    // ...
    'redirect' => [
        'enabled' => true,
        'status_code' => 301,
    ],
],

This relies on a fallback route, which the starter kit already registers at the very end of routes/web.php:

routes/web.php
use App\Http\Controllers\FallbackController;
use Illuminate\Support\Facades\Route;

// Keep this at the end of your web routes file.
Route::fallback(FallbackController::class);

If you replace the default fallback route, make sure you still register one at the end of your routes file so these redirects keep working.

Translating URL segments

Beyond prefixing URLs with a locale, you can translate the path segments themselves. Combine Route::localized(...) with Lang::uri(...) and a routes translation file:

routes/web.php
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Route;

Route::localized(function () {
    Route::get(Lang::uri('login'), function () {
        // ...
    })->name('login');

    Route::get(Lang::uri('products/{product}'), function () {
        // ...
    })->name('products.show');
});
lang/en/routes.php
<?php

return [
    'login' => 'login',
    'products' => 'products',
];
lang/fr/routes.php
<?php

return [
    'login' => 'connexion',
    'products' => 'produits',
];

With the above, the login page is reachable at /login (English) and /fr/connexion (French).

Lang::uri(...) first checks whether the full URI has a translation, then falls back to translating each segment individually. Route parameters such as {product} are preserved automatically.

Configuration & key files

All localization settings live in the localization section of config/inertia-start.php:

SectionWhat it controls
localesThe languages offered in the switcher (loaded from config/env/locales.json).
detectionThe detectors used to resolve the current locale, and their order.
storageWhere the resolved locale is persisted (session, cookie, user model).
localized_urlsBehavior of localized URLs: fallback/omitted locale and redirects. Only used with Route::localized(...).

The main building blocks, if you want to dig deeper:

  • resources/js/plugins/i18n.ts — loads the translations, creates the useTrans composable and the global __ / trans / trans_choice helpers, and keeps the date-fns locale in sync.
  • resources/js/plugins/ziggy.ts & resources/js/composables/useRoute.ts — register the locale-aware global route() helper.
  • resources/js/components/LanguageSwitcher.vue — the language selector.
  • resources/js/components/AlternateLinks.vue — the automatic hreflang SEO tags.

On this page