laravel-lang/json-fallback-hotfix
Restore expected JSON translation fallback behavior in Laravel with this Laravel Lang hotfix. It ensures missing JSON keys resolve through your configured fallback locale instead of returning raw keys, helping multilingual apps stay consistent. Drop-in Composer package for affected Laravel versions.
Install: composer require laravel-lang/json-fallback-hotfix. There is no config to publish, no migration, no env var. Package discovery registers the service provider automatically; on a locked-down app verify it with php artisan about or php artisan package:discover output.
Version to land on: 2.3.0 is the current release (published 2026-03-17) and exists solely to widen the framework constraint — it adds Laravel 13 support with no code changes to the patch itself. On Laravel 13 you need ^2.3; an older 2.x constraint will not resolve. On Laravel 10–12 either version works, but install ^2.3 anyway so you're already on the line that spans your next framework bump.
Prerequisite check: open config/app.php (or config/app.php → .env via APP_FALLBACK_LOCALE) and confirm 'fallback_locale' is a locale you actually ship a lang/{locale}.json for. The hotfix has nothing to fall back to if that file doesn't exist.
Reproduce the bug before installing. This is the step people skip. With the package not installed, in php artisan tinker:
app()->setLocale('fr'); // config app.fallback_locale = 'en'
__('btn.save'); // observed: "btn.save" ← raw key leaking into the UI
If you already see the English string here, your Laravel version already behaves correctly on this path and you may not need the package at all. Confirm with a semantic key (btn.save, errors.required) rather than an English sentence — see the gotcha below.
Where to look first: the service provider class in src/ — it is small, and reading it tells you exactly which translator method is being patched (get, has, or the JSON loader). That determines what is and isn't covered. Then check the package's composer.json for the illuminate/* constraint and confirm it covers your framework version (as of 2.3.0 that ceiling includes Laravel 13).
First real use case: pick one screen that renders untranslated strings for a partially-translated locale, install, and confirm the strings flip from raw keys to fallback-locale text. That single screen is your proof of value; don't roll it out fleet-wide on faith.
Treat the fallback locale's JSON file as the canonical key set. With the hotfix in place, lang/en.json effectively becomes your schema. Every other lang/*.json is a sparse override of it. Write that down in the repo — it changes how translators and lang:add-style tooling should be used.
Guard key drift with a test, not discipline. A cheap Pest/PHPUnit test catches the case where a key exists in fr.json but was renamed in en.json:
test('locale json keys are a subset of the fallback', function () {
$base = array_keys(json_decode(file_get_contents(lang_path('en.json')), true));
$fr = array_keys(json_decode(file_get_contents(lang_path('fr.json')), true));
expect(array_diff($fr, $base))->toBeEmpty();
});
Assert the fallback contract in a feature test. This is your wiring, not the package's internals:
test('missing json key resolves through fallback locale', function () {
app()->setLocale('fr');
expect(__('btn.save'))->toBe('Save');
});
Add a second assertion that a key present in fr.json still returns French — that's the regression the fix could plausibly introduce.
Keep replacements and :attributes working. Verify placeholders survive the fallback path: __('Hello :name', ['name' => 'Ada']) under the non-fallback locale must still interpolate. Retrieval and substitution happen in different layers, so this normally works — but assert it once so a future framework bump doesn't silently break it.
Scope it: JSON only. lang/{locale}/messages.php array files already fall back natively. Don't refactor array-file translations into JSON to "benefit from the package" — JSON files can't do pluralization (trans_choice) or nested keys. Use array files where you need those; use JSON for flat UI strings.
Frontend translations: if you ship a translation bundle to Vue/React/Alpine, remember the hotfix runs server-side. Any __() you call while generating the JS bundle gets the corrected value; any lookup done client-side does not. Generate bundles per-locale server-side rather than merging at runtime.
Locale negotiation middleware should hand off cleanly: App::setLocale($negotiated), and let Laravel + this package handle the locale → fallback_locale → raw key chain. Don't hand-roll a second fallback loop in middleware.
Deploy order: composer install → php artisan optimize:clear → config:cache → queue:restart. If you cache translations or compile a JS bundle, add that step after composer install so the new provider is in effect. A 2.2 → 2.3 upgrade needs nothing beyond this pipeline; there are no migrations or config changes — but re-cache if you bumped the framework at the same time.
lang/en.json is {"Welcome back": "Welcome back"}, the raw-key fallback is the correct English string. You only see the breakage with semantic keys (btn.save), with a non-English fallback locale, or after you change the English copy. Don't conclude "we don't have this problem" from a glance at a sentence-keyed app.Lang::has() / a lint pass reporting keys missing from the active locale, otherwise coverage silently stalls. The package fixes the user-facing symptom, not the translation backlog."^2.3"-style constraints rather than 2.2.*; then composer show laravel-lang/json-fallback-hotfix in each environment to confirm what actually installed. composer why-not laravel/framework tells you whether a framework bump is blocked by this package or something else — run it before scheduling a Laravel 13 upgrade, so a stale 2.2.x pin doesn't surface as a mysterious resolution failure days later.{"btn.save": ""} in fr.json is a present key returning an empty string, not a missing key. Audit for placeholder empties after bulk translation imports — the hotfix won't rescue you there.lang/fr.json — a fr_FR locale does not automatically read fr.json unless you've configured fallback chaining. Also make sure the file is valid JSON; a trailing comma makes the loader skip the file and you'll get raw keys with no error message.app()->getLocale() and config('app.fallback_locale') first (people frequently assert the wrong pair), then Lang::has('key') vs Lang::hasForLocale('key', 'fr') to see which layer disagrees, then app('translator')->getLoader()->load('fr', 'json', '*') to confirm the file was actually loaded and parsed. php artisan config:clear before believing any config value.composer update.How can I help you explore Laravel packages today?