What's happening
When writing an integration test for an extension that ships with locale files via Extend\Locales, translation keys from the extension's own resources/locale/*.yml are never loaded into the catalogue. Calls like $translator->get('my-ext.some.key') just echo the key back.
Reproduction
In an extension's integration test:
public function setUp(): void
{
parent::setUp();
$this->extension('my-vendor-myext');
}
/** @test */
public function extension_translations_are_loaded()
{
$translator = $this->app()->getContainer()->make(
\Illuminate\Contracts\Translation\Translator::class
);
// Fails: returns 'my-vendor-myext.some.key' instead of the translated value
$this->assertNotEquals(
'my-vendor-myext.some.key',
$translator->get('my-vendor-myext.some.key')
);
}
Core translations (flarum.*, validation.*) work fine because they're registered directly by LocaleServiceProvider at the time LocaleManager is constructed.
Root cause
Extend\Locales::extend() registers translation loading via $container->resolving(LocaleManager::class, …) — a callback that only fires when the container resolves the LocaleManager singleton. Because LocaleManager is a singleton, it is resolved exactly once.
The test harness calls ExtensionManager::enable($id) before ExtensionManager::extend($container) (see OverrideExtensionManagerForTests):
foreach ($this->extensions as $extension) {
$extensionManager->enable($extension); // fires Locales::onEnable → make(LocaleManager::class) → singleton resolved NOW
}
$extensionManager->booted = true;
$extensionManager->extend($container); // only now is Locales::extend() called, but the resolving() callback arrives too late
Locales::onEnable() calls $container->make(LocaleManager::class)->clearCache(), which triggers the first resolution of the singleton — before Locales::extend() has had a chance to register its resolving() callback. From that point on, the callback can never fire.
In production, this ordering is not a problem because LocaleManager is resolved lazily at HTTP request time — well after all extenders have registered their resolving() hooks during the normal boot flow.
Impact
- Any integration test that exercises code which calls into the extension's translator returns raw translation keys
- Silently produces misleading test output (e.g. a
Subject: in a sent email containing my-ext.email.default_subject instead of the translated string) rather than failing noisily
- Affects both 1.x and 2.x — identical ordering in both
OverrideExtensionManagerForTests implementations
Current workaround in test
$locales = $this->app()->getContainer()->make(\Flarum\Locale\LocaleManager::class);
$locales->addTranslations('en', __DIR__.'/../../resources/locale/en.yml');
$locales->clearCache();
Suggested fix
Simplest surgical change: swap the two calls in OverrideExtensionManagerForTests::extend():
$extensionManager->booted = true;
$extensionManager->extend($container); // apply extenders first — Locales registers its resolving() callback
foreach ($this->extensions as $extension) {
$extensionManager->enable($extension); // now safe — when onEnable resolves LocaleManager, the callback fires
}
Need to verify this doesn't break other lifecycle assumptions (e.g. migrations running before extenders are applied), but given the current order already leads to broken locale loading, the extender-first approach aligns better with production boot sequencing.
Environment
- Reproduced on Flarum 1.8 (testing 1.8)
- Same ordering confirmed present in 2.x
What's happening
When writing an integration test for an extension that ships with locale files via
Extend\Locales, translation keys from the extension's ownresources/locale/*.ymlare never loaded into the catalogue. Calls like$translator->get('my-ext.some.key')just echo the key back.Reproduction
In an extension's integration test:
Core translations (
flarum.*,validation.*) work fine because they're registered directly byLocaleServiceProviderat the timeLocaleManageris constructed.Root cause
Extend\Locales::extend()registers translation loading via$container->resolving(LocaleManager::class, …)— a callback that only fires when the container resolves theLocaleManagersingleton. BecauseLocaleManageris a singleton, it is resolved exactly once.The test harness calls
ExtensionManager::enable($id)beforeExtensionManager::extend($container)(seeOverrideExtensionManagerForTests):Locales::onEnable()calls$container->make(LocaleManager::class)->clearCache(), which triggers the first resolution of the singleton — beforeLocales::extend()has had a chance to register itsresolving()callback. From that point on, the callback can never fire.In production, this ordering is not a problem because
LocaleManageris resolved lazily at HTTP request time — well after all extenders have registered theirresolving()hooks during the normal boot flow.Impact
Subject:in a sent email containingmy-ext.email.default_subjectinstead of the translated string) rather than failing noisilyOverrideExtensionManagerForTestsimplementationsCurrent workaround in test
Suggested fix
Simplest surgical change: swap the two calls in
OverrideExtensionManagerForTests::extend():Need to verify this doesn't break other lifecycle assumptions (e.g. migrations running before extenders are applied), but given the current order already leads to broken locale loading, the extender-first approach aligns better with production boot sequencing.
Environment