Laravel Inertia translation becomes difficult at the exact moment a form starts doing real work. Labels may be translated in Vue, but validation errors come from Laravel. A custom error is written directly in a component because it is quick. A React page receives a different set of strings from the Vue page. Soon the same form speaks two languages depending on which message the user sees.
The clean solution is to keep the language files in Laravel and share the groups each Inertia page needs. Laravel remains responsible for validation and pluralization, while Vue and React use the synced values for labels, hints and client-side messages.
This guide uses Laravel Inertia translation helpers with a workspace form, but the same pattern works for checkout, profile settings, billing and admin screens.
Keep validation messages in Laravel
Start with Laravel’s normal language files instead of copying validation messages into JavaScript:
// lang/en/validation.php
return [
'required' => 'The :attribute field is required.',
'email' => 'Please enter a valid email address.',
'max' => [
'string' => 'The :attribute may not be greater than :max characters.',
],
'custom' => [
'company_name' => [
'required' => 'Tell us your company name so we can set up your workspace.',
],
],
'attributes' => [
'company_name' => 'company name',
],
];Add a corresponding file for every supported locale. The important detail is that the validator and the frontend are reading the same locale decision. Do not let the browser silently switch to English while Laravel is validating in French or Hindi.
The Laravel translations in Inertia Vue and React guide covers the initial package setup. Once that is installed, the controller only needs to request the groups used by the page.
Sync the groups before rendering the page
Call syncLangFiles() before returning the Inertia response:
use Inertia\Inertia;
use Inertia\Response;
public function create(): Response
{
syncLangFiles(['validation', 'onboarding']);
return Inertia::render('Workspaces/Create');
}The package reads the files for the current Laravel locale and makes them available through page.props.lang. You can sync one group, several groups or a nested group:
syncLangFiles('auth');
syncLangFiles(['layout', 'billing', 'validation']);
syncLangFiles('admin.users');The last example reads lang/{locale}/admin/users.php and exposes it under the matching key path. Sync only what the page needs, plus strings used by the shared layout. Sending every language group with every response makes the page payload larger and hides the dependency between a controller and its copy.
Use the Vue helper for labels and hints
On an Inertia Vue page, import vueLang() and use the keys from your Laravel files:
<script setup lang="ts">
import { vueLang } from '@erag/lang-sync-inertia';
const { __, trans } = vueLang();
</script>
<template>
<label for="company_name">{{ __('onboarding.company_name') }}</label>
<p>{{ trans('onboarding.company_hint') }}</p>
<input id="company_name" name="company_name" type="text">
</template>Use __() for a simple lookup and trans() when a message contains placeholders. The helper reads from the Inertia page props, so a nested component can use a string without receiving it through several layers of props.
The React version uses the same keys:
import { reactLang } from '@erag/lang-sync-inertia';
export default function WorkspaceCreate() {
const { __, trans } = reactLang();
return (
<section>
<label htmlFor="company_name">{__('onboarding.company_name')}</label>
<p>{trans('onboarding.company_hint')}</p>
<input id="company_name" name="company_name" />
</section>
);
}The translation keys stay the same across Vue and React. That makes it possible to move a screen between adapters without maintaining two dictionaries.
Let Inertia display server errors as server errors
When a Laravel Form Request fails, Inertia redirects back with the validation errors. The form component should display the returned message beside the corresponding input:
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
const form = useForm({ company_name: '' });
</script>
<template>
<label for="company_name">Company name</label>
<input id="company_name" v-model="form.company_name" type="text">
<p v-if="form.errors.company_name" role="alert">
{{ form.errors.company_name }}
</p>
</template>Do not pass form.errors.company_name through trans() again. Laravel has already selected the locale, resolved the attribute name and replaced placeholders. Translating it a second time can turn a correct sentence into a missing-key message.
The frontend can still use synced strings for labels, help text and client-only messages. The error returned from the server should remain the source of truth for server validation.
Add custom messages without hardcoding them in a component
Laravel’s custom and attributes sections are useful for making errors sound like the rest of your product:
// lang/hi/validation.php
return [
'custom' => [
'company_name' => [
'required' => 'अपना कंपनी नाम दर्ज करें।',
],
],
'attributes' => [
'company_name' => 'कंपनी नाम',
],
];The Form Request stays language-neutral:
public function rules(): array
{
return [
'company_name' => ['required', 'string', 'max:120'],
];
}This is easier to maintain than checking the locale inside a controller or writing a different error string in each Vue page.
Use placeholders for dynamic copy
Keep the full sentence in the language file. Do not concatenate translated fragments around a number because word order changes between languages:
// lang/en/onboarding.php
return [
'welcome' => 'Welcome, :name.',
'projects_remaining' => '{0} No projects left|{1} One project left|[2,*] :count projects left',
];const welcome = trans('onboarding.welcome', { name: user.name });
const remaining = transChoice('onboarding.projects_remaining', projectCount.value);The package supports Laravel-style placeholders and pluralization helpers. Add zero, one and many cases to your translation files when those states read differently. A phrase that sounds fine in English can be grammatically wrong when translated by splitting it into pieces.
Keep the locale consistent during a language switch
A language switch should update the server locale before the next Inertia visit. A typical flow is:
- Submit the selected locale to a route.
- Store it in the session or on the authenticated user.
- Apply it in locale middleware for the next request.
- Redirect back to the current page.
The next controller call to syncLangFiles() then reads the new language files, and Laravel generates validation errors in that locale. Update the document language as well so screen readers and browser tools understand the page:
watch(() => page.props.locale, (locale) => {
document.documentElement.lang = locale;
}, { immediate: true });Do not change only the JavaScript dictionary. If the server keeps validating in the old locale, the form will still feel inconsistent.
Test the translated response
A feature test should choose a locale, submit invalid data and assert that the returned error is translated:
it('returns a Hindi validation message', function () {
app()->setLocale('hi');
$response = $this->post('/workspaces', [
'company_name' => '',
]);
$response->assertSessionHasErrors('company_name');
expect(session('errors')->first('company_name'))
->toContain('कंपनी');
});Also test that important pages sync the groups they use:
$this->get('/workspaces/create')
->assertInertia(fn (AssertableInertia $page) => $page
->has('props.lang.validation')
->has('props.lang.onboarding')
);The exact assertion depends on your test setup, but the idea is valuable: a forgotten syncLangFiles() call should fail in development instead of appearing as a raw key in production.
Avoid shipping a huge translation payload
Syncing language groups per page keeps the response focused. Shared navigation strings can be synced in a common middleware or base controller, while page-specific groups stay with the page action.
If a static widget or non-Inertia script needs language data, use the package’s JSON export command instead. The Laravel lang files to frontend JSON guide explains when runtime Inertia props are better and when generated JSON is the cleaner choice.
Reuse the same keys outside the form
Validation is only one part of a form’s language experience. The success notification, email verification message and empty state should use the same vocabulary as the labels and errors. Keep those strings in a nearby group instead of writing a second version in a controller:
// lang/en/onboarding.php
return [
'company_name' => 'Company name',
'company_hint' => 'Use the name your team will recognize.',
'saved' => 'Your workspace is ready.',
];The controller can flash __('onboarding.saved') after a successful request, while the Vue page reads onboarding.company_name and onboarding.company_hint through vueLang(). A translator sees the whole flow together and can choose consistent terms. This is especially useful for words such as workspace, organization, project and account, which often drift when each component owns its own copy.
Why not translate validation errors in Vue?
Because Laravel already knows the current locale, validation rule, attribute name and placeholder values. Translating the returned message again duplicates logic and can create mismatched wording.
What happens when a key is missing?
The helper returns the key passed to it. Seeing onboarding.company_name on screen is useful during development because it points directly to the missing language group or key.
Should every page sync validation.php?
Only pages that display translated frontend validation copy need it. Server-returned Form Request messages are generated by Laravel, but syncing the group can still be useful when the page has client-side hints or custom labels from the same file.
Can the same language files work with Vue and React?
Yes. The backend call and translation keys are the same; use vueLang() in Vue pages and reactLang() in React pages.
Laravel Inertia translation works well when the boundary is clear: Laravel owns language files and validation, controllers request the groups a page needs, and the frontend resolves labels and dynamic copy through the synced props. That keeps a form coherent from its first label to its final error message.