badinansoft/nova-language-switch
A Laravel Nova tool that adds a language switcher to the Nova admin panel, letting you change the interface locale on the fly. Supports configurable languages, remembers the selection, and integrates cleanly with Nova's localization.
Prerequisites. Laravel Nova 4 or 5 (release 3.0.0 added Nova 5 support), plus a Laravel version inside the package's Composer constraints. Nova is licensed; there is no fallback path here — if the admin panel isn't Nova, this package does nothing for you.
Install and register.
composer require badinansoft/nova-language-switch
The service provider is auto-discovered. The substantive step is registering the tool with Nova:
// app/Providers/NovaServiceProvider.php
public function tools()
{
return [
new \Badinansoft\NovaLanguageSwitch\NovaLanguageSwitch(),
// ...
];
}
Confirm the exact FQCN against vendor/badinansoft/nova-language-switch/src before pasting — small packages rename classes between majors, and the README lags the release.
Configure the locale list. Publish the config and restrict it to locales you actually ship translations for:
php artisan vendor:publish --tag=nova-language-switch-config
// config/nova-language-switch.php
return [
'languages' => [
'en' => 'English',
'fr' => 'Français',
'de' => 'Deutsch',
],
];
A dropdown with twelve entries where eight resolve to English strings is worse than a dropdown with three.
Find the session key first. Everything downstream depends on knowing what the package writes and where. This one command is worth more than the README:
grep -rn "session\|setLocale\|put(" vendor/badinansoft/nova-language-switch/src
You are looking for whether it writes a session key, calls App::setLocale(), or persists to a user column. That answer determines whether the switch survives the next request at all.
First use case. The highest-value day-one use is QA and translation verification: flip locale, reload a resource index, check that Nova's chrome and your resource labels render correctly. Add a second locale to config/nova-language-switch.php, click through, and you'll know within five minutes whether the package is wired into your app's locale resolution or just painting the dropdown.
Session → middleware → app locale. The package switches the value; your middleware is what makes subsequent requests honour it. This is the piece the package does not ship for you (verify — if it does ship middleware, register it instead of writing your own):
class SetLocaleFromSession
{
public function handle($request, Closure $next)
{
$supported = array_keys(config('nova-language-switch.languages', []));
$locale = $request->session()->get('locale'); // confirm key from source
if ($locale && in_array($locale, $supported, true)) {
app()->setLocale($locale);
}
return $next($request);
}
}
Register it in the web group after StartSession. Validate against the configured list — a tampered or stale session value should fall back silently, not throw inside setLocale.
Durable preference on top of the session. Session-scoped locale resets on logout, which generates "the switch is broken" tickets. If you want it to stick, persist it and treat the session as a cache:
// on login / Nova login event
$user->update(['locale' => session('locale', config('app.locale'))]);
Then in middleware: $locale = $request->user()?->locale ?? session('locale') ?? config('app.locale');. This is the decision that actually drives the schedule — session vs. users.locale is your call, not the package's.
Gate it behind a Nova permission. Wrap the tool registration so ops staff who don't need it never see it:
public function tools()
{
return array_filter([
Gate::allows('viewLanguageSwitch')
? new NovaLanguageSwitch()
: null,
]);
}
Nova renders the header on every page; an unused dropdown is noise for everyone who isn't multilingual.
Keep the config list derived, not hardcoded. If your app already has a canonical locale list (config/app.php, a Locale enum, a locales table), generate the package config from it. Two lists that drift means an admin can select a locale your lang/ directory doesn't have.
Testing. One end-to-end test buys most of the insurance:
public function test_locale_switch_persists_across_requests()
{
$this->withSession(['locale' => 'fr'])
->get('/nova/dashboard')
->assertSee('Tableau de bord'); // or assert app()->getLocale()
}
Add a second assertion that an unsupported locale in the session falls back to config('app.locale') rather than leaking.
Stack it with translation packages, don't replace them. This is admin-chrome only. spatie/laravel-translatable for content models and mcamara/laravel-localization for frontend routing are orthogonal — pairing them is expected, and the package's value doesn't shrink because they're present.
Scope is narrower than the description implies. "Switch language in your application" means the resolved locale on subsequent requests — not translated models, not translated storefront copy, not locale-aware URLs. Put a tooltip on the tool ("changes interface language, not content") and you'll pre-empt the majority of support tickets.
Nova's own translations may not exist for your locale. Nova ships a fixed set of UI translation bundles. app()->setLocale('xx') makes your app strings German while Nova's chrome stays English — a mixed-language UI that looks like a bug. Verify by actually clicking through before demoing.
Locale bleed under Octane and queues is the silent killer. App::setLocale() mutates worker state. If a request sets fr and the worker isn't reset, the next request — or a queued job that sends mail — inherits it. Two habits: set locale unconditionally at the top of middleware (never conditionally leave a previous value in place), and reset explicitly in job handle() or a job middleware. Write one test that asserts a job runs in the app default, not the previous request's locale.
Cache keys need the locale in them. Session-backed locale plus a shared response cache produces the wrong language served to the wrong admin. Include the locale in the cache key or vary on it; Vary: Accept-Language is the wrong header here because the value is session-scoped.
Session store durability. With multiple app servers and the file session driver, the locale ping-pongs as requests hit different nodes. Pin to Redis, or persist to users.locale — the latter also solves the logout-reset complaint in one move.
The silent no-op is the most likely failure. If your app reads locale from users.locale in a middleware but the package writes only to the session, the Nova dropdown appears to work and the front end ignores it. Symptom: switch works inside Nova, doesn't persist to the dashboard or emails. Confirm the write target and the read target are the same channel before you call it integrated.
Nova major bumps are a recurring line item. Nova's tool API has broken between majors, and this is a single-maintainer, ~200-line package. Assume you may need to fork it into app/Nova/Tools at the next upgrade. That's fine — the exit cost is an afternoon — but budget the review at each Nova bump rather than being surprised by a hard error in the admin panel.
When the package stops paying for itself. The moment you add users.locale, a migration, a login hook, and custom middleware, you've written more than the package does. Vendoring the tool class and adapting it is a legitimate first option here, not a fallback — it collapses the vendor-risk question and leaves you owning ~200 readable lines instead of a dependency with 26 stars and zero dependents.
Watch the Composer constraint matrix before committing. Check laravel/nova and laravel/framework constraints in the package's composer.json against your pinned versions. A release that "adds Nova 5 support" doesn't guarantee it declares the constraint correctly, and composer why-not is faster than debugging a missing tool at runtime.
How can I help you explore Laravel packages today?