Skip to content

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: o Translator (que implementa TranslatorInterface), 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 (one e other).
  • IntlMessageFormatter: usa o MessageFormatter da extensão intl e entende o formato ICU completo, com plurais, números e datas por idioma. Use este sempre que a extensão intl estiver 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:

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
<?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
<?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âmetro name.
  • {count, plural, ...} escolhe a forma pelo número. =0 casa com zero exato, one e other sã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
<?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
<?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:

php
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:

plaintext
Hello, Ana!
You have 3 new messages.
Brazil has 212,000,000 inhabitants.
Today is October 8, 2026.

Com ?idioma=pt-BR:

plaintext
Olá, Ana!
Você tem 3 mensagens novas.
O Brasil tem 212.000.000 habitantes.
Hoje é 8 de outubro de 2026.

Com ?idioma=es:

plaintext
¡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 PhpMessageSource do Yii2 corresponde ao MessageSource do yiisoft/translator-message-php, com a mesma ideia de um arquivo por idioma e categoria.
  • O formato ICU das mensagens é o mesmo.
  • O Yii::$app->language dá lugar ao parâmetro locale e ao withLocale(), que cria uma cópia em vez de mudar o estado global.

Conteúdo independente, não é a documentação oficial do Yii.