Локализация Laravel: как сделать правильно с первого раза
Разбор рабочих приёмов локализации в Laravel 11: файлы переводов и JSON, определение локали через middleware, склонение чисел и типичные ошибки, из-за которых переводы протекают между запросами.

Вы добавили в приложение второй язык, разложили строки по файлам lang/ru/messages.php и lang/en/messages.php, а через неделю обнаружили, что часть интерфейса на английском, часть на русском, а один пользователь жалуется, что после его запроса чужой получил ответ не на своём языке. Все три проблемы — следствие того, как устроена локаль в Laravel: это глобальное состояние приложения, живущее внутри одного процесса. Разберём, как раскладывать переводы, где менять локаль и почему её нельзя менять там, где хочется.
Материал написан для Laravel 11.x. Начиная с Laravel 9 каталог переводов лежит в lang/ в корне проекта, а не в resources/lang/ — если вы обновлялись со старой версии, проверьте, где именно у вас файлы.
Два хранилища строк: PHP-файлы и JSON
В Laravel есть два способа хранить переводы, и они не взаимозаменяемы. Выбор влияет на то, как вы будете вызывать строки в коде.
Файлы с короткими ключами — lang/ru/messages.php, возвращающий массив. Обращение по ключу с точкой:
// lang/ru/messages.php
return [
'welcome' => 'Добро пожаловать',
'cart' => [
'empty' => 'Корзина пуста',
],
];
// в шаблоне или коде
__('messages.welcome'); // Добро пожаловать
__('messages.cart.empty'); // Корзина пуста
JSON-файлы — lang/ru.json, где ключ это сама строка на языке-исходнике (обычно английском):
// lang/ru.json
{
"Welcome": "Добро пожаловать",
"Your cart is empty": "Корзина пуста"
}
// в коде — передаёте английскую строку
__('Welcome'); // Добро пожаловать
__('Your cart is empty'); // Корзина пуста
Разница не косметическая. С JSON исходный язык читается прямо в коде — не нужно держать в голове, что скрывается за messages.cart.empty. Но при этом ключ и есть строка: изменили английский текст на одну букву — перевод перестал находиться, и Laravel вернёт сам ключ, то есть английскую фразу.
| Критерий | PHP-файлы, короткие ключи | JSON, ключ-строка |
|---|---|---|
| Читаемость в коде | Нужно помнить, что за ключом | Видно исходную фразу сразу |
| Структура | Вложенные массивы, группировка | Плоский список |
| Стабильность при правках | Ключ не меняется, текст правится свободно | Правка исходной фразы ломает связь |
| Fallback при отсутствии | Возвращает ключ (напр. messages.welcome) | Возвращает саму фразу — выглядит терпимо |
Рабочий компромисс: JSON для сплошного пользовательского текста интерфейса, PHP-файлы — для служебных групп вроде validation.php и auth.php, которые Laravel и так поставляет в этом формате.
Где менять локаль: только middleware, не контроллер
Самая частая ошибка новичков — вызвать App::setLocale($locale) в контроллере, когда уже понадобился перевод. К этому моменту часть строк могла отрендериться на локали по умолчанию. Локаль нужно установить до того, как обработчик начнёт формировать ответ. Место для этого — middleware.
<?php
// app/Http/Middleware/SetLocale.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
class SetLocale
{
// Разрешённые локали держим списком, а не берём что попало из запроса
private array $supported = ['ru', 'en'];
public function handle(Request $request, Closure $next)
{
// приоритет: сохранённый выбор пользователя, затем сессия, затем заголовок браузера
$locale = $request->user()?->locale
?? $request->session()->get('locale')
?? $request->getPreferredLanguage($this->supported);
if (in_array($locale, $this->supported, true)) {
App::setLocale($locale);
}
return $next($request);
}
}
В Laravel 11 middleware регистрируется в bootstrap/app.php через ->withMiddleware(), а не в старом app/Http/Kernel.php. Точный синтаксис регистрации групп и алиасов смотрите в разделе документации Middleware для вашей минорной версии — API конфигурации в 11 отличается от 10.
Проверка списком через in_array здесь не паранойя: getPreferredLanguage берёт данные из заголовка Accept-Language, а его формирует клиент. Без белого списка вы рискуете передать в setLocale произвольную строку и получить имя файла, которого нет.
Локаль в Laravel — это глобальное состояние процесса, а не свойство запроса. В обычном PHP-FPM каждый запрос стартует с чистого листа, и это спасает. В долгоживущих воркерах Octane состояние переживает запрос — и вот там протекание локали между пользователями превращается из теории в баг на проде.
Октейн и другие долгоживущие процессы
Если приложение работает под Laravel Octane (Swoole или RoadRunner) или строки переводятся внутри очередей, помните: App::setLocale() меняет состояние воркера, а воркер обслуживает много запросов подряд. Установили локаль в одном запросе — она осталась для следующего, пока кто-то её не переопределит.
Поэтому middleware, который выставляет локаль на каждый запрос, — не избыточность, а необходимость. Не полагайтесь на то, что PHP «сам сбросит»: под Octane не сбросит. В job'ах очереди, где нет HTTP-запроса и вашего middleware, локаль устанавливайте явно в начале handle() задачи, беря язык из данных самой задачи.
Множественное число: pluralization
«1 товар», «2 товара», «5 товаров» — в русском три формы, а не две, как в английском. Простой str_replace с числом здесь ломается. Laravel решает это через trans_choice и разделитель |:
// lang/ru/messages.php
return [
'items' => '{0} нет товаров|[1,4] :count товара|[5,*] :count товаров',
];
// в коде
trans_choice('messages.items', 0); // нет товаров
trans_choice('messages.items', 3); // 3 товара
trans_choice('messages.items', 12); // 12 товаров
Синтаксис с диапазонами в квадратных скобках даёт точный контроль над формами. Но осторожно: правило «2, 3, 4 → товара» верно не для всех чисел. По-русски правильно «22 товара», но «12 товаров» — форма зависит от последней цифры, кроме чисел от 11 до 14. Диапазоны [1,4] и [5,*] из примера выше это не покрывают и на 12 сработают неверно.
Для корректного русского склонения по всем числам полагайтесь на встроенный MessageSelector Laravel с простыми формами без ручных диапазонов — он знает правила языка из локали. Проверьте поведение на числах 1, 2, 11, 21, 111 прежде, чем считать задачу закрытой.
Что ломается чаще всего
- Строка вернулась как ключ. В интерфейсе видно
messages.welcome— значит, файла, группы или ключа нет для текущей локали, а fallback тоже пуст. Проверьтеapp.fallback_localeвconfig/app.php. - Перевод не подхватился после правки файла. Если включено кеширование конфигурации или используете пакет-кеш переводов — очистите кеш. Сами файлы
lang/Laravel не кеширует по умолчанию, ноconfig:cacheзамораживаетfallback_locale. - JSON не находит фразу. Лишний пробел, другой регистр или изменённая пунктуация в исходной строке — и связь потеряна. Ключ в JSON должен совпадать с аргументом
__()символ в символ. - Локаль «залипла» под Octane. Один пользователь получил язык другого. Причина —
setLocaleне в middleware, а где-то по требованию, и воркер сохранил её между запросами. - Даты и числа не локализуются.
__()переводит только строки. Формат даты и разделитель разрядов — отдельная задача: для дат используйтеCarbonс->locale(), для чисел —NumberFormatterиз ext-intl.
Что настроить дальше
Локаль расставлена, склонения работают — следующие шаги, которые обычно всплывают уже на проде:
- Настройте
fallback_localeосознанно: на какой язык падать, если перевода нет. Пустая строка вместо ключа хуже, чем английский текст. - Проверьте локализацию через
Carbon:Carbon::now()->locale(app()->getLocale())->translatedFormat('d MMMM Y')— но убедитесь, что нужные локали Carbon установлены. - Для форматирования чисел и валют подключите расширение
ext-intlиNumberFormatter—__()эту задачу не решает.
Выбор языка через URL-сегмент (/ru/products) и работа с RTL-языками — отдельные темы, здесь мы разобрали только определение и хранение переводов.
Универсальный ассистент: Обычный чат: тексты, идеи, разбор, перевод, расчёты.
Спросить за 8 ₽Источники: Laravel Localization — официальная документация, Laravel Octane — concurrent tasks и состояние
Частые вопросы
У меня Laravel 10, а не 11 — примеры с middleware подойдут?
Что делать, если перевод не находится и в интерфейсе видно сам ключ?
Почему под Laravel Octane локаль иногда достаётся не тому пользователю?
Русское склонение не работает на числах вроде 12 и 22 — что не так?
Как локализовать даты и суммы — __() их не переводит?
Нужно ли очищать кеш после правки файлов переводов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.