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.
What it is, in PM terms. A ~single-responsibility HTTP middleware that reads Accept-Language, negotiates it against a configured supported-locale list, and calls Laravel's App::setLocale for that request. It is not a localization framework — it does one step of the negotiation chain and leaves routing, translation storage, URL strategy, and persistence to you.
Architecture fit. Clean. Middleware is Laravel's native seam for per-request cross-cutting behavior; the package writes to app()->getLocale(), which the translator, route() URL defaults, {locale} route binding, and Carbon all read. No domain coupling, no service-provider sprawl. The one architectural note is that locale becomes request-scoped global state mutated by a middleware — which means its correctness depends entirely on ordering and on nothing else in the stack also claiming ownership of locale.
Integration feasibility. Composer install, publish config, register on a route group. This is an afternoon of engineering, not a project. The cost is not integration; it is deciding the precedence chain and testing it.
Technical risk.
* was being treated as a literal locale code. That is a correct RFC 7231 value ("any language") reaching the resolver routinely from crawlers and privacy browsers. A package that shipped with that hole in a 3.x line tells you the edge-case test matrix is thin — expect the same class of gap around weighted wildcards (*, en;q=0.9), region variants (pt-BR vs pt_BR), case normalization, and malformed headers.Vary: Accept-Language at the CDN or reverse proxy, or you serve one visitor's language to everyone. This is the highest-severity failure mode and it is outside the package's control.Leverage for a TPM. Two things this buys beyond the feature itself: (1) it is the cheapest way to ship header-based multilingual behavior and test demand before committing translation budget, and (2) logging the raw Accept-Language header for a few weeks gives you data to rank which locales to translate next — a prioritization input you would otherwise guess at.
Key questions.
Accept-Language → app fallback → config default?Stack fit. Fits any Laravel application that either serves localized URLs (/fr/...) or returns translated API payloads. It is a natural first step for a localization program because it front-loads the decision that matters — what locales we claim to support — into a versioned config file rather than scattered controller logic.
Build vs. adopt, stated honestly. The functionality is small enough that a competent Laravel engineer could reproduce it in a day. Adopt it if (a) the test matrix in the repo is credible and (b) we are willing to keep the dependency current. Otherwise vendor a fork under our namespace and own the tests — same code, no bus-factor exposure. Time-box this decision to the spike; do not let it become a debate.
Migration path.
App::setLocale call — controllers, route model binding, view composers, jobs, mailables. The middleware only covers the HTTP entry point; if a controller sets locale afterward, you have two sources of truth and the middleware becomes noise. Consolidate to one resolution chain in one place and delete the ad-hoc calls deliberately. The 3.0.1 change also means any defensive filter you added to strip * from the header before the package saw it can be removed — but remove it consciously, since it may also have been handling q-value forms.Compatibility checks to run explicitly.
config:cache — confirm the supported-locale list is read in a cache-safe way, and that editing the config invalidates properly in each environment.route:cache — confirm dynamic {locale} constraints (built from the config list) survive route caching.Vary: Accept-Language present on every negotiated response.Sequencing.
* and xx identically.*; weighted *, fr;q=0.8; exact-match-beats-lower-q; pt-BR and pt_BR; empty header; malformed header; unsupported locale; session-pinned locale outranking the header; and the resolved-locale-not-equal-to-* assertion.web globally.Vary.^3.0.1, and treat every subsequent patch as suspect until the locale suite passes.Maintenance. Low churn, low signal. Pin exactly, automate dependency PRs, and gate every bump on the locale regression suite — the 3.0.1 precedent shows this package will change negotiation behavior on a patch version. Budget a few hours per release review; budget one engineer as the named owner.
Support. The support load does not disappear — it moves. "The site showed up in the wrong language" will still be the ticket, and it is now triaged against a middleware instead of a controller. Two concrete investments: (1) a thin subclass or wrapper over the middleware that logs the raw header and the resolved locale; (2) one line in the i18n runbook documenting wildcard handling, the fallback policy, and where to look for config:cache staleness. Support should be able to read one log line and know whether the header or the session won.
Scaling. Compute cost is negligible — one header parse per request. The real scaling cost is cache cardinality: Vary: Accept-Language multiplies CDN cache entries per URL by the number of distinct headers seen, and the long tail of header strings is effectively unbounded. Mitigate by normalizing to a small set of supported locales before the response is cached, or by negotiating at the edge. If the resolver ever falls back to enumerating all locales, that cost multiplies again.
Failure modes, ordered by how much they should worry you.
Vary serves a German page to an English user from the CDN. High severity, high blast radius, trivially preventable.* or to the wrong locale. Low severity per event, but this is the known-thin area.Ramp-up. Roughly one engineer-day to read the code, write the tests, and confirm the resolution chain; roughly one QA-day across the locale matrix and cache configuration; TPM time concentrated in the ADR (precedence, fallback policy, wildcard behavior) and the build-vs-adopt call. Call it one sprint-week end to end including canary. Define success up front: locale-mismatch ticket volume, share of traffic served in a non-default locale, and CDN cache hit ratio after Vary is in place.
How can I help you explore Laravel packages today?