Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Browser Locale Laravel Package

chaim-chv/filament-browser-locale

A Filament plugin that detects each visitor's preferred browser language and applies it as the application locale automatically. Includes middleware, configurable locale mapping, and seamless integration with Filament panels for multilingual admin interfaces.

View on GitHub
Deep Wiki
Context7

Getting Started

This package auto-detects a visitor's browser language (the Accept-Language header) and applies a matching application locale to a Filament panel, so a French visitor lands on a French admin panel without a language picker.

Install and pin the new line:

composer require chaim-chv/filament-browser-locale:^1.2

Verify the platform solves before you touch code — v1.2.0 requires PHP 8.2+, Laravel 11.28+ / 12 / 13, Filament 4 or 5, and Livewire 3 or 4. Laravel 11.0–11.27 will fail resolution on composer update, which is the most likely upgrade blocker on older projects.

Where to look first, in order:

  1. README.md — installation snippet and the exact middleware/plugin class name.
  2. config/filament-browser-locale.php (publish it) — the supported-locale whitelist and the normalization flag.
  3. src/ — find the middleware and the locale-matching method; you will want to read the matcher once, because the hyphen/underscore behavior is the thing that bites people.
  4. tests/ — the new Pest suite (19 tests) is the fastest way to see exactly which header shapes produce which locale.

Publish config, then register the middleware on your panel (typical wiring — confirm the class name against the README, as the package is small and names have moved between minor versions):

php artisan vendor:publish --tag="filament-browser-locale-config"
public function panel(Panel $panel): Panel
{
    return $panel
        ->default()
        ->id('admin')
        ->middleware([
            \ChaimChv\FilamentBrowserLocale\Middleware\SetBrowserLocale::class,
        ]);
}

First use case: add fr, de, es to the locale whitelist, ship matching lang/fr/… files, then hit /admin with Accept-Language: fr-FR,fr;q=0.9 and confirm the panel renders in French. If it renders in English, the whitelist is almost certainly the problem — not the header.

Implementation Patterns

Whitelist, don't trust the header. Always constrain the matcher to the locales you have translations for. An unrestricted list means a visitor with Accept-Language: cy-GB puts Laravel into a locale with no files, and you get raw keys in the UI. Treat the config array as the single source of truth and keep it in sync with lang/.

Persist the resolution to the session. Header parsing should not run on every Livewire round-trip. Resolve once, write the chosen locale to the session, and let a small resolver short-circuit on the stored value. This also gives you a clean extension point when you add a user preference later.

Precedence: user preference > browser > app default. Browser detection is a good default, not a good override. The moment you add a language switcher, read users.locale (or a session key set by the switcher) first, fall back to the middleware, then config('app.locale'). Otherwise a user who picks English gets snapped back to French on their next request, which reads as a bug to them.

Whitelist the base language explicitly. With the v1.2.0 fix, en-AU now correctly falls back to en. That's what you want, but it means your whitelist should include the base code (en) even if you only ever intended to support en_US — the matcher may hand you the bare form.

Test the matcher directly rather than through a panel. The new Pest + Testbench suite shows the pattern; mirror it for your own locale set:

it('resolves a regional locale to the base language', function () {
    $this->withHeader('Accept-Language', 'en-AU,en;q=0.9')
        ->get('/admin')
        ->assertOk();

    expect(app()->getLocale())->toBe('en');
});

Parametrize over fr-FR, fr, fr-CA, a header with a high-q unknown language followed by a known one, and an empty header. Those five cases cover nearly all real traffic.

Multi-panel setups: register the middleware per panel, not globally, if your public-facing panel should stay in the app default while the admin panel localizes. Registering it as global HTTP middleware affects every route including API endpoints, which you almost never want.

With Filament SPA mode / wire:navigate: the middleware runs on each HTTP request including Livewire updates, so the locale stays consistent. But translated content already rendered in the DOM won't re-render on navigation — if you need a visible language change without a hard reload, force a full page load after switching.

Gotchas and Tips

The hyphen/underscore trap — this is the bug v1.2.0 fixed. Symfony canonicalizes en-US to en_US. Before 1.2.0, if you set normalize(false), a regional locale was never matched; base-language fallback only worked incidentally via an en;q=0.9 entry in the header. If you are upgrading from 1.1.x and your configured locale keys are in the underscore form (en_US, pt_BR), retest: you may now be matching where you previously weren't, which can change what users see. Conversely, if you "fixed" it by storing keys as en-US and relying on the old quirk, that workaround may now double-handle.

Pick one canonical form and stick to it. Decide whether your config whitelist and lang/ directories use en_US or en-US, and set the normalize flag to match. Mixed forms are the single biggest source of "why is this in English" reports. Laravel's own directories are en_US-style; Symfony's default normalizer output is en-US-style.

Accept-Language: * and absent headers. Bots and curl requests often send * or nothing. Make sure the code path falls through to config('app.locale') rather than setting the locale to the literal * or an empty string.

Middleware order matters. Place the locale middleware after StartSession and after any auth middleware if you plan to read a user's stored preference. Placed too early, auth()->user() is null and you silently fall back to browser detection every request.

Laravel 11.28+ is a hard floor. If you maintain a package or a monorepo that also supports Laravel 11.0–11.27, you cannot adopt 1.2.0 everywhere. Consider a ^1.1 constraint on the older branch, or bump the whole fleet.

Filament 4 vs 5 in the same codebase. The package declares support for both, but your own panel code may not. The CI matrix (PHP 8.2–8.4 × Filament 4/5) is on the package's side, not yours — test your panel on the Filament major you actually run before bumping.

Missing translation files fail silently. The middleware setting fr doesn't create lang/fr. Laravel's translator falls back to fallback_locale and you see English with no error. When debugging, log app()->getLocale() inside a panel page and compare it to the keys present under lang/.

Extension points. Because v1.2.0 ships an actual test suite and a published config, the matcher is now safe to subclass — check whether the middleware exposes the locale-resolution as a protected method; if so, override it for custom logic (e.g. mapping zh-Hant → zh_TW) rather than forking the package. Keep that override covered by a test so a future minor release doesn't quietly change matcher semantics under you.

Maturity signal, read with care. 19 tests / 22 assertions is a real improvement over zero, but it is thin for a header-parsing parser. Don't assume q-value ordering edge cases, wildcard handling, or malformed headers are covered — add your own tests for the header shapes your traffic actually sends, and check the CI badge before trusting a patch release.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums