Skip to content

Extend\Locales translations never register in integration tests (1.x + 2.x) #4600

@imorland

Description

@imorland

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions