Skip to content

Propagate current locale to custom 404/403/500 error pages - #3726

Open
Vondry wants to merge 1 commit into
bolt:6.2from
Vondry:feature/localized-error-pages
Open

Propagate current locale to custom 404/403/500 error pages#3726
Vondry wants to merge 1 commit into
bolt:6.2from
Vondry:feature/localized-error-pages

Conversation

@Vondry

@Vondry Vondry commented May 25, 2026

Copy link
Copy Markdown
Contributor

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-locale path() URLs.

Two causes:

  • On an unrouted 404/403, LocaleListener never runs, so the locale is never set from the URL.
  • On a routed error, ErrorListener::duplicateRequest() gives the error page a fresh sub-request with a replaced attribute bag, which Bolt's LocaleSubscriber forces 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 a TypeError — 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 — via LocaleSwitcher — the translator and the router's RequestContext. 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 ContentType it isn't Italian). The sync runs either way.
  • attemptToRender() passes $request->getLocale() to DetailController::record().
  • redirectToDefaultLocaleOrFallback() returns null when there's no route to redirect to, so the caller renders in the default locale instead of erroring. redirectToDefaultLocale() guards the missing _route case and reads it from attributes rather than $request->get().

Testing localization of 404 page in skeleton theme

Screenshot 2026-08-17 at 12 48 30 Screenshot 2026-08-17 at 12 48 46 Screenshot 2026-08-17 at 12 48 19

@Vondry
Vondry force-pushed the feature/localized-error-pages branch 2 times, most recently from 1222eef to 0590e45 Compare May 25, 2026 16:55
@bobvandevijver
bobvandevijver requested a review from Copilot May 29, 2026 19:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _route is 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 the false case 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.

Comment thread src/Controller/ErrorController.php
Comment thread tests/php/Controller/Frontend/ErrorControllerTest.php

@bobvandevijver bobvandevijver left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 😄

@Vondry
Vondry requested a review from bobvandevijver June 1, 2026 07:17
@bobvandevijver bobvandevijver added this to the Bolt 6.2 milestone Jul 4, 2026
@Vondry
Vondry force-pushed the feature/localized-error-pages branch from ad1bd68 to 6f3e996 Compare July 8, 2026 12:55
@Vondry
Vondry force-pushed the feature/localized-error-pages branch 3 times, most recently from 9b6e981 to b9e2917 Compare August 17, 2026 09:44
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.
@Vondry
Vondry force-pushed the feature/localized-error-pages branch from b9e2917 to 3fab2bf Compare August 17, 2026 10:10
@Vondry

Vondry commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Rebased it, updated description and add tests screenshots

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants