conedevelopment/i18n
Multilingual routing for Laravel: define a route once, then serve translated URIs and route names per locale. Includes middleware to detect and set the application locale from the URL, plus helpers for generating localized links and language switchers.
Install the package and publish its config:
composer require conedevelopment/i18n
php artisan vendor:publish --tag=i18n
This drops config/i18n.php where you control which locales, translation groups, and namespaces get exported to the front-end.
Expose the translations to your layout once. The package provides a Blade directive (typically @i18n) that injects a script definition into the page:
{{-- resources/views/layouts/app.blade.php --}}
<head>
...
@i18n
</head>
In JavaScript you now have a global __() helper that mirrors Laravel's translator:
__('messages.welcome') // "Welcome"
__('messages.greeting', { name: 'Taylor' }) // "Hello, Taylor"
First use case: replace hard-coded English strings in your Blade/JS templates with translation keys. Start with one group (resources/lang/en/messages.php), verify __('messages.some_key') resolves in the browser console, then expand.
Where to look first:
config/i18n.php — output path, included/excluded groups, fallback locale.public/vendor/i18n/translations.js or similar) — confirms what was actually exported.Inline in the Blade layout vs. separate asset
Two common approaches:
@i18n — simplest, no extra request, translations available before your JS bundle runs. Good for small-to-medium key sets.public/vendor/i18n/*.js from your layout, benefit from HTTP caching and cache busting. Better when you have hundreds of keys or multiple bundles.For inline, place @i18n before your compiled JS so __() exists when your app initializes:
@i18n
<script src="{{ mix('js/app.js') }}"></script>
Using with Vue/React
Since the helper is global, wrap it once rather than importing per-component:
// resources/js/i18n.js
export const t = (key, replacements) => window.__(key, replacements);
Then import t wherever needed. This keeps components testable (you can stub t) and avoids scattering globals.
<template>
<h1>{{ t('dashboard.title') }}</h1>
</template>
Placeholder replacements
The JS helper supports Laravel-style :attribute replacements:
__('messages.items_in_cart', { count: cart.length })
// lang: 'messages.items_in_cart' => 'You have :count items in your cart.'
For pluralization, prefer exporting explicit keys per count from the back-end or use a small pluralization helper — don't assume the JS helper handles | plural syntax identically to PHP.
Scoping which groups are exported
Keep the payload lean by including only front-end-facing groups:
// config/i18n.php
'groups' => ['messages', 'validation', 'auth'],
// or
'exclude' => ['admin', 'emails', 'pagination'],
This is the single biggest lever on page weight. Validation is usually worth including; emails and admin are usually not.
Locale switching
When a user changes language, the translated payload must change too. Typical pattern:
// routes/web.php
Route::get('/locale/{locale}', function ($locale) {
session(['locale' => $locale]);
app()->setLocale($locale);
return back();
});
Then ensure @i18n renders under the current app()->getLocale(). If you cache the inline output, key the cache by locale.
Build-time generation
If you generate a static JS file, run the generator as part of your deploy pipeline, after config caching:
php artisan config:cache
php artisan i18n:generate # or the package command name in your version
npm run production
Treat the generated file as a build artifact: add it to .gitignore, generate per environment.
Caching translations for large apps
For a big app with dozens of groups, wrap generation in a scheduled command or a deploy hook, and serve the file with long cache headers plus a hash in the filename. Avoid regenerating on every request.
Stale translations in production
The most common bug: you edit a lang file, deploy, and the front-end still shows old strings. Causes:
config:cache was run before the lang files changed (config caching doesn’t cover lang files, but the generator may have run earlier).Debug by inspecting the generated asset directly for the new key.
JSON translations vs. nested arrays
Laravel has both resources/lang/en.json (flat string keys) and resources/lang/en/*.php (nested arrays). The package exports these differently — flat JSON keys are looked up with the full string, array keys with dot notation. If __('Welcome') fails in JS but __('messages.welcome') works, check which file the key lives in. Be consistent across your app.
Missing-key behavior
When a key is absent, Laravel returns the key itself. In JS this can silently leak messages.foo into your UI. Add a dev-only assertion:
if (import.meta.env.DEV) {
const orig = window.__;
window.__ = (key, repl) => {
const out = orig(key, repl);
if (out === key) console.warn('[i18n] missing key:', key);
return out;
};
}
Escaping and HTML
The JS helper typically returns plain strings, not HTML. Don’t inject translation output with v-html unless you control the content — it’s a straightforward XSS vector if translations come from a user-editable source. For :attribute replacements containing user data, escape at the call site.
Placeholder syntax mismatches
__('msg.hi', { name }) works, but __('msg.hi', ['name' => ...]) (PHP array) will not. Keep JS call sites using object literals.
Config cache order
After changing config/i18n.php, always run:
php artisan config:clear
php artisan config:cache
in that order during deploy. Forgetting this means your changed groups/exclude list is ignored.
Extension points
Testing
Assert that every key used in JS exists in the lang files. A cheap approach: grep your resources/js for __('...') patterns and diff against array_keys from the lang files in a Pest/PHPUnit test. Catching missing keys in CI is far cheaper than catching them in production.
Version drift
Last release is 2026-04-07, so pin the package version in composer.json and re-verify the generator command name and config keys after upgrading — these have changed across major versions in similar packages. Run the test suite and check the generated asset diff on upgrade.
How can I help you explore Laravel packages today?