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

Laravel Accept Language Middleware Laravel Package

orkhanahmadov/laravel-accept-language-middleware

Lightweight Laravel middleware that reads the Accept-Language request header and sets the application locale automatically. It parses language preferences, matches them against your configured locales, and falls back gracefully. Easy to register and works without configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install the package with Composer, then publish its config:

    composer require orkhanahmadov/laravel-accept-language-middleware
    php artisan vendor:publish --tag=accept-language
    
  • Open the published config first. It contains the supported locales list and the fallback locale — these two values determine every outcome the middleware can produce. Match supported_locales to the locales your lang/ (or resources/lang/) directories actually contain; a mismatch produces silent fallbacks rather than errors.

  • Register the middleware. For a site that should honour the header everywhere, add it to the web group in app/Http/Kernel.php (Laravel 10 and earlier) or bootstrap/app.php (Laravel 11+). For API-only usage, attach it to the api group. If you only want negotiation on a subset of routes, alias it and apply per route group instead — that keeps untranslated admin routes deterministic.

  • Verify it works with a single request:

    curl -H "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8" https://your-app.test/
    

    Echo app()->getLocale() from a test route or log it in a temporary middleware to confirm the resolved value before building on top of it.

  • First real use case: a public marketing site or API where anonymous visitors should land in their browser language, while logged-in users get a stored preference. Start with the header-only case, then layer user preference on top (see the next section) — that ordering is the one design decision you will live with.

Implementation Patterns

  • Keep one explicit resolution chain. The middleware handles Accept-Language, but production apps almost always need more inputs. Settle the order once — route {locale} segment → session/user preference → Accept-Language → fallback → config('app.fallback_locale') — and implement it in a single place (a small LocaleResolver service, or a subclass of the middleware). Scattering this logic across middleware, controllers, and Blade is where locale bugs breed.

  • Pin the negotiated locale to the session. A header-only negotiation is stateless: the next request may arrive with a different or absent Accept-Language, and the user flips language. After resolving, store the result and prefer the stored value on subsequent requests:

    $locale = session('locale') ?? $this->negotiate($request);
    app()->setLocale($locale);
    session(['locale' => $locale]);
    

    Provide an explicit language-switcher route that writes the session key and redirects back, so users can override the header.

  • Localised route groups with a where constraint. Derive the constraint from the same supported-locale list the package uses, so middleware and router can never disagree:

    Route::group([
        'prefix' => '{locale}',
        'where'  => ['locale' => implode('|', array_keys(config('accept-language.supported_locales')))],
    ], function () {
        // routes
    });
    
    URL::defaults(['locale' => app()->getLocale()]);
    

    URL::defaults means route('dashboard') keeps the current locale without passing it explicitly everywhere.

  • Set Vary: Accept-Language on negotiated routes. Any response whose content depends on the header must advertise it, or a shared cache or CDN will serve the wrong language:

    $response = $next($request);
    $response->headers->set('Vary', 'Accept-Language', false);
    

    Do this in the middleware (or a wrapper) whenever you cache, and remember to include / variants in any CDN cache key if you also prefix by locale.

  • Prefer the container/config extension points over editing vendor code. If the package exposes a negotiator binding or a config key for the resolver, swap the class rather than subclassing the middleware. Extension through config survives upgrades; copy-pasted vendor code does not.

  • Integration with APIs: for JSON APIs, return the negotiated locale in a response header (Content-Language), and use it for message translation and date/number formatting. This makes client-side debugging trivial and is a one-line addition to the middleware's response handling.

  • Test matrix worth committing:

    • Accept-Language: fr → fr
    • Accept-Language: de, fr;q=0.8 → de
    • Accept-Language: xx, fr;q=0.5 → fr (unsupported first choice)
    • Accept-Language: xx → fallback
    • missing header → fallback
    • Accept-Language: * → not * (the 3.0.1 behaviour)

Gotchas and Tips

  • Header-negotiated pages and HTTP caching are a classic trap. If you cache or use a CDN without Vary: Accept-Language, one visitor's German page is served to everyone. If you cannot control the cache, negotiate only on uncached routes or move locale into the URL instead of the header.

  • Region and case variants are not normalised for you. pt-BR, pt_BR, pt-br, and bare pt are distinct strings. If your supported list contains pt_BR, a browser sending pt-BR will not match it. Either store matches in the format the package compares against, or add a normalising step in a custom resolver before the lookup.

  • Wildcards and malformed tags beyond *. The 3.0.1 release ignores a bare * as a locale code — good — but *, en;q=0.9, en-*, and und are separate concerns. Test them. If users report en-*-style values leaking into app()->getLocale(), harden at the resolver rather than filtering the raw header upstream (your filter may also swallow q-values).

  • Session beats header — by design, but surprising in tests. If a test asserts the header wins but the session says otherwise, the test is wrong, not the package. Clear the session ($this->flushSession()) in tests that exercise header negotiation in isolation.

  • config:cache staleness. After editing supported_locales, run php artisan config:clear (dev) or re-run config:cache (prod). A stale cached list is the number one cause of "I added the locale and it still doesn't work."

  • Debugging locale surprises: log the raw Accept-Language header and the resolved locale at the end of the middleware, ideally tagged with a request ID. This turns a two-hour investigation into a one-line grep:

    Log::debug('locale', ['header' => $request->header('Accept-Language'), 'resolved' => app()->getLocale()]);
    
  • Decide unsupported-locale behaviour explicitly. Does Accept-Language: xx fall back silently, redirect, or 404? Pick one and apply it consistently to the wildcard case too. Silent fallback is fine for a marketing site and wrong for an API that promises a specific locale.

  • Long-running workers and Octane. The middleware sets locale per request; verify under Octane that no locale is held in static state. Run a two-request test with different headers against the same worker, asserting the second request gets its own locale — this catches leakage that never appears under php artisan serve.

  • Extension points to know: the published config (locale list, fallback), the negotiator/resolver binding if present, and the middleware's handle method. Overriding handle in a subclass is the least invasive hook for logging or A/B experiments, but re-read it on every minor release — it sits directly in the locale resolution path, so a regression there is user-visible and slow to trace.

  • Pin to ^3.0 and re-run your locale test suite on patch bumps. The 3.0.0 → 3.0.1 change altered behaviour in a patch release; treat future patches as potentially behaviour-affecting and let the tests be the gate.

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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log