A intenção deste texto é servir como introdução à tradução no WordPress, facilitando o entendimento dos conceitos básicos envolvidos no processo. Nada substitui a documentação oficial de internacionalização de temas e plugins. No momento a versão do WordPress é a 4.9.8, então se você está lendo isso no futuro e alguma coisa ficou desatualizada, você pode me avisar.

Além de tradução, você vai encontrar dois termos importantes neste texto: internacionalização e localização. Internacionalização é o processo de produzir um material, no nosso caso temas, plugins e o próprio WordPress, de forma que ele possa ser traduzido para vários grupos diferentes de pessoas. Localização é o processo de transformar um material internacionalizado em algo de fácil entendimento para pessoas de uma determinada cultura.

Os dois conceitos são complementares: internacionalização é um movimento “pra fora”, alcançando o maior número possível de pessoas, e localização é um movimento “pra dentro”, focando em um número mais especializado de indivíduos.

Em outros materiais você vai encontrar o termo internacionalização como i18n, porque existem 18 letras entre o i e o n da palavra em inglês internationalization. Da mesma forma localização também aparece com l10n, vindo de localization.

Por que isso é importante?

É fácil deduzir porque o processo de localização é importante quando você tem um tema ou plugin em inglês e precisa dele em português. É o processo de internacionalização que muitas vezes é difícil de ser levado em consideração. Deixo aqui, então, duas coisas para você pensar:

  1. O core, plugins e temas do repositório são obrigatoriamente em inglês;
  2. O esforço de deixar seu tema ou plugin traduzível durante o desenvolvimento é muito menor do que fazer isso depois que ele estiver pronto. Seu cliente pode até não querer o site em outro idioma hoje, mas ele pode mudar de ideia amanhã.

O item 2 também está relacionado com a sua reputação. Alguém que pegar um código legado seu pode pensar que você fez aquilo de maneira desleixada, ou pior, por não saber como isso funciona ou sequer que isso existia.

Se você pensou “eu não ligo pro que os outros pensam de mim”, eu vou respeitar isso. Só prefiro não ter que manter um código que você fez xD

Para o core as strings sempre serão em inglês, mas para projetos privados as strings originais, presentes no código, podem estar em português mesmo.

O básico do gettext

Se a gente fosse criança, tudo seria resolvido com um monte de ifs. Se o idioma do site é inglês imprime Read More, se for português imprime Leia mais. O problema é que pode aparecer a necessidade e traduzir pra espanhol e esses ifs ficariam (ainda mais) bizarros. Para não ter esse problema, o WordPress usa o gettext.

O básico do gettext que vamos precisar entender agora é como os arquivos funcionam.

Arquivos .po

Os arquivos .po (PO de Portable Object) são os arquivos editáveis. São arquivos de texto, com as strings de origem, normalmente em inglês, e as de destino, no nosso caso em português, sendo um arquivo .po por idioma. Embora eles sejam arquivos de texto aberto é altamente recomendado editá-los com programas específicos como o PoEdit e o OmegaT. A Sheila Gomes, minha referência de tradução aqui no Brasil, oferece bastante material sobre o OmegaT.

Você também pode traduzir através da própria interface do WordPress com um plugin chamado Loco Translate. Falei sobre ele em um meetup há dois anos, pode ser que os slides ajudem em alguma coisa.

Além de todas essas, existe ainda a melhor forma de traduzir, que é através da ferramenta oficial. Pretendo fazer um outro texto falando só sobre isso, mas o importante aqui é que para traduzir dessa forma você deve seguir o nosso Guia de tradução.

Arquivos .mo

No fim das contas, são esses os arquivos que contam. MO, de Machine Object, são arquivos binários gerados a partir dos arquivos .po e que são lidos pelo servidor.

Embora sejam eles os arquivos que realmente importem, sempre mantenha o par .po/.mo. São arquivos extremamente leves e ter o .po à mão para gerar um novo .mo pode salvar a sua vida.

Promete que nunca vai separar os dois arquivos, né?

Arquivos .pot

Menos famoso, mas não menos importante, esse formato é o modelo para gerar novos arquivos .po, ou seja, se um plugin ou tema tem um arquivo .pot, você pode criar um .po a partir dele, sem ter que analisar o código atrás de chamadas para as funções de tradução. Nos arquivos .pot só existem as strings originais.

Na prática, como isso funciona?

Como exemplo, vamos pensar dentro do loop, no tema, um link para a interna do post:

<a href="<?php the_permalink(); ?>">Read More</a>

Para deixá-lo traduzível, seria só usar:

