Перейти к содержанию

Локализация

Namespace Keel\I18n. Translator над PHP-файлами сообщений с плейсхолдерами и формами множественного числа, LocaleMiddleware, выбирающий локаль запроса, Locale, внедряемая в хэндлеры, и фильтр trans для Twig и Latte.

// config/i18n.php
return static fn(string $projectDir): I18nConfig => new I18nConfig(
    path: $projectDir . '/translations',      // <path>/<locale>/<domain>.php
    locales: ['en', 'ru', 'pt-BR'],
    defaultLocale: 'en',
    fromPath: true,                           // /ru/about
    sessionKey: 'locale',                     // после SessionMiddleware
    acceptLanguage: true,
);

// config/services.php
return new ServicesConfig([I18nProvider::class]);

// config/http.php
return new HttpConfig(middleware: [SessionMiddleware::class, LocaleMiddleware::class]);
// translations/en/messages.php
return [
    'hello' => 'Hello, {name}!',
    'cart' => ['items' => '{count} item|{count} items'],
];

// translations/ru/messages.php
return [
    'hello' => 'Привет, {name}!',
    'cart' => ['items' => '{count} товар|{count} товара|{count} товаров'],
];
#[Get('/cart')]
public function cart(Translator $t, Locale $locale): array
{
    return ['title' => $t->trans('cart.items', ['count' => 3]), 'locale' => $locale->value];   // "3 товара", "ru"
}

Сообщения

Файл сообщений возвращает вложенный массив; ключи — пути через точку (cart.items), плоский ключ с точками побеждает путь. {placeholders} подставляются из параметров. Сообщение с | перечисляет формы числа в порядке CLDR (one|other для английского, one|few|many для русского, украинского, польского; one|few|other для чешского и так далее), а параметр count выбирает форму по правилу языка. С загруженным ext-intl ICU-шаблон ({count, plural, one {# item} other {# items}}) форматирует MessageFormatter. Отсутствующее сообщение возвращается как свой ключ, так что до появления перевода ничего не ломается; has() подскажет. Аргумент domain называет другой файл (errors.php); locale перекрывает текущую.

Сообщение, которого нет в локали запроса, ищется в fallbackLocale, иначе в локали по умолчанию. Файлы загружаются один раз на воркер и остаются в памяти; текущая локаль — на запрос: переводчик Resettable, а LocaleMiddleware выставляет её (LocaleAware). Поэтому перевод внутри сервиса — это локаль запроса без лишней обвязки.

Выбор локали

LocaleMiddleware перебирает включённые в конфиге источники в таком порядке и берёт первый, который называет поддерживаемую локаль: ведущий сегмент пути (fromPath, /ru/about), query-параметр (queryParam), ключ сессии (sessionKey, поэтому SessionMiddleware идёт раньше), cookie (cookie), заголовок Accept-Language (acceptLanguage, с учётом q и сопоставлением en-GB → en или pt → pt-BR, когда поддерживаются только они). Если ничего не подошло — defaultLocale. Локаль становится атрибутом запроса Locale (value, language(), region()) и заголовком ответа Content-Language; согласованная по заголовку локаль добавляет Vary: Accept-Language. Теги приводятся к каноническому виду (en_us → en-US).

Сообщения валидации (must not be blank, ...) не переводятся: errors API предназначены разработчикам, а HTML-формы рисуют свои сообщения по путям полей. Правила с аргументом message принимают переведённый текст.

Шаблоны

Если зарегистрирован Translator, TwigProvider и LatteProvider добавляют trans как фильтр и функцию, а также locale():

{{ 'hello'|trans({name: user.name}) }} {{ trans('cart.items', {count: cart.count}) }} [{{ locale() }}]
{='hello'|trans: ['name' => $user->name]} {trans('cart.items', ['count' => $cart->count])} [{locale()}]