Localization
Translation files, __() and trans_choice, locale switching and translated validation messages.
Localization means keeping every user-facing string out of your code and in translation files, so the same app can speak several languages. Laravel's translator handles placeholders, plurals and validation messages, and picks the language from the current locale. This lesson uses a real demo endpoint with English, French and Spanish strings.
Try it: the same strings in three languages
GET /api/demo/i18n sets the locale you pick, then renders a greeting with a placeholder, a pluralised count, a string from a JSON file and a real validation error. Change the locale, the count and the name.
Where translations live
Translation files sit in the lang/ folder at the project root. A fresh Laravel app doesn't have one: run php artisan lang:publish to create it with the framework's English files (validation, auth, pagination, passwords). This app's files:
There are two styles, and you can mix them:
| Style | File | Call |
|---|---|---|
| Short keys | lang/fr/tasks.php returns an array |
__('tasks.greeting') |
| Strings as keys | lang/fr.json maps English to French |
__('Your tasks') |
Short keys suit structured text such as labels and messages. JSON files suit a site written in English first: the English string is the key, so an untranslated string still reads correctly.
lang/fr/tasks.php
return [
'greeting' => 'Bon retour, :name !',
'open_tasks' => '{0} Vous n’avez aucune tâche ouverte|{1} Vous avez une tâche ouverte|[2,*] Vous avez :count tâches ouvertes',
'priority' => [
'low' => 'Priorité basse',
'medium' => 'Priorité moyenne',
'high' => 'Priorité haute',
],
];
lang/fr.json
{
"Your tasks": "Vos tâches"
}
Translating strings with __()
__() looks the key up in the current locale, falls back to fallback_locale (English here) if it's missing, and returns the key itself if nothing matches.
__('tasks.greeting', ['name' => 'Ben']); // "Bon retour, Ben !"
__('tasks.priority.high'); // "Priorité haute" — dots reach into nested arrays
__('Your tasks'); // "Vos tâches" — from lang/fr.json
Placeholders start with a colon. :Name capitalises the value and :NAME upper-cases it.
In Blade, __() goes inside the normal echo tags so the output is still escaped:
<h1>{{ __('Your tasks') }}</h1>
<p>{{ __('tasks.greeting', ['name' => auth()->user()->name]) }}</p>
@lang('tasks.priority.low')
Plurals with trans_choice()
Languages count differently, so a plural string lists its forms separated by |. Ranges in braces and brackets pick the form:
// '{0} You have no open tasks|{1} You have one open task|[2,*] You have :count open tasks'
trans_choice('tasks.open_tasks', 0, ['count' => 0]); // "You have no open tasks"
trans_choice('tasks.open_tasks', 1, ['count' => 1]); // "You have one open task"
trans_choice('tasks.open_tasks', 12, ['count' => 12]); // "You have 12 open tasks"
Without ranges, 'apple|apples' uses the language's own plural rules. The ranges make the zero case explicit, which reads better.
Translated validation messages
Validation messages come from lang/{locale}/validation.php. This app's French file overrides only the two rules the demo uses, plus a friendly name for the title field:
lang/fr/validation.php
return [
'required' => 'Le champ :attribute est obligatoire.',
'min' => [
'string' => 'Le champ :attribute doit contenir au moins :min caractères.',
],
'attributes' => [
'title' => 'titre',
],
];
So with the locale set to fr, Validator::make(['title' => 'Hi'], ['title' => ['required', 'min:3']]) produces "Le champ titre doit contenir au moins 3 caractères.". Any rule not in the file falls back to the English message. For a full translation, community packages such as laravel-lang/lang ship complete files for dozens of languages.
Setting the locale per request
The locale is app-wide state, set with App::setLocale(). The demo controller does exactly that, after checking the requested locale against an allow-list:
app/Http/Controllers/Api/LocalizationDemoController.php
$validated = $request->validate([
'locale' => ['nullable', Rule::in(self::LOCALES)],
'count' => ['nullable', 'integer', 'between:0,1000'],
'name' => ['nullable', 'string', 'max:40'],
]);
App::setLocale($validated['locale'] ?? 'en');
In a real site you'd do this once in middleware, reading the user's saved preference, the session or the browser's Accept-Language header (illustrative — not in this app):
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->user()?->locale
?? session('locale')
?? $request->getPreferredLanguage(['en', 'fr', 'es']);
App::setLocale($locale);
return $next($request);
}
}
lang/, and an unknown value would just fall back to English anyway.
Dates and numbers are separate from strings. now()->locale('fr')->translatedFormat('l j F') gives "lundi 19 octobre", and Laravel's Number::currency(1234.5, in: 'EUR', locale: 'fr') formats money the French way (it needs PHP's intl extension).
Key takeaways
- Keep user-facing text in
lang/: short-key PHP files for structured strings, JSON files when the English sentence is the key. Runphp artisan lang:publishto start the folder. __('file.key', ['name' => …])translates with:placeholders; use it inside{{ }}in Blade so output stays escaped.trans_choice()picks a plural form using{0},{1}and[2,*]ranges.- Validation messages and attribute names come from
lang/{locale}/validation.php, falling back to English key by key. - Set the locale per request with
App::setLocale(), usually in middleware, and only from an allow-list.