On a multinational Laravel HR portal, how would you apply each employee's saved language on every request, falling back to the browser's Accept-Language?
answer
- one middleware, not every controller
- user column checked against an allowlist
- getPreferredLanguage with your list
- appended to the web group
- the session must start first
basics
~10 sWrite a middleware that takes $request->user()?->locale when it is supported, else $request->getPreferredLanguage($supported), and calls App::setLocale(). Append it to the web group so it runs after the session starts and the employee is known.
solid answer
~40 sPut the rule in one middleware so every page gets it. It reads the signed-in employee's saved `locale` column, uses it if it is in the supported list, and otherwise asks `$request->getPreferredLanguage($supported)`, which matches the `Accept-Language` header against your list; then it calls `App::setLocale()`. Register it in `bootstrap/app.php` with `$middleware->web(append: SetEmployeeLocale::class)`. Placement matters: global middleware runs before the `web` group's `StartSession`, so `$request->user()` would be `null` there, whereas appended to the group it runs after the session is loaded. It does not need the `auth` route middleware, because the guard resolves the user lazily from the session. Never pass the header or the column to `setLocale` unchecked.
code
php · 24 lines<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
class SetEmployeeLocale
{
private const SUPPORTED = ['en', 'de', 'pl', 'pt_BR'];
public function handle(Request $request, Closure $next): Response
{
$saved = $request->user()?->locale;
App::setLocale(in_array($saved, self::SUPPORTED, true)
? $saved
: $request->getPreferredLanguage(self::SUPPORTED));
return $next($request);
}
}go deeper
Know that a middleware calls App::setLocale() on each request, using the user's saved language or the browser header.
Explain the resolution order, getPreferredLanguage() with an allowlist, and why appending to the web group, after StartSession, is what makes user() available.
Cover the edges in production: API routes without sessions, jobs that bypass middleware, per-user caching, and updating the locale in the request that changes it.
Define one locale-resolution policy for web, API and background work across the portal's regions, and who owns the list of supported languages.
## Why a middleware An HR portal serving offices in several countries needs every page, form error and email preview in the employee's language. Setting the locale in each controller is repetitive and easy to forget, so the rule belongs in one **middleware** that runs before any controller. ## The resolution order in code For a signed-in portal, a sensible order is: 1. The employee's **saved preference**, a `locale` column on the users table set from a profile form. 2. The browser's **`Accept-Language` header**, matched against the locales you support. 3. The configured default, `app.locale`, which is already active if neither applies. `$request->getPreferredLanguage(array $locales)` does step 2. It reads the header's language ranges with their quality values and returns the best entry from the list you pass; with a non-empty list it always returns one of your entries (the first when nothing matches), so put your default first. ## Registering it in Laravel 13 The Laravel 13 skeleton has no `app/Http/Kernel.php`; middleware is configured in `bootstrap/app.php`: ```php ->withMiddleware(function (Middleware $middleware): void { $middleware->web(append: SetEmployeeLocale::class); }) ``` Where it sits decides whether it works: | Placement | Runs | Can it see the employee? | |---|---|---| | `$middleware->append()` (global) | before routing and before the `web` group | no: the session has not started, so `user()` is `null` | | `$middleware->web(append: ...)` | after `StartSession` in the `web` group | yes, for every web route | | on individual routes | only where attached | yes, but easy to miss a route | The `auth` route middleware is not required for `$request->user()` to work: the session guard loads the user lazily from the session once `StartSession` has run. For guests, `user()` is `null` and the header decides. ## Validating every source - The column should only ever hold supported codes; validate the profile form with an allowlist too. - The header is attacker-controlled, but `getPreferredLanguage()` can only return entries from your list. - `App::setLocale()` itself only rejects path characters, so an unchecked `xx` would be accepted and every lookup would fall back. ## Beyond web pages - **API routes**: the `api` group has no session. If API responses are translated, append the same middleware to the `api` group; token guards resolve the user per request. - **Queued work**: jobs never pass through HTTP middleware, so a job must carry the employee's locale itself. - **Response caching**: a page cached per URL but rendered per user language serves one employee's language to another; include the locale in any cache key. ## Pitfalls - Using `$request->setLocale()`, which only changes the request object. - Registering the middleware globally and then wondering why every signed-in employee sees the header's language. - Storing a raw header value such as `pt-BR,pt;q=0.9` in the users table instead of a normalized supported code. ## A worked example: three employees | Employee | Saved `locale` | `Accept-Language` | Result with `['en', 'de', 'pl', 'pt_BR']` | |---|---|---|---| | Payroll clerk in Berlin | `de` | `en-US,en` | `de`: the saved preference wins | | New hire in Warsaw, profile not set | `null` | `pl,en;q=0.8` | `pl`: best header match in the list | | Contractor with a Japanese browser | `null` | `ja` | `en`: no match, so the first entry | The third row is why the order of the supported list matters: it doubles as the default for visitors whose browser asks for nothing you offer. ## Saving the preference - The profile form offers only the supported codes and validates the submitted value against the same list the middleware uses. - Keep that list in one place, a config value or an enum, so the form, the middleware and any tests cannot drift apart. - Store normalized codes such as `pt_BR`, matching the directory names under `lang/`.
- Why does the middleware work without the auth route middleware in front of it?`$request->user()` asks the default guard, and the session guard loads the user lazily from the session. Once the `web` group's `StartSession` has run, a signed-in employee is found whether or not `auth` is on the route; `auth` only enforces that someone is signed in.
- An employee changes language on the profile page, but the confirmation message still appears in the old language. Why?The middleware already ran for this request with the old column value. Either call `App::setLocale()` with the new value in the update action before building the message, or redirect so the next request picks up the saved column.
saying these in an interview costs you the question
- Register the locale middleware globally so it runs as early as possible
- $request->user() is always null unless the auth middleware ran first
- Pass the raw Accept-Language header straight to App::setLocale()
- A queued job automatically runs in the locale the web middleware set
- Once set by middleware, the locale is remembered for the next request