erag/laravel-lang-sync-inertia
Syncs Laravel localization files with Inertia.js so frontend and backend share one translation source. Expose PHP lang strings as Inertia props, keep Vue/React text aligned with backend locales, and avoid duplicating translation management across your stack.
1. Install and publish
composer require erag/laravel-lang-sync-inertia
php artisan vendor:publish --tag="lang-sync-inertia-config"
Read config/lang-sync-inertia.php immediately after publishing — this is the single most important file. It determines which locale directories are scanned, which loader (json vs php), the generated output path, and the shared Inertia prop name. Everything else in the package is driven from here.
2. Confirm your language file layout
The package assumes Laravel's standard lang/{locale} structure. Decide early whether you are translating with flat JSON (lang/en.json) or nested PHP arrays (lang/en/messages.php). The two produce different frontend shapes, and mixing them in one project is the most common source of "why is this key undefined" confusion. Pick one and enforce it.
3. Generate the frontend bundle
php artisan lang:sync
This scans your lang directory and emits the translation payload the frontend consumes. Run it once now and inspect the output file — seeing the generated shape (flat dot-notation keys vs. nested objects) tells you exactly how to call translations from Vue/React/Svelte.
4. Wire it into Inertia
The package hooks into the shared props layer. If it ships a middleware, register it; if it exposes a shareable class, add it to app/Http/Middleware/HandleInertiaRequests.php:
public function share(Request $request): array
{
return [
...parent::share($request),
'translations' => fn () => lang_sync()->forLocale(app()->getLocale()),
'locale' => app()->getLocale(),
];
}
Keep it in a closure. Sharing translations eagerly on every request — including JSON API calls and file downloads — is a measurable waste once your translation set grows.
5. First real use case
A locale switcher is the canonical first integration:
// routes/web.php
Route::post('/locale/{locale}', function (string $locale) {
abort_unless(in_array($locale, config('app.supported_locales')), 404);
session(['locale' => $locale]);
return back();
})->name('locale.switch');
Pair it with a middleware that reads session('locale') before app()->setLocale(), then render a dropdown in your layout that submits to that route. You now have end-to-end localization with a single generated artifact.
The default HandleInertiaRequests share sends the full translation dictionary to every page. That is fine for a 200-key app and painful for a 5,000-key one. Two mitigations:
lang/{locale}/admin.php, lang/{locale}/shop.php, etc., and share only the groups a controller needs via a ->only(['shop', 'common'])-style filter.Start global, then move to scoped sharing the first time your Inertia payload exceeds ~50 KB of JSON.
The package's auto-sync/watch mode is a developer convenience — it regenerates the bundle when you edit a lang file so HMR picks up changes without restarting Vite. Treat it as dev only. In CI and production, generate the bundle as an explicit build step before vite build:
"scripts": {
"build": "php artisan lang:sync --force && vite build"
}
This makes the translation artifact a build input rather than a runtime side effect, which is what you want for cacheable, reproducible deploys.
The cleanest pattern is a single middleware that resolves locale in a fixed priority order and nothing else touches setLocale:
/de/pricing)Accept-Language header negotiationconfig('app.locale') fallbackConsolidating this prevents the classic bug where one controller sets the locale and a queued job or an Inertia partial reload sees a different one.
Persisting locale via GET /locale/de invites crawlers and prefetchers to flip users' languages. A POST + back() keeps it a user action and preserves the current page, which is what users expect.
Wrap the injected translations in a tiny composable/helper rather than reaching into usePage().props.translations at each call site:
// resources/js/lang.js
import { usePage } from '@inertiajs/vue3'
export function t(key, replacements = {}) {
const dict = usePage().props.translations ?? {}
let value = dict[key] ?? key
for (const [k, v] of Object.entries(replacements)) {
value = value.replaceAll(`:${k}`, v)
}
return value
}
The indirection pays off the first time you need to add fallback-to-key, pluralization, or a test-mode override.
A frequently missed integration: Laravel's validation errors arrive in errors already translated server-side, so form errors are localized for free — but only if your locale middleware runs before validation. Verify it does. Client-side validation messages, by contrast, must come from the shared dictionary. Don't duplicate the strings; keep client-side copy minimal.
Payload bloat is the #1 killer. The default full-dictionary share is convenient and quietly expensive. Measure JSON.stringify(usePage().props).length on a real page before you ship, and keep an eye on it as the app grows. Inertia partial reloads only help if you're already scoping props.
Generated artifacts and .gitignore. Decide explicitly whether the synced bundle is committed or built. Committed artifacts drift silently; generated-only artifacts break local onboarding if the file isn't produced by a post-install script. The safest middle ground: gitignore it, and add php artisan lang:sync to both composer install scripts (post-autoload-dump) and the CI build.
SSR breaks here first. If you enable Inertia SSR, translations must be present in the SSR request payload. Since they arrive via shared props this usually works — but any code that reads from window, localStorage, or a browser-only locale cookie will throw during SSR. Pass the locale through props, never read it client-side.
Cache invalidation after deploy. If you cache the generated bundle (config cache, OPcache, or a CDN), a stale bundle means users see last week's copy after you fix a translation. Bust it on deploy — tie the bundle's filename or a version prop to a build hash, or simply clear the relevant cache in your deploy script.
Locale and session middleware ordering. Session-backed locale switching only works if StartSession runs before whatever sets the locale. Registering the locale middleware at the top of web produces the confusing "switcher works on refresh but not after redirect" behavior. Put it after the session group.
PHP/JSON shape mismatch. Nested PHP array files produce nested JS objects; flat JSON files produce flat objects with dot keys. If t('auth.failed') returns the raw key for one file and the translation for another, that mismatch is why. Normalize on the frontend helper, or standardize on one file format.
Key naming drift is silent. There is no compile-time link between a translation key used in a template and one defined in lang/. Add a CI check that greps for missing keys, or at minimum log misses in non-production by overriding the helper's fallback branch. Untranslated strings reaching production are the most common regression in localized apps.
Missing-key fallback semantics. Confirm whether the package falls back to config('app.fallback_locale'), to the key itself, or to an empty string. Each produces very different visible behavior, and the "returns the key" case can look like a sync failure when it's actually a typo in the template.
Pluralization is not uniform. Laravel's trans_choice and the |-separated plural forms don't map cleanly onto Intl.PluralRules, and languages with more than two plural categories (Polish, Arabic, Russian) will be wrong if you naively split on |. For anything beyond English/French/Spanish, use the package's generated forms or call back to the server; don't hand-roll count > 1 ? ... : ....
Docker/WSL watch mode. Filesystem event watchers inside containers frequently miss changes on bind-mounted volumes (inotify limits, especially on Windows/macOS hosts). If auto-sync "randomly stops working," switch the watcher to polling or just re-run lang:sync manually — don't burn an afternoon on it.
Escape untranslated interpolations. If you support :attribute-style placeholders or HTML in translations, never render them with v-html/dangerouslySetInnerHTML unless the source file is trusted and reviewed. Prefer splitting the string into parts and composing in the template.
Maturity check before betting on it. The package is young and low-adoption (66 stars, no known dependents). Pin the version in composer.json, read the source of the sync command before relying on its output format, and keep the generated bundle's shape isolated behind your own t() helper. That way, if you later swap the package for a hand-rolled lang:export artisan command, only one file changes.
How can I help you explore Laravel packages today?