From 928173e8be8a1ff8a2f29447fca7cb6a3f3e90ac Mon Sep 17 00:00:00 2001 From: ignacionelson Date: Sun, 16 Aug 2026 14:44:58 -0300 Subject: [PATCH] Let a package's translations reach the screen it wrote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend's catalogue was read straight out of lang/{locale}.json, so it held exactly the strings this repository owns. That was true for as long as this repository owned every screen — but the companion packages own several: Branding, Custom Assets, the whole v1 import. Their strings have been rendering in English in all sixteen languages, in silence, because a package catalogue registered through loadJsonTranslationsFrom() never got as far as the browser. Asked of the framework's own loader now, which is where that registration already lands. Same answer as before for this installation — no package registers a path today, and the merged result is byte-identical to the file — and the right answer the moment one does. Precedence comes free and is the useful way round: the loader merges the application's own catalogue last, so an installation can override a package's wording without editing the package. There is a test for that, because it is the kind of ordering that gets reversed by accident. One thing the test needed and is worth knowing: SetLocale honours an account's chosen language only while that language is enabled for the installation, and the Settings cache outlives RefreshDatabase. A test that sets users.locale and assumes it takes effect gets English and a very confusing failure. Co-Authored-By: Claude Opus 5 --- app/Http/Middleware/HandleInertiaRequests.php | 20 +++-- .../Platform/PackageTranslationsTest.php | 76 +++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/Platform/PackageTranslationsTest.php diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 88af8977..ea04697c 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -249,6 +249,18 @@ class HandleInertiaRequests extends Middleware * App strings use English text as the translation key, so "en" ships no * messages — the key itself is the fallback. * + * Asked of the framework's own loader rather than read out of + * lang/{locale}.json directly, so that a package which registers its + * catalogue with loadJsonTranslationsFrom() reaches the frontend too. + * Reading the file worked for as long as every translatable string + * belonged to this repository; the companion packages own screens of + * their own, and theirs were rendering in English in every language + * because their catalogue never got this far. + * + * Precedence comes from the loader and is the useful way round: an + * installation's own lang/{locale}.json is merged last and therefore + * wins, so a package string can be overridden locally. + * * @return array */ protected function translations(string $locale): array @@ -257,13 +269,7 @@ class HandleInertiaRequests extends Middleware return []; } - $path = lang_path("{$locale}.json"); - - if (! is_file($path)) { - return []; - } - /** @var array */ - return json_decode((string) file_get_contents($path), true, flags: JSON_THROW_ON_ERROR); + return app('translator')->getLoader()->load($locale, '*', '*'); } } diff --git a/tests/Feature/Platform/PackageTranslationsTest.php b/tests/Feature/Platform/PackageTranslationsTest.php new file mode 100644 index 00000000..73bde99f --- /dev/null +++ b/tests/Feature/Platform/PackageTranslationsTest.php @@ -0,0 +1,76 @@ +set(Setting::EnabledLocales, ['en', 'es']); + + $this->admin = User::factory()->create(['locale' => 'es']); + + $this->packageLang = sys_get_temp_dir().'/package-lang-'.bin2hex(random_bytes(6)); + File::makeDirectory($this->packageLang); +}); + +afterEach(function () { + File::deleteDirectory($this->packageLang); +}); + +function packageCatalogue(string $dir, array $entries): void +{ + File::put($dir.'/es.json', json_encode($entries, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + + app('translator')->getLoader()->addJsonPath($dir); +} + +function sharedTranslations(User $user): array +{ + return test()->actingAs($user)->get('/dashboard')->viewData('page')['props']['translations']; +} + +test('a package catalogue reaches the frontend', function () { + packageCatalogue($this->packageLang, ['Attribution settings saved.' => 'Ajustes de atribución guardados.']); + + expect(sharedTranslations($this->admin)) + ->toHaveKey('Attribution settings saved.', 'Ajustes de atribución guardados.'); +}); + +test('the application keeps its own strings', function () { + packageCatalogue($this->packageLang, ['Something from a package' => 'Algo de un paquete']); + + $translations = sharedTranslations($this->admin); + + expect($translations)->toHaveKey('Something from a package') + // Any string this repository owns, still translated as before. + ->and($translations['Dashboard'] ?? null)->toBe('Panel de control'); +}); + +// The useful way round: an installation can override a package's wording +// in its own catalogue without editing the package. +test('the installation wins when both define the same key', function () { + packageCatalogue($this->packageLang, ['Dashboard' => 'From the package']); + + expect(sharedTranslations($this->admin)['Dashboard'])->toBe('Panel de control'); +}); + +// English is the key, so there is nothing to send and nothing to merge. +test('english still ships no catalogue at all', function () { + packageCatalogue($this->packageLang, ['Attribution' => 'Should never be sent']); + + $english = User::factory()->create(['locale' => 'en']); + + expect(sharedTranslations($english))->toBe([]); +});