In Laravel, how would a middleware take the locale from a /{locale}/ URL segment without shifting every controller action's parameters?
answer
- read the route parameter
- abort on unsupported codes
- forgetParameter after reading
- URL::defaults for generated links
- prioritise before SubstituteBindings
basics
~10 sRead $request->route('locale'), reject unsupported codes, call App::setLocale(), then $request->route()->forgetParameter('locale') so actions do not receive it. URL::defaults(['locale' => $locale]) lets route() fill the segment in generated links.
solid answer
~30 sPut the public routes in a group prefixed with `{locale}` and constrain it, for example `whereIn('locale', ['en', 'de', 'pl'])`. A route middleware reads `$request->route('locale')`, aborts with a 404 for anything unsupported, and calls `App::setLocale()`. It should then call `$request->route()->forgetParameter('locale')`: Laravel passes route parameters to controller actions by position, so otherwise `show(string $slug)` would receive `de` as its slug. To keep links working it calls `URL::defaults(['locale' => $locale])`, so `route('jobs.show', $job)` fills the segment automatically. The docs advise running a middleware that sets URL defaults before `SubstituteBindings`, via `prependToPriorityList` in `bootstrap/app.php`, so implicit model binding is not disturbed.
code
php · 25 lines<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response;
class SetLocaleFromUrl
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->route('locale');
abort_unless(in_array($locale, ['en', 'de', 'pl'], true), 404);
App::setLocale($locale);
$request->route()->forgetParameter('locale');
URL::defaults(['locale' => $locale]);
return $next($request);
}
}go deeper
Know that a {locale} prefix plus a middleware calling App::setLocale() gives each language its own URL.
Explain reading the parameter, validating it, forgetParameter() and why actions receive parameters by position, plus URL::defaults() for links.
Handle the production edges: middleware priority before SubstituteBindings, the language switcher, root redirects and duplicate-content signals.
Decide which parts of the product get language URLs and which rely on stored preferences, and how that choice affects search and sharing.
## When a URL segment fits On the HR portal, the **public careers pages** are shared by link and indexed by search engines, so each language needs its own URL: `/de/jobs/senior-accountant`, `/pl/jobs/senior-accountant`. Signed-in pages can use the employee's saved preference instead; the segment is for pages that must be addressable per language. ## The route shape Group the public routes under a `{locale}` prefix and constrain the parameter so unknown codes never match: ```php Route::prefix('{locale}') ->whereIn('locale', ['en', 'de', 'pl']) ->middleware(SetLocaleFromUrl::class) ->group(function () { Route::get('/jobs/{slug}', [JobController::class, 'show'])->name('jobs.show'); }); ``` ## The middleware, step by step 1. Read the value with `$request->route('locale')`, which returns the named route parameter. 2. Check it against the supported list and `abort(404)` if it is missing or unknown, even with a route constraint in place, since the same middleware may be reused elsewhere. 3. Call `App::setLocale($locale)`. 4. Call `$request->route()->forgetParameter('locale')`. 5. Call `URL::defaults(['locale' => $locale])`. 6. Continue with `$next($request)`. ## Why forgetParameter matters Laravel's controller dispatcher passes the route's parameters to the action as a **positional list**. With a `{locale}` prefix, the parameters for `/de/jobs/senior-accountant` are `locale => de` and `slug => senior-accountant`, so: | Middleware behaviour | `JobController::show(string $slug)` receives | |---|---| | leaves `locale` in place | `$slug = 'de'` | | calls `forgetParameter('locale')` | `$slug = 'senior-accountant'` | The alternative, adding `string $locale` as the first argument of every action, works but spreads the concern across every controller. ## Generating links Every `route('jobs.show', ...)` call now needs a locale. `URL::defaults(['locale' => $locale])` sets a default for the rest of the request, so views keep calling `route('jobs.show', ['slug' => $job->slug])` and the segment is filled in. The docs warn that URL defaults can interfere with implicit model binding, and recommend prioritising such a middleware before `SubstituteBindings`: ```php $middleware->prependToPriorityList( before: \Illuminate\Routing\Middleware\SubstituteBindings::class, prepend: SetLocaleFromUrl::class, ); ``` ## Pitfalls - Registering this middleware globally: routing has not happened yet, so `$request->route()` is `null`. - Forgetting the language switcher: build its links with an explicit `locale` parameter, which overrides the default. - Duplicate content: the unprefixed root needs a redirect to one language, and each page should declare its alternates. - Trusting the segment without a check: `App::setLocale()` accepts almost any string, so an unchecked segment would silently fall back on every line. ## Combining with the stored preference The HR portal can use both mechanisms without conflict: - **Public careers pages** sit under the `{locale}` prefix and use `SetLocaleFromUrl`, so every shared link opens in the language it was shared in. - **Signed-in portal pages** have no prefix and use the employee's saved preference. - If the preference middleware is appended to the whole `web` group, it also runs on the public pages and can overwrite the URL's language. Attach it to the portal's route group instead, so no request runs both. ## Testing it - Request `/de/jobs/senior-accountant` and assert a German heading and a German link to the next job, which proves both `App::setLocale()` and `URL::defaults()`. - Request `/xx/jobs/senior-accountant` and assert a 404. - Assert that the controller receives the slug, not the locale, by checking the job title it looked up.
- Why is this a route middleware rather than a global one?Global middleware runs before routing, so `$request->route()` is still `null` and there is no `locale` parameter to read. Attached to the route group, it runs after the route has matched and its parameters are known.
- How does the language switcher link to the same page in another language?Generate the current route's URL with an explicit `locale`, for example `route(Route::currentRouteName(), ['locale' => 'pl', 'slug' => $job->slug])`. An explicit parameter wins over the value set with `URL::defaults()`.
saying these in an interview costs you the question
- Register the segment middleware globally so it runs before routing
- Route parameters reach controller actions by name, so the locale is harmless
- App::setLocale() rejects any code outside the lang folder
- URL::defaults() changes the locale for later requests too
- Every controller action must declare a $locale argument