<a href="<?php the_permalink(); ?>"><?php _e( 'Read More', 'meu-tema' ); ?></a>

Viu o meu-tema ali? Esse é o text domain.

Definindo o text domain

O text domain, ou o domínio do texto, serve para agrupar as strings por tema ou plugin e separá-los do core, que usa default como domínio.

Na prática o seu domínio pode até ser qualquer texto, mas sempre use o slug do seu tema ou plugin. Para temas e plugins hospedados nos repositórios do WordPress isso é ainda mais fundamental: se o text domain não for igual ao plugin, por exemplo, ele não será traduzível pelo GlotPress.

Como esse é um parâmetro aceito por todas as funções de tradução, você pode se sentir tentado a usar uma constante ou uma variável. Não faça isso. Use sempre o formato de texto, assim você aumenta a compatibilidade do seu código com algumas ferramentas de análise. 

Funções de tradução no WordPress

No exemplo vimos a função _e(), que imprime uma string traduzida. Ela faz par com a função __(), que não imprime, mas retorna uma string traduzida. Elas funcionam como as funções printf e sprintf, nativas do PHP e que também serão úteis daqui a pouco.

Os nomes das funções de tradução do WordPress seguem alguns padrões:

Não confunda esse uso do underscore no nome da função com as funções privadas, como a _wp_get_current_user(). As funções de tradução que começam com _ tem nome realmente curtos, caso contrário ele é colocado depois, como no grupo de funções relacionadas à esc_attr__().

O que é contexto

As funções de tradução que contém a letra x permitem que você faça a desambiguação de um termo ou expressão. O core, por exemplo, usa _x( 'Add New', 'post' ) e _x( 'Add New', 'page' ). Um vira Adiconar novo, o outro Adicionar nova. Note que nas chamadas às funções de tradução feitas pelo core não há nenhum text domain.

O WooCommerce, como outro exemplo, usa _x( 'product', 'default-slug', 'woocommerce' ). Note que aqui já temos o domínio como terceiro parâmetro.

Placeholders e variáveis

Algumas strings tem conteúdo variável e, por isso, precisamos usar placeholders, isto é, variáveis que reservarão o espaço da informação que não temos no momento para que ela seja substituída quando for a hora.

O WooCommerce tem um exemplo útil: __( 'Order number: %s', 'woocommerce' ). Aqui, como na hora de traduzir a gente ainda não sabe o número do pedido, o %s serve só para guardar o lugar. Normalmente se usa o %s mesmo, mas outros formatos são descritos na documentação da sprintf.

Quando precisamos de mais de um placeholder, o certo é numerá-los. Veja a string __( '%1$s created the group %2$s', 'buddypress'), por exemplo. Dessa forma, numerando as marcações, é possível traduzir como O grupo %2$s foi criado pelo usuário %1$s, usando o segundo valor antes do primeiro.

É na substituição desses placeholders pelas informações reais que as funções sprintf e printf serão úteis. Neste último exemplo do BuddyPress, o código completo é:

$action = sprintf( __( '%1$s created the group %2$s', 'buddypress'), $user_link, $group_link );

Adicionando notas para os tradutores

Nem sempre é óbvio o que será substituído em uma string e, para ajudar, é possível colocar uma nota. Para isso basta colocar um comentário PHP na linha anterior à chamada da função e começar seu texto com translators:, dessa forma:

/* translators: %s: Weight unit */
sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ) => 'weight',

Se você programa usando o WordPress Coding Standards, já deve ter notado que ele reclama quando não há comentários nesses casos.

Plurais

Outro exemplo de quando é preciso substituir placeholders são as funções de plural. A função mais simples, _n(), aceita 4 parâmetros: a string no singular, a string no plural, o número que será usado para decidir e o text domain. Na documentação existe um exemplo bem fácil para entender:

$rating = '3';

$text = sprintf( _n( '%s star', '%s stars', $rating, 'wpdocs_textdomain' ), $rating );

Um detalhe importante aqui: se você está fazendo código para o core ou tem alguma intenção de alcançar o público russo ou de idiomas eslavos, não considere essa função para cenários de um ou muitos. Deixei isso explicado em um comentário lá na documentação.

Resumão das funções

Básicas

  • __(): retorna uma string.
  • _e(): imprime uma string.
  • _x(): retorna uma string, mas permite contexto.
  • _ex(): imprime uma string, mas permite contexto.
  • _n(): retorna uma string com plural.
  • _nx(): retorna uma string com plural, mas permite contexto.
  • _n_noop(): permite registrar uma string com plural, sem passar um operador (no-op). Esta função retorna um array com as informações e pode ser usada com a função translate_nooped_plural().
  • _nx_noop(): mesma coisa que a de cima, mas permitindo contexto.
  • translate_nooped_plural(): útil para pegar o índice certo do array retornado por _n_noop() e _nx_noop().

