DesenvolvedorWordPress WhatsApp

Internacionalização (i18n) de plugins e temas WordPress

Um plugin ou tema bem feito precisa estar pronto para falar qualquer idioma — e isso não é "tradução", é arquitetura. A internacionalização (i18n) é o trabalho de envolver cada string nas funções gettext do WordPress, com um text domain literal e placeholders que o tradutor possa reordenar. Este guia cobre as funções, o fluxo .pot → .po → .mo e as armadilhas que silenciosamente impedem a tradução.

Precisa preparar seu plugin para múltiplos idiomas?

Atualizado em 19/06/2026

Internacionalizar (i18n) é deixar o seu código pronto para ser traduzido sem ter de editá-lo — e essa é uma das marcas que separam um plugin amador de um profissional. O WordPress resolve o problema com a biblioteca gettext: você envolve cada texto visível ao usuário em uma função (__(), _e(), _n()…), informa um text domain que identifica o seu projeto, e a partir daí um tradutor pode produzir os arquivos de cada idioma sem nunca abrir o seu PHP. Parece simples, mas há nuances — text domain literal, placeholders numerados, comentários para tradutores — que, ignoradas, fazem strings "desaparecerem" do catálogo. Vamos a elas.

Por que internacionalizar (e por que cedo)

Mesmo que o seu público hoje seja só o Brasil, há motivos concretos para internacionalizar desde a primeira versão:

A internacionalização é trabalho do desenvolvedor; a tradução em si (a localização, ou l10n) é feita depois, sobre os arquivos que você gera. Aqui tratamos da primeira metade — a que vive no código.

O text domain: a regra que mais gera bug

Toda string traduzível carrega um text domain: um identificador que diz a qual projeto aquela tradução pertence. É o segundo argumento das funções gettext. Três regras inegociáveis:

Um text domain digitado errado em uma única chamada (um 'minha_loja' com underscore onde o resto usa 'minha-loja') faz aquela string nunca encontrar a tradução — e o bug é silencioso: o texto simplesmente aparece em inglês. Padronize o domain e, na dúvida, deixe o linter de i18n do wp i18n apontar divergências.

Declarando o text domain e carregando as traduções

No cabeçalho do plugin, declare Text Domain (e, por convenção histórica, Domain Path). Em seguida, registre o carregamento dos arquivos .mo a partir de uma pasta /languages:

<?php
/**
 * Plugin Name: Minha Loja
 * Description: Exemplo de plugin internacionalizado.
 * Version:     1.0.0
 * Text Domain: minha-loja
 * Domain Path: /languages
 */

add_action( 'init', 'minha_loja_load_textdomain' );

function minha_loja_load_textdomain() {
    load_plugin_textdomain(
        'minha-loja',                          // text domain (= slug)
        false,                                  // parâmetro legado, sempre false
        dirname( plugin_basename( __FILE__ ) ) . '/languages'
    );
}

Desde o WordPress 4.6, plugins e temas hospedados no .org têm as traduções carregadas automaticamente a partir de translate.wordpress.org (o chamado just-in-time loading), e a chamada load_plugin_textdomain() passa a ser opcional nesse cenário. Ela continua indispensável para produtos distribuídos por conta própria, em que você embute os .mo em /languages. Mantê-la é seguro nos dois casos. Observação de versão: o load_plugin_textdomain() deve rodar no máximo no hook init — chamá-lo cedo demais (em plugins_loaded antes do init, ou no topo do arquivo) pode preceder o momento em que o WordPress já sabe o locale do usuário; a partir do WP 6.7 o próprio núcleo passou a emitir um aviso (_load_textdomain_just_in_time) quando uma tradução é solicitada cedo demais.

As funções gettext: traduzir, imprimir, escapar

Existe uma função para cada combinação de "retornar ou imprimir" e "escapar ou não". Memorize a lógica dos nomes — ela é consistente:

<?php
// Retorna (para usar numa variável, num return, etc.)
$titulo = __( 'Configurações', 'minha-loja' );

