Propagate current locale to custom 404/403/500 error pages - #3726
Conversation
1222eef to
0590e45
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes locale handling for Bolt’s custom frontend error pages (404/403/500/maintenance) by recovering the locale from the URL path when Symfony routing/locale listeners don’t run, and by making “wrong locale” handling degrade safely when no route is available (e.g. forwarded sub-requests).
Changes:
- Recover locale from the first URL path segment on error pages and apply it to the request/sub-request and translator; pass locale explicitly into record rendering.
- Add
redirectToDefaultLocaleOrFallback()and update listing/single rendering to avoid redirect errors when_routeis missing. - Add functional tests for localized error/listing behavior; clean up PHPStan baseline and a few PHP 8.4 static-analysis casts.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/php/Controller/Frontend/ListingControllerTest.php | Adds coverage for redirect vs fallback when locale isn’t supported (matched vs forwarded requests). |
| tests/php/Controller/Frontend/ErrorControllerTest.php | Adds coverage for localized error records, <html lang>, and translator locale recovery. |
| src/Twig/ContentExtension.php | Adds cast to satisfy PHP 8.4 static analysis for mb_rtrim(). |
| src/Repository/ContentRepository.php | Adds casts around json_encode()/mb_trim() for PHP 8.4 static analysis. |
| src/Menu/FrontendMenuBuilder.php | Adds cast to satisfy PHP 8.4 static analysis for mb_trim(). |
| src/Entity/Translatable/BoltTranslationTrait.php | Adds cast to satisfy PHP 8.4 static analysis for mb_trim(). |
| src/Controller/TwigAwareController.php | Introduces redirect-or-fallback behavior and guards missing _route. |
| src/Controller/Frontend/ListingController.php | Uses the redirect-or-fallback behavior for listings. |
| src/Controller/ErrorController.php | Recovers locale from path for error pages; injects locales + translator; forwards locale into record rendering. |
| src/Canonical.php | Adds cast to satisfy PHP 8.4 static analysis for mb_trim(). |
| phpstan-baseline.php | Removes baseline entries made obsolete by the return-type and argument-type fixes. |
Comments suppressed due to low confidence (1)
src/Repository/ContentRepository.php:133
- Casting a
json_encode()failure to''makes the search parameter become%%, which can match everything and turn an invalid/ill-encoded query into a full-table scan. Handle thefalsecase explicitly (e.g. fall back to the raw term, or return no results) instead of silently converting it to an empty search term.
// The search term must match the format of the content in the database
// Therefore, it is JSON encoded and escaped with backslashes
// Casts: `json_encode()` is `string|false`, and on PHP 8.4 `mb_trim()` is too.
$encodedSearchTerm = addslashes((string) mb_trim((string) json_encode($searchTerm, JSON_UNESCAPED_UNICODE), '"'));
$qb->addSelect('f')
->innerJoin('content.fields', 'f')
->innerJoin('f.translations', 't')
->andWhere($qb->expr()->like($where, ':search'))
->setParameter('search', '%' . $encodedSearchTerm . '%');
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Okay, so I really wanted to take a real look at this PR and test it locally, but my time has run out working on a stupid failing a11y test (!3730), setting up a fully functional test environment with a complete nginx setup instead of the local symfony server and the recent responsible disclosures. So, rest assured, I haven't forgotten, I'm just lacking some time!
Note that I did already cherry-pick your second commit into #3730, so thank you for that 😄
ad1bd68 to
6f3e996
Compare
9b6e981 to
b9e2917
Compare
When no route matches (404/403), Symfony's LocaleListener never runs, so
custom error pages and their records rendered in the default locale and
ignored the locale in the URL (e.g. /nl/...).
- ErrorController recovers the locale from the URL path and applies it to the
request and the Twig sub-request, and syncs the locale-aware services via
the LocaleSwitcher, so both {% trans %}/__() strings and the router's
RequestContext (path()/url() in the template) follow it. The record locale
is now passed explicitly to DetailController::record().
- The path is only consulted when routing never ran: with a matched route the
locale is already set, and the first segment is a ContentType slug rather
than a locale - a ContentType `it` must not render its error page in
Italian. The sub-request and the locale-aware services are synced either
way, because ErrorListener::duplicateRequest() hands the error page a fresh
request that never saw the URL's locale.
- Add redirectToDefaultLocaleOrFallback(): when there's no route to redirect
to (error page / forwarded request), reset to the default locale and render
instead of erroring. Guards the missing _route case that previously threw a
TypeError (a 404-within-a-404).
- ListingController uses the fallback so a forwarded listing renders in the
default locale instead of erroring.
Adds ErrorControllerTest and ListingControllerTest covering unrouted and
routed 404s, default-locale rendering, translator and router-context locale
recovery, the ContentType-slug guard, an end-to-end __() assertion through a
fixture template, the 403 path, the non-localized ContentType case, the
listing redirect, and the forwarded-listing fallback.
b9e2917 to
3fab2bf
Compare
|
Rebased it, updated description and add tests screenshots |
Problem
Custom error pages (
notfound,forbidden,internal_server_error,maintenance) always rendered in the default locale, even when the URL carried one — wrong record fields, wrong<html lang>, untranslated__()strings, default-localepath()URLs.Two causes:
LocaleListenernever runs, so the locale is never set from the URL.ErrorListener::duplicateRequest()gives the error page a fresh sub-request with a replaced attribute bag, which Bolt'sLocaleSubscriberforces to the default locale — and that sub-request is what the template renders against.Plus a latent bug: the "wrong locale" path called
redirectToRoute(null, …)on a request without a matched route, throwing aTypeError— a 404-within-a-404.Solution
ErrorController::setLocaleFromPath()derives the locale from the first path segment and applies it to the request, the error sub-request, and — viaLocaleSwitcher— the translator and the router'sRequestContext. The path is only consulted when routing didn't run; otherwise the locale is already set and the first segment is a ContentType slug, not a locale (a ContentTypeitisn't Italian). The sync runs either way.attemptToRender()passes$request->getLocale()toDetailController::record().redirectToDefaultLocaleOrFallback()returnsnullwhen there's no route to redirect to, so the caller renders in the default locale instead of erroring.redirectToDefaultLocale()guards the missing_routecase and reads it from attributes rather than$request->get().Testing localization of 404 page in skeleton theme