eolica/nova-locale-switcher
A Laravel Nova tool that adds a locale switcher to your admin panel, letting you switch the application language from the Nova UI. Useful for multilingual apps where editors need to preview content or work across locales without leaving Nova. Simple to install and configure, it integrates directly i...
1. Install and register the tool
composer require eolica/nova-locale-switcher
In app/Providers/NovaServiceProvider.php, add the tool to the tools() array so Nova renders the switcher in the sidebar:
use Eolica\NovaLocaleSwitcher\LocaleSwitcher;
public function tools()
{
return [
new LocaleSwitcher,
];
}
2. Publish and inspect the config
php artisan vendor:publish --provider="Eolica\NovaLocaleSwitcher\ToolServiceProvider"
The published config (typically config/nova-locale-switcher.php) holds the list of locales the dropdown exposes — an associative array of code => label. Look at vendor/eolica/nova-locale-switcher/src first: the package is small (a Tool class, a Vue component, and a config), and reading the source is faster than reading docs for a 13-star, 2020-era package.
3. Understand the contract before you wire anything
The description is the important part: "without any concrete localization implementation." The package only persists the chosen locale (session key) and renders the UI to change it. It does not call app()->setLocale(), does not translate your resources, and does not touch URLs. That is your job in step 4, and it is usually 10 lines of middleware.
4. First use case: your own middleware
The package's session value is only useful once something reads it on every request:
namespace App\Http\Middleware;
use Closure;
class SetLocaleFromNova
{
public function handle($request, Closure $next)
{
$allowed = array_keys(config('nova-locale-switcher.locales', []));
$locale = $request->session()->get('locale');
if ($locale && in_array($locale, $allowed, true)) {
app()->setLocale($locale);
}
return $next($request);
}
}
Register it in the web group after \Illuminate\Session\Middleware\StartSession::class in app/Http/Kernel.php. Reload Nova, switch locale, and confirm with dd(app()->getLocale()) inside a Nova resource's fields() method.
That's the whole loop: package stores it → your middleware applies it → your app's normal translation layer (Laravel lang/, spatie translatable, etc.) does the rest.
Scope the middleware to Nova only
Applying the locale globally will surprise your public site if a customer service agent's session leaks into a shared browser. Either register it just on Nova's middleware stack, or gate it:
if ($request->is('nova', 'nova/*')) {
// apply locale
}
Nova 4+ exposes Nova::middleware([...]); on Nova 3 you'll either use the web group with a path check or a custom route middleware. Prefer the explicit path check — it survives Nova upgrades.
Validate against the config, never trust the session
$locale = $request->session()->get('locale', config('app.locale'));
if (! array_key_exists($locale, config('nova-locale-switcher.locales'))) {
$locale = config('app.locale');
}
Sessions are long-lived; someone will remove 'de' from the config and a stale session will hit setLocale('de'), which then throws when a translation file is missing.
Persist per user instead of per session (the upgrade everyone wants)
Session scope means the locale dies at logout and is shared across tabs but not devices. If you need durable preference, add a locale column to users, listen for the package's switch action (or simply mirror the session value on the next request), and resolve in the middleware as $user->locale ?? session('locale'). This is a ~30-minute change and removes the main behavioural complaint about the package.
Pair it with a real translation layer
The switcher is only the control surface. Typical pairings:
lang/{locale}/*.php — no extra packages, works for Nova labels via __('...').spatie/laravel-translatable — for model attribute translation; the switcher gives you app()->getLocale().mcamara/laravel-localization — if you also want URL prefixes on the front end. Do not try to make the Nova switcher drive front-end URLs; keep the two concerns separate.Translate Nova UI strings
Nova ships its own translations. Once app()->setLocale('fr') is set, Nova's built-in labels follow automatically. Your custom label() calls need ->label(__('fields.name')) — those are evaluated at field-definition time, which is after your middleware runs, so they will reflect the switched locale correctly.
Testing
The switcher is testable without the UI by seeding the session:
$this->withSession(['locale' => 'fr'])
->get('/nova/resources/users')
->assertSee('Utilisateurs');
And test your middleware in isolation for the invalid-locale fallback path — that's where the bugs live.
Where to extend
If the dropdown's ordering or labels don't suit you, the Tool class is the extension point: subclass it, override the locales payload it serialises, and register the subclass in NovaServiceProvider. Forking the Vue component is almost never worth it for this package.
Released 2020-10-29 — treat it as a Nova 3 artifact. Zero dependents, 13 stars, no release in over four years. Before installing, check composer.json for the Nova version constraint. On Nova 4/5 you will likely need "eolica/nova-locale-switcher": "*" with a fork, or the asset pipeline (Mix → Vite) will not register the component. Budget for a fork; the source is small enough that vendoring it into nova-components/ is a legitimate option and removes the dependency risk entirely.
Assets must actually publish. If the switcher renders as a blank or missing component, it's almost always unpublished assets or a cached service provider. Run:
php artisan vendor:publish
php artisan nova:publish
php artisan optimize:clear
Then hard-refresh — Nova's Mix/Vite manifest is aggressively cached by the browser.
Session driver quirks. With SESSION_DRIVER=array (a common testing/local default) nothing persists between requests and the switcher appears broken. With cookie driver, watch the payload size. With multiple app domains, SESSION_DOMAIN must match the Nova host or the switch silently no-ops on the next request.
Octane and long-running workers. app()->setLocale() is request-scoped but the container is not always reset the way you expect. Confirm your middleware runs on every request, not once per worker bootstrap, and be explicit about resetting locale in Octane::tick/listeners if you set it anywhere global.
Queued jobs, notifications, and mail ignore the session. A job dispatched from Nova runs in a worker with no session — it will fall back to config('app.locale'). Pass the locale explicitly:
dispatch((new SendInvoice($invoice))->locale(app()->getLocale()));
Laravel's ->locale() on mailables and notifications is the correct hook here; don't try to read the session inside the job.
Carbon and number formatting are separate. app()->setLocale('fr') does not automatically localise diffForHumans() or Number::format() in every version. Set Carbon::setLocale() in the same middleware if you rely on it, and check intl extension availability — Number::format needs it.
Never trust the switch endpoint. If the package's route accepts a locale parameter, confirm it validates against the config allow-list server-side. An unvalidated locale reaching setLocale() is a path-traversal-ish smell once you start doing lang_path($locale).
Debugging checklist, in order:
php artisan route:list | grep -i locale — does the switch route exist and use the web (session) group?Set-Cookie, and whether the session cookie actually changed.session('locale') vs app()->getLocale() — if these disagree, your middleware isn't running or runs too early.php artisan config:clear — a cached config is the single most common cause of "I added a locale and it didn't appear."Opportunity note: the package's low score reflects low adoption, not poor design — it does one narrow thing. The gap worth filling is the durable per-user locale, which the package deliberately leaves to you. Implement that with a users.locale column plus a small middleware and you've got the 90% solution without maintaining a fork of the UI.
How can I help you explore Laravel packages today?