// Imprime direto
_e( 'Salvar alterações', 'minha-loja' );

// Traduz E escapa na saída (a forma recomendada ao ecoar em HTML)
echo esc_html__( 'Bem-vindo à sua loja', 'minha-loja' );

// Dentro de um atributo:
printf( '<input type="submit" value="%s">', esc_attr__( 'Enviar', 'minha-loja' ) );

// Plural: _n( singular, plural, número, domain )
$n = count( $itens );
$texto = sprintf(
    _n( '%s item no carrinho', '%s itens no carrinho', $n, 'minha-loja' ),
    number_format_i18n( $n )
);

// Contexto com _x(): a MESMA palavra, sentidos diferentes
$rotulo_botao = _x( 'Post', 'verbo, publicar conteúdo', 'minha-loja' );
$rotulo_tipo  = _x( 'Post', 'substantivo, um artigo', 'minha-loja' );

Por que _x() existe? Porque uma palavra curta em inglês pode ter traduções diferentes conforme o sentido. "Post" pode ser o verbo "publicar" ou o substantivo "artigo"; em português viram palavras distintas. O contexto (segundo argumento) faz cada ocorrência aparecer separadamente no catálogo, permitindo ao tradutor escolher a palavra certa para cada uma.

Referência rápida das funções

FunçãoO que fazRetorna ou imprimeEscapa?
__()TraduzRetornaNão
_e()TraduzImprimeNão
esc_html__()Traduz + escapa p/ HTMLRetornaSim (HTML)
esc_html_e()Traduz + escapa p/ HTMLImprimeSim (HTML)
esc_attr__()Traduz + escapa p/ atributoRetornaSim (atributo)
esc_attr_e()Traduz + escapa p/ atributoImprimeSim (atributo)
_n()Singular / plural por númeroRetornaNão
_x()Traduz com contextoRetornaNão
_ex()Traduz com contextoImprimeNão
_nx()Plural + contextoRetornaNão

Regra de bolso: ao ecoar texto em HTML, prefira sempre a variante esc_html_* ou esc_attr_*. Você ganha tradução e escaping de saída num único passo — duas boas práticas pelo preço de uma.

Variáveis em strings: sprintf e placeholders numerados

Aqui mora um dos erros mais comuns. Você nunca concatena uma variável dentro de __(), porque isso quebra a string em pedaços que o tradutor não consegue traduzir como uma frase. A forma correta é deixar a frase inteira como placeholder e injetar o valor com sprintf():

<?php
$nome  = 'Roger';
$total = 3;

// ERRADO: concatenação quebra a frase e impede a tradução
echo __( 'Olá, ', 'minha-loja' ) . $nome . __( '! Você tem ', 'minha-loja' ) . $total . __( ' pedidos.', 'minha-loja' );

// CERTO: frase inteira traduzível + sprintf com placeholders NUMERADOS
/* translators: 1: nome do cliente, 2: quantidade de pedidos */
echo sprintf(
    esc_html__( 'Olá, %1$s! Você tem %2$d pedidos.', 'minha-loja' ),
    esc_html( $nome ),
    (int) $total
);

Por que numerar (%1$s, %2$d) e não usar %s/%d simples? Porque a ordem das palavras varia entre idiomas. Com placeholders numerados, o tradutor pode escrever, por exemplo, "Você tem %2$d pedidos, %1$s!" — reordenando os argumentos sem tocar no seu PHP. Com placeholders sem número, a ordem fica presa à sequência dos argumentos passados ao sprintf(). Use a forma simples apenas quando houver um único placeholder.

O comentário para tradutores

Sempre que uma string tem placeholders ou é ambígua, deixe um comentário /* translators: ... */ na linha imediatamente acima da chamada gettext. O extrator anexa esse comentário à string no .pot, e é ele que diz ao tradutor o que cada %s significa:

<?php
/* translators: %s = título do produto */
$msg = sprintf( esc_html__( 'O produto "%s" foi adicionado ao carrinho.', 'minha-loja' ), esc_html( $titulo ) );

