Internacionalização
Quando a aplicação precisa falar mais de um idioma, os textos não podem ficar fixos no código. Eles passam por um tradutor, que procura a versão certa para o idioma atual e formata números, datas e plurais do jeito de cada lugar. No Yii3, quem faz isso é o pacote yiisoft/translator.
As peças
O template já traz dois pacotes:
yiisoft/translator: oTranslator(que implementaTranslatorInterface), as categorias de mensagens (CategorySource) e os formatadores.yiisoft/translator-message-php: um leitor que guarda as traduções em arquivos PHP que devolvem arrays.
As mensagens são agrupadas em categorias. Cada categoria é um CategorySource com três coisas: um nome, um leitor de mensagens e um formatador. Pacotes do Yii trazem as próprias categorias. O yiisoft/validator, por exemplo, registra a categoria yii-validator com traduções das mensagens de erro para alemão, polonês, português do Brasil, russo e uzbeque.
Há dois formatadores:
SimpleMessageFormatter: troca{nome}pelo valor e entende um plural simples (oneeother).IntlMessageFormatter: usa oMessageFormatterda extensãointle entende o formato ICU completo, com plurais, números e datas por idioma. Use este sempre que a extensãointlestiver instalada.
A configuração padrão do pacote fica nos parâmetros yiisoft/translator: idioma en-US, sem idioma de reserva (fallbackLocale) e categoria padrão app.
Registrando uma categoria
O Translator do container recebe todas as definições marcadas com a tag translation.categorySource. Para criar a sua categoria, basta definir um serviço com essa tag. Trecho de config/common/di/topicos.php:
use Yiisoft\Aliases\Aliases;
use Yiisoft\Translator\CategorySource;
use Yiisoft\Translator\IntlMessageFormatter;
use Yiisoft\Translator\Message\Php\MessageSource;
return [
'topicos.translation.categorySource' => [
'definition' => static function (Aliases $aliases): CategorySource {
$messageSource = new MessageSource($aliases->get('@root/messages'));
return new CategorySource('topicos', $messageSource, new IntlMessageFormatter());
},
'tags' => ['translation.categorySource'],
],
];A chave 'topicos.translation.categorySource' é só um nome único para o serviço. O que importa é a tag. O MessageSource procura os arquivos em messages/<idioma>/<categoria>.php.
O guia usa a categoria topicos para não interferir nos outros exemplos. Numa aplicação sua, o natural é usar a categoria app, que já é a padrão.
Os arquivos de mensagens
A chave de cada mensagem é o texto no idioma de origem, e o valor é a tradução. Aqui a origem é o inglês. Crie messages/pt-BR/topicos.php:
<?php
declare(strict_types=1);
return [
'Hello, {name}!' => 'Olá, {name}!',
'You have {count, plural, =0{no new messages} one{# new message} other{# new messages}}.'
=> 'Você tem {count, plural, =0{nenhuma mensagem nova} one{# mensagem nova} other{# mensagens novas}}.',
'Brazil has {population, number} inhabitants.' => 'O Brasil tem {population, number} habitantes.',
'Today is {today, date, long}.' => 'Hoje é {today, date, long}.',
];E messages/es/topicos.php:
<?php
declare(strict_types=1);
return [
'Hello, {name}!' => '¡Hola, {name}!',
'You have {count, plural, =0{no new messages} one{# new message} other{# new messages}}.'
=> 'Tienes {count, plural, =0{ningún mensaje nuevo} one{# mensaje nuevo} other{# mensajes nuevos}}.',
'Brazil has {population, number} inhabitants.' => 'Brasil tiene {population, number} habitantes.',
'Today is {today, date, long}.' => 'Hoy es {today, date, long}.',
];Não há arquivo para o inglês. Quando o tradutor não encontra a mensagem, ele usa a própria chave, já formatada com os parâmetros.
Os trechos entre chaves seguem o formato ICU:
{name}é trocado pelo parâmetroname.{count, plural, ...}escolhe a forma pelo número.=0casa com zero exato,oneeothersão as categorias de plural do idioma, e#é o número.{population, number}formata o número com o separador do idioma.{today, date, long}formata uma data por extenso.
Usando o tradutor
A action pede a TranslatorInterface ao container, escolhe a categoria e o idioma e passa o tradutor para a view. Crie src/Web/Topicos/I18n/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Topicos\I18n;
use DateTimeImmutable;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Input\Http\Attribute\Parameter\Query;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\Yii\View\Renderer\WebViewRenderer;
use function in_array;
final readonly class Action
{
private const IDIOMAS = ['en-US', 'pt-BR', 'es'];
public function __construct(
private WebViewRenderer $viewRenderer,
private TranslatorInterface $translator,
) {}
public function __invoke(#[Query('idioma')] ?string $idioma = null): ResponseInterface
{
$translator = $this->translator->withDefaultCategory('topicos');
if ($idioma !== null && in_array($idioma, self::IDIOMAS, true)) {
$translator = $translator->withLocale($idioma);
}
return $this->viewRenderer->render(__DIR__ . '/template', [
't' => $translator,
'idiomas' => self::IDIOMAS,
'hoje' => new DateTimeImmutable(),
]);
}
}O withDefaultCategory() e o withLocale() devolvem cópias do tradutor. O serviço compartilhado do container não muda, o que importa quando a aplicação roda num servidor de longa duração. O idioma vem da query string e só é aceito se estiver na lista.
A view, em src/Web/Topicos/I18n/template.php:
<?php
declare(strict_types=1);
use Yiisoft\Html\Html;
/**
* @var Yiisoft\View\WebView $this
* @var Yiisoft\Router\UrlGeneratorInterface $urlGenerator
* @var Yiisoft\Translator\TranslatorInterface $t
* @var string[] $idiomas
* @var DateTimeImmutable $hoje
*/
$this->setTitle('i18n');
?>
<p lang="<?= Html::encodeAttribute($t->getLocale()) ?>">
<?= Html::encode($t->translate('Hello, {name}!', ['name' => 'Ana'])) ?><br>
<?= Html::encode($t->translate('You have {count, plural, =0{no new messages} one{# new message} other{# new messages}}.', ['count' => 3])) ?><br>
<?= Html::encode($t->translate('Brazil has {population, number} inhabitants.', ['population' => 212_000_000])) ?><br>
<?= Html::encode($t->translate('Today is {today, date, long}.', ['today' => $hoje])) ?>
</p>
<nav>
<?php foreach ($idiomas as $idioma): ?>
<?= Html::a($idioma, $urlGenerator->generate('topicos/i18n', queryParameters: ['idioma' => $idioma])) ?>
<?php endforeach ?>
</nav>O translate() recebe o ID da mensagem, os parâmetros e, opcionalmente, a categoria e o idioma: $t->translate('Hello, {name}!', ['name' => 'Ana'], 'topicos', 'es'). A tradução é texto como qualquer outro, então continua passando pelo Html::encode().
E a rota, no arquivo de rotas:
Group::create('/topicos')
->namePrefix('topicos/')
->routes(
Route::get('/i18n')
->action(Topicos\I18n\Action::class)
->name('i18n'),
),Testando
Abra http://127.0.0.1:8080/topicos/i18n. Sem o parâmetro, vale o idioma padrão, en-US:
Hello, Ana!
You have 3 new messages.
Brazil has 212,000,000 inhabitants.
Today is October 8, 2026.Com ?idioma=pt-BR:
Olá, Ana!
Você tem 3 mensagens novas.
O Brasil tem 212.000.000 habitantes.
Hoje é 8 de outubro de 2026.Com ?idioma=es:
¡Hola, Ana!
Tienes 3 mensajes nuevos.
Brasil tiene 212.000.000 habitantes.
Hoy es 8 de octubre de 2026.A data é a do dia em que você abrir a página. Com ?idioma=fr, que não está na lista, a página volta ao inglês.
Variando o count em português, o plural muda: 0 vira "Você tem nenhuma mensagem nova.", 1 vira "Você tem 1 mensagem nova." e 2 vira "Você tem 2 mensagens novas.".
Idioma de reserva
Se a mensagem não existe no idioma pedido, o Translator tenta o idioma mais geral: de pt-BR ele passa para pt. Se ainda assim não achar, tenta o fallbackLocale configurado. Por fim, usa o próprio ID.
Por isso, pedir pt-PT neste exemplo devolve o texto em inglês: não existe messages/pt-PT/ nem messages/pt/. Se você quiser que Portugal veja o texto brasileiro, coloque as traduções em messages/pt/ em vez de messages/pt-BR/.
O idioma padrão da aplicação
O idioma usado quando ninguém chama withLocale() vem do parâmetro locale de yiisoft/translator. Mudando esse parâmetro para pt-BR, todas as categorias passam a responder em português, inclusive a yii-validator. Com isso, as mensagens padrão das regras de validação, como a do Required, aparecem traduzidas sem que você escreva message: em cada regra.
Para escolher o idioma por usuário (pela URL, por cookie ou pelo cabeçalho Accept-Language), o pacote yiisoft/yii-middleware tem o middleware Locale. Ele é pensado para ficar na pilha global, antes do roteador, e muda a forma das URLs (o código do idioma vira prefixo, como em /pt/...), então não é usado no app de exemplo.
Comparando com o Yii2
- O
Yii::t('app', 'Hello, {name}!', ['name' => 'Ana'])vira$translator->translate('Hello, {name}!', ['name' => 'Ana']), com a categoria como terceiro parâmetro. Não há função global: o tradutor chega por injeção de dependência. - O
PhpMessageSourcedo Yii2 corresponde aoMessageSourcedoyiisoft/translator-message-php, com a mesma ideia de um arquivo por idioma e categoria. - O formato ICU das mensagens é o mesmo.
- O
Yii::$app->languagedá lugar ao parâmetrolocalee aowithLocale(), que cria uma cópia em vez de mudar o estado global.