What is Fallback Locale?
A fallback locale is the locale the translation layer consults when the active locale has no value for a key. If a French user hits a key that exists only in English, they see the English string instead of a raw key or an error. Fallbacks keep partially translated apps shippable while coverage catches up.
Laravel configures this as fallback_locale in config/app.php, and JSON-style translations fall back naturally because the key is the source text. Laravel's built-in fallback is a single hop, but chains can be more nuanced: a common pattern is region to language to default, so pt_BR falls back to pt before en, which Laravel supports with a small customization.
The trap is that fallbacks hide gaps. Users see mixed-language screens nobody reports, and nothing fails loudly. Treat the fallback as a safety net, not a strategy: track translation coverage per locale explicitly, and alert on missing keys rather than discovering them in production screenshots.
// config/app.php
'locale' => 'fr',
'fallback_locale' => 'en',
// Key missing in fr, present in en:
__('billing.invoice_ready'); // returns the English string
How Laravel resolves a missing key
For keys stored in PHP files, Laravel tries the active locale, then fallback_locale, and returns the key itself if neither defines it. The fallback comes from config/app.php (APP_FALLBACK_LOCALE in .env) and can be changed at runtime with App::setFallbackLocale(). To read one locale without the fallback, pass false as the fourth argument to Lang::get(), or call Lang::hasForLocale() for a yes-or-no answer.
JSON keys take a different path. Laravel looks them up in the active locale's JSON file only and never consults the fallback locale's JSON file, so a key missing from lang/fr.json renders the key text itself. That is usually fine, because the key is the English source. It stops being fine when lang/en.json rewords a key: French users then see the original key text, not the reworded English.
// lang/en/billing.php defines invoice_ready, lang/fr/billing.php does not
App::setLocale('fr');
__('billing.invoice_ready'); // "Your invoice is ready"
Lang::get('billing.invoice_ready', [], 'fr', false); // "billing.invoice_ready"
Lang::hasForLocale('billing.invoice_ready', 'fr'); // false
// lang/en.json: {"Save changes": "Save your changes"}, nothing in lang/fr.json
__('Save changes'); // "Save changes"
Region-to-language fallback chains
Out of the box, a pt_BR user who hits a key missing from lang/pt_BR jumps straight to English, even when lang/pt has a perfectly good Portuguese string. Laravel's translator accepts a callback that decides which locales to try and in what order, which is enough to insert the language-only step.
The callback receives the default list, the requested locale followed by the fallback locale, and returns the list to search. Register it in a service provider. It applies to keys in PHP files; JSON keys are still looked up in the active locale only.
// app/Providers/AppServiceProvider.php, in boot()
Lang::determineLocalesUsing(function (array $locales): array {
// ['pt_BR', 'en'] becomes ['pt_BR', 'pt', 'en']
return collect($locales)
->flatMap(fn (string $locale) => str_contains($locale, '_')
? [$locale, strstr($locale, '_', true)]
: [$locale])
->unique()
->values()
->all();
});
Making gaps visible
Lang::handleMissingKeysUsing() looks like the right tool for finding gaps, but it only fires when the whole chain fails. A key missing in French but present in English resolves through the fallback, so the callback never runs. Use it to log keys that are missing everywhere, which are bugs, and measure per-locale gaps separately.
The loop below walks every key in the English PHP files and collects the ones French does not define itself. Put it in a test or a scheduled command and fail when the list grows. The same idea works for JSON files by comparing the keys of the decoded arrays.
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Lang;
$gaps = [];
foreach (glob(lang_path('en/*.php')) as $file) {
$group = basename($file, '.php');
foreach (array_keys(Arr::dot(require $file)) as $item) {
if (! Lang::hasForLocale("{$group}.{$item}", 'fr')) {
$gaps[] = "{$group}.{$item}";
}
}
}
Choosing the fallback locale
The fallback should be the most complete locale you have, which is almost always the language developers write keys in. If the team writes source strings in English but the product launched in Spanish, English is still the right fallback, because it is the one locale guaranteed to contain every key the code uses. A fallback with gaps of its own turns one missing translation into a raw key on screen.
fallback_locale also applies to the whole translator, not just your own lang files. Validation messages, notification text and package translations go through the same lookup, so a gap in any of them shows up as text in the fallback language rather than as an error.