/* translators: 1: data do pedido, 2: valor total formatado */
$resumo = sprintf(
    esc_html__( 'Pedido feito em %1$s, total de %2$s.', 'minha-loja' ),
    esc_html( $data ),
    esc_html( $valor )
);

O comentário precisa começar exatamente com a palavra translators: e estar colado à linha da função (sem linhas em branco entre eles) para que o make-pot o capture. Um comentário bem-intencionado mas mal posicionado simplesmente não chega ao tradutor.

Gerando o catálogo: o fluxo .pot → .po → .mo

Com o código internacionalizado, o próximo passo é gerar o template de tradução (.pot), que lista todas as strings extraídas. A ferramenta oficial é o WP-CLI:

# Na raiz do plugin, gera languages/minha-loja.pot
wp i18n make-pot . languages/minha-loja.pot --domain=minha-loja

# (opcional) gerar os .json para as traduções de JavaScript
wp i18n make-json languages/ --no-purge

O ciclo completo de arquivos é:

  1. .pot (Portable Object Template): o catálogo-mestre, sem traduções, gerado do seu código. É o que você distribui para os tradutores.
  2. .po (Portable Object): uma cópia do .pot preenchida com as traduções de um idioma (ex.: minha-loja-pt_BR.po). Editável em ferramentas como o Poedit ou no GlotPress.
  3. .mo (Machine Object): a versão binária e compilada do .po, que é o que o WordPress realmente lê em tempo de execução. Gerada a partir do .po (o Poedit faz ao salvar; o WP-CLI faz com wp i18n make-mo).

A convenção de nomes dos arquivos importa: para plugins, o padrão é {textdomain}-{locale}.mo — por exemplo, minha-loja-pt_BR.mo. Errar o locale ou o domain no nome do arquivo faz o WordPress não encontrar a tradução, mesmo com tudo o mais correto.

i18n no JavaScript

Strings em blocos do editor, em scripts do admin ou no front-end também precisam ser traduzíveis. O WordPress expõe o pacote @wordpress/i18n (acessível como wp.i18n), com as mesmas funções do PHP, e você conecta o catálogo ao script com wp_set_script_translations() no momento do enfileiramento:

<?php
// PHP: enfileira o script e conecta as traduções (arquivos .json em /languages)
add_action( 'wp_enqueue_scripts', 'minha_loja_scripts' );

function minha_loja_scripts() {
    wp_enqueue_script(
        'minha-loja-front',
        plugins_url( 'js/front.js', __FILE__ ),
        array( 'wp-i18n' ),       // dependência obrigatória
        '1.0.0',
        true
    );
    wp_set_script_translations( 'minha-loja-front', 'minha-loja', plugin_dir_path( __FILE__ ) . 'languages' );
}

No lado do JavaScript, importe e use __() normalmente. Lembre-se: a tradução de JS usa arquivos .json (gerados com wp i18n make-json), não .mo — mas o text domain é o mesmo do PHP.

Armadilhas comuns

SintomaCausa provávelCorreção
String aparece em inglês mesmo traduzidaText domain divergente do slug (ou digitado errado)Padronize o domain literal em todas as chamadas
String não aparece no .potTexto ou domain passado como variávelUse sempre strings literais nos dois argumentos
Frase impossível de traduzir bemConcatenação de variáveis dentro de __()Frase inteira + sprintf() com %1$s
Tradutor não sabe o que é cada %sFalta o comentário /* translators: */Adicione-o na linha logo acima da função
Aviso "loaded too early" no WP 6.7+load_plugin_textdomain ou string traduzida antes do initCarregue no hook init; não traduza no topo do arquivo
Mesma palavra com duas traduções necessáriasFalta de contextoUse _x() para desambiguar

Boas práticas

Internacionalizar é, no fim, uma questão de disciplina com um punhado de funções — exatamente como acontece com hooks e com a segurança. Feito desde o primeiro commit do seu plugin, custa segundos por string e abre o seu produto para o mundo inteiro.

