Локализация Laravel: рабочие приёмы из практики
Как хранить переводы, чтобы не утонуть в массивах, где ломается смена языка в очередях и почему __() иногда возвращает ключ вместо строки. Разбор подходов на Laravel 11.x.

Задача звучит просто: показать интерфейс на русском и английском. Но как только в проекте появляется больше двадцати строк перевода, письма из очередей, форматирование дат и валидация форм, наивный подход через файлы resources/lang/*.php начинает трещать. Строки дублируются, часть остаётся непереведённой, а в фоновых задачах язык внезапно сбрасывается на дефолтный. Разберём, где очевидные решения дают сбой и чем платит каждый способ хранения переводов. Всё проверено на Laravel 11.x, но большая часть применима начиная с 9.x.
Где Laravel хранит переводы и почему это два разных мира
В Laravel есть две несовместимые по стилю системы переводов, и путаница между ними — источник половины багов локализации.
- Короткие ключи — массивы в файлах
lang/en/messages.php, доступ через__('messages.welcome')илиtrans('messages.welcome'). Если строки нет — вернётся сам ключmessages.welcome. - Строки как ключи — JSON-файлы
lang/en.json, где ключ и есть английский текст:__('Welcome to our store'). Если перевода нет — вернётся исходная английская строка.
Начиная с Laravel 9 каталог переводов переехал из resources/lang в корневой lang. В новых проектах его нужно опубликовать командой php artisan lang:publish — по умолчанию папки может не быть вовсе. Проверьте актуальное расположение в официальной документации по локализации, если работаете на промежуточной версии.
Правило, которое экономит часы отладки: не смешивайте два подхода в одном проекте. Либо везде короткие ключи, либо везде строки-ключи. Иначе разработчик не знает, куда лезть за конкретной фразой.
Три подхода к хранению переводов
1. PHP-массивы (короткие ключи)
Классика. Строки сгруппированы по смыслу: auth.php, validation.php, orders.php.
// lang/ru/orders.php
return [
'created' => 'Заказ :number создан',
'statuses' => [
'paid' => 'Оплачен',
'shipped' => 'Отправлен',
],
];
// В коде
echo __('orders.created', ['number' => $order->id]);
echo __('orders.statuses.paid');
Плюс — вложенность и группировка. Минус — ключ orders.created ничего не говорит о содержимом, и при удалении фразы легко забыть про перевод в другом языке.
2. JSON (строка как ключ)
Удобно, когда текста много и он читаемый прямо в шаблоне.
// lang/ru.json
{
"Add to cart": "В корзину",
"Your order :number is confirmed": "Ваш заказ :number подтверждён"
}
// В коде
{{ __('Add to cart') }}
{{ __('Your order :number is confirmed', ['number' => $order->id]) }}
Плюс — в шаблоне сразу видно смысл, а без перевода интерфейс остаётся на английском, а не показывает голые ключи. Минус — длинная фраза как ключ хрупка: поправили точку в исходнике — перевод отвалился.
3. Переводы в базе данных
Нужен, когда переводы редактируют неразработчики через админку или строк тысячи. В Laravel это не встроено — берут пакет вроде spatie/laravel-translation-loader, который подменяет стандартный загрузчик переводов на чтение из БД с кешированием. Цена — дополнительная зависимость и обязательный кеш, иначе каждый __() бьёт в базу.
| Критерий | PHP-массивы | JSON | База данных |
|---|---|---|---|
| Кто редактирует | Разработчик | Разработчик | Контент-менеджер |
| Поведение без перевода | Виден ключ | Виден оригинал | Зависит от настройки |
| Группировка по смыслу | Да, вложенность | Плоский файл | По namespace |
| Нагрузка | Файл в память | Файл в память | Запросы + кеш |
| Когда брать | Служебные строки, валидация | Интерфейс, много текста | Правки без деплоя |
Как переключать язык и не потерять его в очередях
Смена языка на лету — App::setLocale('ru'). Обычно это делают в middleware, читая язык из URL, заголовка Accept-Language или профиля пользователя.
// app/Http/Middleware/SetLocale.php
use Illuminate\Support\Facades\App;
public function handle($request, Closure $next)
{
$locale = $request->user()?->locale
?? $request->getPreferredLanguage(['en', 'ru'])
?? config('app.locale');
App::setLocale($locale);
return $next($request);
}
Здесь и начинается неочевидная боль. App::setLocale() действует только в рамках текущего HTTP-запроса. Когда вы формируете уведомление и отправляете его в очередь, воркер поднимает задачу в отдельном процессе с локалью по умолчанию — и письмо уходит на английском, хотя пользователь выбрал русский.
Правильный способ — привязать локаль к самому уведомлению или задаче.
// В классе Notification
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Authenticatable implements HasLocalePreference
{
public function preferredLocale(): string
{
return $this->locale; // 'ru' или 'en' из профиля
}
}
Laravel сам подхватит preferredLocale() при отправке уведомления, в том числе из очереди. Для произвольного кода, который надо выполнить в конкретной локали, используйте замыкание:
use Illuminate\Support\Facades\App;
App::setLocale('ru');
// либо временно, с автоматическим возвратом прежней локали
app('translator')->setLocale('en');
Точное имя контракта и метода для локали уведомлений сверьте в разделе документации о локализации — с версиями оно не менялось, но пакет уведомлений эволюционирует.
Множественное число: :count и pipe-синтаксис
«1 товар», «2 товара», «5 товаров» — русский язык требует три формы, и наивная замена :count товар(ов) выглядит убого. Laravel решает это через trans_choice() и разделитель |.
// lang/ru/cart.php
return [
'items' => '{0} нет товаров|{1} :count товар|[2,4] :count товара|[5,*] :count товаров',
];
// В коде
echo trans_choice('cart.items', $count, ['count' => $count]);
Диапазоны [2,4] и [5,*] позволяют вручную задать русские правила. Не полагайтесь на автоматический выбор формы по двум вариантам через простой | — он рассчитан на английский, где форм всего две, и для русского даст ошибки на числах 2–4 и 21–24.
Даты, деньги и то, что не лежит в файлах переводов
Строки — только часть локализации. Форматы дат и чисел живут отдельно.
- Даты — Carbon умеет локализованный вывод:
$date->locale('ru')->isoFormat('D MMMM YYYY')вернёт «5 марта 2025». Убедитесь, что в системе или в конфиге приложения выставлена нужная локаль, иначе месяцы останутся английскими. - Числа и валюта — с Laravel 10 есть хелпер
Number::currency()иNumber::format()с параметром локали. На более старых версиях его нет — там форматируют черезNumberFormatterиз расширения intl. - Валидация — сообщения об ошибках берутся из
lang/ru/validation.php. Полный русский перевод этого файла проще всего взять из готового пакета локалей, а не переписывать вручную.
Что ломается на практике
- Кеш переводов из пакетов. Если используете загрузчик из БД или сторонний пакет, после правки перевода нужно чистить кеш приложения — иначе увидите старый текст. Обычный
config:cacheтут не при чём, смотрите документацию конкретного пакета. - Ключ вместо строки. Если
__('orders.created')выводит сам ключ — значит, файлаlang/ru/orders.phpнет, в нём нет такого ключа, или локаль по факту не переключилась. Проверьтеapp()->getLocale()в момент вызова. - Fallback-локаль. В
config/app.phpестьfallback_locale. Если перевода нет в текущем языке, Laravel для коротких ключей уходит в fallback, а не показывает ключ. Настройте её осознанно, обычно на английский. - Переводы в JS. Строки из PHP-файлов недоступны во фронтенде из коробки. Их либо прокидывают через
@jsonв Blade, либо генерируют отдельный пакет для JS-локализации.
Когда что выбирать
Служебные строки, валидация, письма — короткие ключи в PHP-массивах: они и так поставляются в этом формате. Интерфейс с большим количеством витринного текста — JSON, чтобы шаблон читался. База данных — только когда переводы правит не разработчик и деплой ради опечатки неприемлем.
Что делать дальше
Настройте fallback_locale и middleware для определения языка — это база. Дальше пройдитесь по всем местам, где формируются письма и фоновые задачи, и внедрите HasLocalePreference, чтобы язык не терялся в очередях. Отдельная тема, которую здесь не разворачиваем, — синхронизация ключей между языками: для этого есть пакеты-линтеры, которые находят непереведённые и осиротевшие строки. Мерьте не производительность (переводы из файлов дешёвы), а полноту: доля ключей без перевода в каждом языке — вот показатель, за которым стоит следить.
Локализатор видео: Перевод и озвучка видео на другой язык с укладкой в исходный тайминг. Глоссарий терминов, отметки проблемных реплик.
Спросить за 250 ₽Источники: Laravel Localization — официальная документация, spatie/laravel-translation-loader — GitHub
Частые вопросы
У меня Laravel 8, а в статье путь lang в корне проекта — почему?
Почему __() возвращает сам ключ вместо перевода?
Письмо из очереди пришло на английском, хотя пользователь выбрал русский. Что делать?
Как правильно сделать множественное число для русского языка?
Стоит ли хранить переводы в базе данных?
Как отдать переводы во фронтенд на JS?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.