[2.x] fix: resolve translation references in implicitly-loaded fallback catalogues - #5023
Merged
imorland merged 1 commit intoSep 2, 2026
Conversation
…alogues
Flarum's Translator::getCatalogue() only ran `=>`-reference resolution
(parseCatalogue()) when the requested locale was not yet present in
$this->catalogues. But Symfony's Translator::loadFallbackCatalogues()
pre-stores the *original* fallback catalogue in $this->catalogues while
attaching only a fresh copy to the requesting locale's catalogue.
So loading e.g. 'de' first stored a raw, never-parsed 'en' original;
a later getCatalogue('en') found the locale already set, skipped
parsing, and returned it with unresolved references. Consumers that
snapshot the catalogue - most damagingly Frontend\AddTranslations,
which compiles the locale JS assets - then emitted literal
"=> core.admin.dashboard.title" strings, and the poisoned compiled
asset persisted until a cache flush (revisions gate recompiles).
Track parsed state per catalogue *object* (WeakMap) instead of per
locale name, and walk the fallback chain on every access. The raw
stored original and its parsed copy share a locale name, so a
locale-keyed flag cannot distinguish them; per-object tracking parses
each catalogue object exactly once. parseCatalogue() is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
imorland
approved these changes
Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes unresolved translation references leaking into compiled locale JS assets.
Symptom
Compiled locale assets (e.g.
admin-en.js) sometimes contain unresolved translation references — literal values like:The admin UI then displays raw
=> core.admin.…strings. Observed in production on Flarum 1.8.19 (community.sbb.ch); the affected logic is identical on2.xand1.x. Characteristics:en, hardcoded viasetFallbackLocales(['en'])).Root cause
Flarum\Locale\Translator::getCatalogue()decided whether to run=>-reference resolution (parseCatalogue()) with a locale-keyed flag:But Symfony's
Translator::loadFallbackCatalogues()(same pattern verified on symfony/translation v5.4 and v7.4), when loading any other locale, pre-stores the original fallback catalogue in$this->catalogues['en']while attaching only a fresh copy to the requesting catalogue:So loading
defirst stores a raw, never-parsedenoriginal. Flarum's parse-walk resolvesdeand the copy — the stored original stays raw. A latergetCatalogue('en')finds the locale already set, skips parsing, and returns the raw original. Any consumer that snapshots the catalogue — most damaginglyFlarum\Frontend\AddTranslations, which compiles the locale JS assets — emits the literal=> …strings.Trigger shape in production: any request that touches a non-
encatalogue before compiling anenasset (forums whose default locale is noten, or cross-locale rendering such as notification emails).Reproduction
Deterministic 12-line script (run from the monorepo root after
composer install), mirroringLocaleServiceProvider's wiring — full script in the test file added by this PR; essence:Also confirmed end-to-end on a live Flarum 1.8.19 install: a script that calls
getCatalogue('de')before force-recompiling the English admin locale asset produces literal=> …strings in the real admin UI; the identical script minus that one line produces a clean asset.Fix
Track parsed state per catalogue object (
WeakMap) instead of per locale name, and walk the fallback chain on everygetCatalogue()call. The raw stored original and its parsed copy share a locale name, so a locale-keyed flag cannot distinguish them; per-object tracking parses each catalogue object exactly once. Already-parsed objects cost oneWeakMaplookup per call.parseCatalogue()is unchanged (and idempotent, so even a double parse would be harmless).Note: reference resolution remains in-memory only, by design — the on-disk catalogue cache files always contain raw references. The fix guarantees the in-memory parse for every object handed out.
Tests
New
TranslatorReferenceResolutionTest, wired likeLocaleServiceProvider(non-debug, cache dir,enfallback):enloaded directly → references resolved (regression guard, passed before).enfirst loaded implicitly as a fallback ofde, then requested directly → references resolved (failed before this fix with exactly'=> bar'vs'Resolved').deis resolved, both viagetFallbackCatalogue()and viatrans()fallthrough (regression guard).Full unit suite and PHPStan pass.
Context
Surfaced while working on FriendsOfFlarum/redis#34 — multi-instance cache invalidation increases recompile frequency, raising exposure — but the bug is reproducible on a bare single-instance install. A
1.xbackport PR (PHP 7.3-compatible,SplObjectStorageinstead ofWeakMap) follows.🤖 Generated with Claude Code