Quer um plugin pronto para qualquer idioma, do jeito certo?

Internacionalizo plugins e temas seguindo as diretrizes do WordPress.org — text domain, catálogo .pot e fluxo de tradução prontos para a comunidade ou para o seu cliente. Me chame no WhatsApp.

WhatsApp: (43) 99932-9697

ou e-mail: [email protected]

Perguntas frequentes

Qual a diferença entre i18n e l10n?

Internacionalização (i18n) é o trabalho que você, desenvolvedor, faz no código: envolver cada string em uma função gettext (__(), _e() etc.) e declarar um text domain, de modo que o texto possa ser traduzido sem editar o programa. Localização (l10n) é o passo seguinte, feito por tradutores: gerar os arquivos .po/.mo de cada idioma a partir do .pot. Em resumo: i18n prepara, l10n traduz. As abreviações vêm de "internationalization" (18 letras entre o i e o n) e "localization" (10 letras).

Por que o text domain não pode ser uma variável?

Porque o gerador do arquivo .pot (o wp i18n make-pot) lê o código estaticamente, sem executá-lo. Ele precisa ver o text domain escrito como string literal__( 'Salvar', 'meu-plugin' ) — para extrair a string corretamente. Se você passar uma variável ou uma constante (__( 'Salvar', $dominio )), a ferramenta não consegue resolver o valor e a string fica de fora do catálogo, ou gera um aviso. Pela mesma razão, o próprio texto a traduzir também deve ser literal, nunca uma variável.

Ainda preciso de load_plugin_textdomain() se publico no WordPress.org?

Para plugins e temas hospedados no repositório oficial, não é mais obrigatório: desde o WordPress 4.6 o núcleo carrega automaticamente as traduções fornecidas pelo sistema translate.wordpress.org (a chamada "just-in-time" loading). Porém, o load_plugin_textdomain() continua válido e necessário sempre que você distribui o produto por conta própria (venda direta, cliente privado, plugin premium) e embute os arquivos .mo em uma pasta /languages. Mantê-lo não causa problema; removê-lo quebra a tradução fora do .org.

Por que usar %1$s e %2$s em vez de %s?

Porque a ordem das palavras muda entre idiomas. Com placeholders numerados (%1$s, %2$s), o tradutor pode reordenar os argumentos na frase sem tocar no seu código PHP — em algumas línguas o objeto vem antes do sujeito. Com %s simples, a ordem fica travada na sequência em que você passou os argumentos ao sprintf(). Use %s apenas quando houver um único placeholder; com dois ou mais, sempre numere.

O que é o comentário /* translators: */ e por que ele importa?

É um comentário especial, colocado na linha imediatamente acima da função gettext, que o make-pot extrai e anexa à string no arquivo .pot. Ele dá contexto ao tradutor: o que cada %s representa, se a palavra é verbo ou substantivo, qual o tom. Sem ele, o tradutor recebe "%s de %s" sem saber o que preencher, e a tradução sai errada. É exigência das diretrizes do WordPress.org sempre que a string contém placeholders.

Como traduzir strings que estão no JavaScript?

Use o pacote @wordpress/i18n (wp.i18n), que expõe __(), _n() e _x() no lado do JS, e conecte o catálogo ao script enfileirado com wp_set_script_translations( 'handle', 'meu-plugin' ) no PHP. O fluxo de arquivos muda: as traduções de JS usam arquivos .json (gerados a partir do .po com wp i18n make-json), não .mo. O text domain, contudo, é o mesmo do PHP.

Referências oficiais

  1. WordPress.org — Internationalization (Plugin Handbook)
  2. WordPress.org — How to Internationalize Your Plugin
  3. WordPress.org — Localization
  4. WordPress.org — load_plugin_textdomain() (Code Reference)
  5. WordPress.org — WP-CLI: wp i18n
Tem alguma dúvida? Ficarei feliz em ajudar. Clique Aqui