Funções com escape

Data e número

Como criar o arquivo .pot do seu plugin ou tema

A forma mais fácil de criar um arquivo de modelo de tradução do seu tema ou plugin é através do wp-cli. Na versão 2.0.1, a mais recente enquanto eu escrevo esse texto, é só você entrar na pasta do seu plugin, por exemplo, e  executar:

wp i18n make-pot . languages/slug-do-seu-plugin.pot

Como eu expliquei no começo, a partir desse arquivo você pode gerar o arquivo .po do locale para o qual você pretende traduzir.

Carregando os arquivos de tradução

Por padrão, o WordPress lê os arquivos .po da pasta wp-content/languages, cuidando assim das coisas baixadas através do repositório. Se você está fazendo um plugin ou um tema para um cliente, você precisará dizer ao WP onde ele deve buscar os arquivos com as traduções.

Plugins

Para plugins você deve associar ao hook plugins_loaded uma chamada à função load_plugin_textdomain:

function seu_plugin_load_plugin_textdomain() {
    load_plugin_textdomain( 'seu-text-domain-aqui', false, basename( dirname( __FILE__ ) ) . '/languages/' );
}
add_action( 'plugins_loaded', 'seu_plugin_load_plugin_textdomain' );

Isso fará o WordPress procurar pelo arquivo {text-domain}-{locale}.mo na pasta languages do seu plugin. No nosso exemplo, para português do Brasil, o arquivo seria o wp-content/plugins/teste/languages/teste-pt_BR.mo.

Para alguns plugins maiores, a função seu_plugin_load_plugin_textdomain() poderia ser um pouco mais elaborada. O BuddyPress, por exemplo, oferece o filtro buddypress_locale_locations na função bp_core_load_buddypress_textdomain para que os desenvolvedores possam carregar traduções personalizadas sem ter que alterar nem o plugin nem a tradução presente no repositório.

Temas

Para temas o esquema é parecido, mas você deve associar ao hook after_setup_theme uma chamada à função load_theme_textdomain ou load_child_theme_textdomain, se estiver usando um tema descendente (nome atual para os temas filhos):

function seu_tema_load_theme_textdomain() {
    load_theme_textdomain( 'seu-text-domain-aqui', get_stylesheet_directory() . '/languages' );
}
add_action( 'after_setup_theme', 'seu_tema_load_theme_textdomain' );

Existe uma outra diferença muito importante: para temas, diferente do que acontece nos plugins, o nome do arquivo deve conter apenas o locale e a extensão. Então, por exemplo, o arquivo seria wp-content/themes/teste/languages/pt_BR.mo.

Traduções de temas e plugins desativados

Em algumas situações o WordPress precisa das traduções dos temas e plugins sem executar seus códigos. Exemplos disso são as listas do repositório e a lista de plugins no Painel, que exibe os nomes e descrições traduzidos mesmo dos plugins que estão desativados.

Para resolver é simples: tanto para temas quanto para plugins, você precisa definir no cabeçalho dois valores: Text Domain e Domain Path. Um exemplo:

/*
 * Theme Name:  Meu tema
 * Author:      Felipe Elia
 * Text Domain: meu-tema
 * Domain Path: /languages
 */

Textos em arquivos JavaScript

A vinda do Gutenberg está alterando a forma como a API do WordPress lida com traduções, mas por enquanto tudo é feito através de PHP. Por isso, a forma mais simples de traduzir um texto usado em um arquivo js é usar a função wp_localize_script.

Vamos pensar na string Loading..., que pode substituir o conteúdo de alguma coisa durante uma chamada AJAX. Ao invés de usar

$( '#minha-div' ).html( 'Loading...' );

você pode associar a tradução da string à uma variável logo depois de incluir o arquivo JavaScript. No PHP use

wp_enqueue_script( 'script-do-tema', get_template_directory_uri() . '/assets/js/scripts.js' );
wp_localize_script( 'script-do-tema', 'tema_l10n',
	array(
		'loading' => __( 'Loading...', 'meu-tema' ),
	)
);

e no js use

$( '#minha-div' ).html( tema_l10n.loading );

É um texto gigante, eu sei. Se achou algum erro não deixe de avisar e se você gostou não se esqueça de comentar e de se cadastrar na newsletter.