Symfony Translation component -- мощная система интернационализации (i18n). На экзамене проверяют: форматы файлов переводов, ICU MessageFormat, pluralization, домены, определение локали и интеграцию с Twig.
Установка и конфигурация
composer require symfony/translation
# config/packages/translation.yaml
framework:
default_locale: 'ru'
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- 'en'
providers:
# Optional: external translation providers
# crowdin:
# dsn: '%env(CROWDIN_DSN)%'
Подвох экзамена:
fallbacks-- массив локалей. Если перевод не найден для текущей локали, Symfony ищет по цепочке fallbacks. Если не найден нигде -- возвращает оригинальный ключ.
TranslatorInterface
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
final class OrderController extends AbstractController
{
public function __construct(
private readonly TranslatorInterface $translator,
) {}
#[Route('/orders/{id}', name: 'order_show')]
public function show(int $id): Response
{
// Basic translation
$message = $this->translator->trans('order.created');
// Translation with parameters
$message = $this->translator->trans('order.total', [
'{total}' => '1 500',
'{currency}' => 'RUB',
]);
// Explicit locale override
$message = $this->translator->trans(
'order.created',
[], // parameters
'messages', // domain
'en', // locale override
);
return new Response($message);
}
}
Сигнатура метода trans()
<?php
declare(strict_types=1);
// TranslatorInterface::trans() signature
public function trans(
string $id, // Translation key
array $parameters = [], // Replacement parameters
?string $domain = null, // Translation domain (default: 'messages')
?string $locale = null, // Locale override (default: current locale)
): string;
Подвох экзамена: Порядок аргументов
trans(): id, parameters, domain, locale. Если нужно указать locale, но не domain -- передайтеnullили'messages'в качестве domain.
Форматы файлов переводов
Symfony поддерживает несколько форматов. Имена файлов следуют конвенции: domain.locale.format.
translations/
messages.ru.yaml # Домен: messages, Локаль: ru, Формат: YAML
messages.en.yaml # Домен: messages, Локаль: en
messages.ru.xlf # XLIFF формат
validators.ru.yaml # Домен: validators
security.ru.yaml # Домен: security
admin.ru.yaml # Кастомный домен: admin
YAML формат
# translations/messages.ru.yaml
order:
created: 'Заказ успешно создан'
total: 'Итого: {total} {currency}'
status:
pending: 'В ожидании'
confirmed: 'Подтверждён'
shipped: 'Отправлен'
delivered: 'Доставлен'
cancelled: 'Отменён'
user:
greeting: 'Здравствуйте, {name}!'
role:
admin: 'Администратор'
manager: 'Менеджер'
user: 'Пользователь'
# Nested keys are flattened to dot notation
# 'order.created' => 'Заказ успешно создан'
# 'order.status.pending' => 'В ожидании'
# translations/messages.en.yaml
order:
created: 'Order successfully created'
total: 'Total: {total} {currency}'
status:
pending: 'Pending'
confirmed: 'Confirmed'
shipped: 'Shipped'
delivered: 'Delivered'
cancelled: 'Cancelled'
user:
greeting: 'Hello, {name}!'
XLIFF формат (рекомендуемый для production)
<!-- translations/messages.ru.xlf -->
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="ru" datatype="plaintext"
original="messages">
<body>
<trans-unit id="order.created">
<source>order.created</source>
<target>Заказ успешно создан</target>
</trans-unit>
<trans-unit id="order.total">
<source>order.total</source>
<target>Итого: {total} {currency}</target>
</trans-unit>
<trans-unit id="user.greeting">
<source>user.greeting</source>
<target>Здравствуйте, {name}!</target>
<note>Greeting message for the user</note>
</trans-unit>
</body>
</file>
</xliff>
Подвох экзамена: XLIFF -- рекомендуемый формат для профессиональных переводов. Он поддерживается профессиональными инструментами перевода (CAT tools). YAML проще для разработки. Оба формата поддерживают одинаковые возможности в Symfony.
XLIFF 2.0
<!-- translations/messages+intl-icu.ru.xlf -->
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0"
version="2.0" srcLang="en" trgLang="ru">
<file id="messages">
<unit id="order.created">
<segment>
<source>order.created</source>
<target>Заказ успешно создан</target>
</segment>
</unit>
</file>
</xliff>
ICU MessageFormat
Для включения ICU MessageFormat добавьте +intl-icu к имени файла.
# translations/messages+intl-icu.ru.yaml
# ICU MessageFormat enabled for this file
# Simple parameter substitution (uses {param} not %param%)
greeting: 'Здравствуйте, {name}!'
# Pluralization with ICU plural rules
items_count: >-
{count, plural,
one {# товар в корзине}
few {# товара в корзине}
many {# товаров в корзине}
other {# товаров в корзине}
}
# Select based on gender
user_action: >-
{gender, select,
female {{name} добавила товар в корзину}
male {{name} добавил товар в корзину}
other {{name} добавил(а) товар в корзину}
}
# Number formatting
price: 'Цена: {price, number, currency}'
# Date formatting
created_at: 'Создан: {date, date, medium}'
# Nested: plural + select
notification: >-
{gender, select,
female {{count, plural,
one {{name} оставила # отзыв}
few {{name} оставила # отзыва}
many {{name} оставила # отзывов}
other {{name} оставила # отзывов}
}}
other {{count, plural,
one {{name} оставил # отзыв}
few {{name} оставил # отзыва}
many {{name} оставил # отзывов}
other {{name} оставил # отзывов}
}}
}
<?php
declare(strict_types=1);
// Using ICU MessageFormat translations
$translator->trans('items_count', ['count' => 1]);
// "1 товар в корзине"
$translator->trans('items_count', ['count' => 3]);
// "3 товара в корзине"
$translator->trans('items_count', ['count' => 15]);
// "15 товаров в корзине"
$translator->trans('user_action', [
'gender' => 'female',
'name' => 'Мария',
]);
// "Мария добавила товар в корзину"
Подвох экзамена: ICU MessageFormat активируется через
+intl-icuв имени файла (например,messages+intl-icu.ru.yaml). Без этого суффикса Symfony использует стандартный формат с%param%. В ICU используются{param}и#для текущего числа.
Разница стандартного и ICU формата
| Аспект | Стандартный | ICU MessageFormat |
|---|---|---|
| Файл | messages.ru.yaml |
messages+intl-icu.ru.yaml |
| Параметры | %name% или {name} |
{name} |
| Pluralization | {0} No items|{1} One item|]1,Inf[ %count% items |
{count, plural, one {# item} other {# items}} |
| Gender | Не поддерживается | {gender, select, ...} |
| Number format | Ручное | {price, number, currency} |
Pluralization
Стандартный формат (без ICU)
# translations/messages.ru.yaml (standard format)
items_in_cart: '{0} Корзина пуста|{1} В корзине один товар|]1,Inf[ В корзине %count% товаров'
<?php
declare(strict_types=1);
// Standard pluralization
$translator->trans('items_in_cart', ['%count%' => 0]);
// "Корзина пуста"
$translator->trans('items_in_cart', ['%count%' => 1]);
// "В корзине один товар"
$translator->trans('items_in_cart', ['%count%' => 42]);
// "В корзине 42 товаров"
ICU plural rules для русского языка
one: 1, 21, 31, 41, ... (кроме 11)
few: 2-4, 22-24, 32-34, ... (кроме 12-14)
many: 0, 5-20, 25-30, ...
other: 1.5, 2.3, ... (дробные числа)
# translations/messages+intl-icu.ru.yaml
files_uploaded: >-
{count, plural,
one {Загружен {count} файл}
few {Загружено {count} файла}
many {Загружено {count} файлов}
other {Загружено {count} файлов}
}
Подвох экзамена: В русском языке ЧЕТЫРЕ формы множественного числа в ICU:
one,few,many,other. В английском только две:oneиother. Если забытьfewилиmanyдля русского -- перевод может работать некорректно.
Домены переводов (Translation Domains)
Домены разделяют переводы по контексту. По умолчанию используется домен messages.
# translations/messages.ru.yaml — default domain
app:
title: 'Мой магазин'
# translations/validators.ru.yaml — validation messages
user.email.unique: 'Этот email уже занят'
product.name.blank: 'Название товара обязательно'
# translations/security.ru.yaml — security messages
login.bad_credentials: 'Неверный логин или пароль'
login.account_disabled: 'Аккаунт деактивирован'
# translations/admin.ru.yaml — custom domain for admin panel
dashboard.title: 'Панель управления'
users.list: 'Список пользователей'
<?php
declare(strict_types=1);
// Using domains
$translator->trans('app.title'); // domain: messages (default)
$translator->trans('app.title', [], 'messages'); // explicit default domain
$translator->trans('user.email.unique', [], 'validators');
$translator->trans('login.bad_credentials', [], 'security');
$translator->trans('dashboard.title', [], 'admin');
Подвох экзамена: Домен
validatorsиспользуется автоматически компонентом Validator для сообщений об ошибках валидации. Доменsecurity-- для сообщений безопасности. Кастомные домены полезны для изоляции переводов (например,admin,api).
Переводы в Twig
Фильтр trans
{# Basic translation #}
{{ 'order.created'|trans }}
{# With parameters #}
{{ 'order.total'|trans({'{total}': order.total, '{currency}': 'RUB'}) }}
{# With explicit domain #}
{{ 'dashboard.title'|trans({}, 'admin') }}
{# With explicit locale #}
{{ 'order.created'|trans({}, 'messages', 'en') }}
{# ICU MessageFormat parameters #}
{{ 'items_count'|trans({count: cart.itemsCount}) }}
{{ 'user_action'|trans({gender: user.gender, name: user.name}) }}
Тег trans
{# Block translation (for long texts) #}
{% trans %}order.created{% endtrans %}
{# With domain #}
{% trans from 'admin' %}dashboard.title{% endtrans %}
{# With locale #}
{% trans into 'en' %}order.created{% endtrans %}
{# With parameters #}
{% trans with {'{name}': user.name} %}user.greeting{% endtrans %}
Перевод в шаблонах форм
{# Form labels are auto-translated from 'messages' domain #}
{{ form_label(form.name) }}
{# Override label translation domain #}
{{ form_label(form.name, null, {
'translation_domain': 'admin'
}) }}
{# Disable translation for a field #}
{{ form_row(form.internalCode, {
'translation_domain': false
}) }}
Pluralization в Twig (ICU)
{# ICU pluralization #}
{{ 'items_count'|trans({count: cart.count}) }}
{# Standard pluralization (deprecated approach) #}
{{ 'items_in_cart'|trans({'%count%': cart.count}) }}
Определение локали (Locale Detection)
Через маршруты
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\Routing\Attribute\Route;
// Locale in URL prefix
#[Route('/{_locale}/products', name: 'product_list', requirements: ['_locale' => 'ru|en|de'])]
public function list(): Response
{
// _locale is automatically set as request locale
return $this->render('product/list.html.twig');
}
// Global locale prefix for all routes in controller
#[Route('/{_locale}', requirements: ['_locale' => 'ru|en|de'])]
final class ProductController extends AbstractController
{
#[Route('/products', name: 'product_list')]
public function list(): Response
{
return $this->render('product/list.html.twig');
}
}
Через Event Listener
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
#[AsEventListener(event: RequestEvent::class, priority: 20)]
final class LocaleListener
{
public function __invoke(RequestEvent $event): void
{
$request = $event->getRequest();
// Priority: URL param > session > Accept-Language header > default
$locale = $request->query->get('lang')
?? $request->getSession()->get('_locale')
?? $request->getPreferredLanguage(['ru', 'en', 'de'])
?? 'ru';
$request->setLocale($locale);
}
}
Подвох экзамена: Специальный параметр
_localeв маршруте автоматически устанавливает локаль на объекте Request. Это встроенное поведение Symfony. Для других способов определения локали нужен EventListener, выполняющийся ДОLocaleListenerSymfony (priority > 16).
Извлечение переводов (Translation Extraction)
# Extract translation keys from templates and PHP code
php bin/console translation:extract ru --force --format=yaml
# Extract to XLIFF format
php bin/console translation:extract ru --force --format=xlf
# Extract for specific domain
php bin/console translation:extract ru --force --domain=admin
# Preview without writing (dry-run)
php bin/console translation:extract ru
# Show statistics
php bin/console debug:translation ru
php bin/console debug:translation ru --domain=messages
<?php
declare(strict_types=1);
// For the extractor to find translations in PHP code,
// use the TranslatableMessage or t() shortcut
use Symfony\Component\Translation\TranslatableMessage;
// Option 1: TranslatableMessage object
$message = new TranslatableMessage('order.created', [], 'messages');
// Option 2: t() shortcut (in controllers extending AbstractController)
$message = $this->translator->trans('order.created');
// Option 3: TranslatableMessage with parameters
$message = new TranslatableMessage('user.greeting', [
'{name}' => $user->getName(),
]);
TranslatableMessage
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\Translation\TranslatableMessage;
final class OrderService
{
// TranslatableMessage defers translation until rendering
public function getStatusLabel(string $status): TranslatableMessage
{
return new TranslatableMessage(
'order.status.' . $status,
[],
'messages',
);
}
}
{# TranslatableMessage is auto-translated in Twig #}
{{ order_status_label }}
{# Explicitly translate #}
{{ order_status_label|trans }}
Переводы в Validator
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
final class Product
{
// Validator automatically uses 'validators' domain
#[Assert\NotBlank(message: 'product.name.required')]
#[Assert\Length(
min: 3,
max: 255,
minMessage: 'product.name.too_short',
maxMessage: 'product.name.too_long',
)]
private string $name = '';
#[Assert\Positive(message: 'product.price.positive')]
private int $price = 0;
}
# translations/validators.ru.yaml
product:
name:
required: 'Название товара обязательно'
too_short: 'Название должно быть не менее {{ limit }} символов'
too_long: 'Название не должно превышать {{ limit }} символов'
price:
positive: 'Цена должна быть положительной'
Подвох экзамена: Параметры валидатора в переводах используют
{{ limit }}(двойные фигурные скобки), а НЕ{limit}или%limit%. Это специфика Validator component. Не путайте с ICU{param}или стандартным%param%.
Переводы в формах
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'product.form.name', // Auto-translated
'help' => 'product.form.name_help',
])
->add('status', ChoiceType::class, [
'choices' => [
'order.status.pending' => 'pending', // Keys are translated
'order.status.confirmed' => 'confirmed',
'order.status.shipped' => 'shipped',
],
'choice_translation_domain' => 'messages',
]);
}
public function configureOptions(OptionsResolver $options): void
{
$options->setDefaults([
'translation_domain' => 'messages',
]);
}
}
Кэширование переводов
# config/packages/translation.yaml
framework:
translator:
cache_dir: '%kernel.cache_dir%/translations'
В production переводы кэшируются автоматически. При разработке (APP_ENV=dev) кэш обновляется при каждом изменении файлов.
# Clear translation cache
php bin/console cache:clear
# Warm up cache (including translations)
php bin/console cache:warmup
Итоги
TranslatorInterface::trans()-- основной метод перевода: id, parameters, domain, locale- Форматы файлов: YAML (простой), XLIFF (профессиональный), PHP
- ICU MessageFormat:
messages+intl-icu.locale.yaml-- plural, select, number, date - Русский язык: 4 формы plural (one, few, many, other)
- Домены:
messages(по умолчанию),validators,security, кастомные _localeв маршруте автоматически устанавливает локальTranslatableMessage-- отложенный перевод до момента рендеринга- Параметры: стандартный
%param%, ICU{param}, Validator{{ param }}