O php-gettext é uma biblioteca em PHP que emula as funcionalidades do gettext, que por sua vez é uma poderosa biblioteca para suporte de múltiplos idiomas em qualquer sistema (inclusive o sistema operacional). O gettext é utilizado pela maioria dos programas GNU e do Linux e por isso é o preferido da galera também em muitos sistemas PHP.
Basicamente, utilizar o gettext significa que:
- Toda mensagem dentro de um sistema vai ser chamado via uma função específica, ao invés de apenas mostrar pro usuário;
- Um programa vasculha o código-fonte e compila um arquivo com todas as mensagens do sistema (esse arquivo se chama potfile);
- Os tradutores traduzem apenas este arquivo potfile para o idioma que quiserem;
- O desenvolvedor pega o arquivo traduzido e compila-o para o formato gettext, colocando num diretório dentro do sistema;
- A partir daí, toda chamada para a função (item 1) vai primeiro procurar nos arquivos traduzidos, e se achar a mensagem, já mostra traduzido.
Principalmente por causa dos itens 2 e 3, fica muito fácil manter múltiplas traduções de um sistema, pois o processo fica bastante automatizado e fácil para apenas fazer o que deve ser feito: traduzir as mensagens!
Vamos então ver como fazer isso na prática, usando o PHP!
php-gettext
No PHP, existem duas formas de usar os métodos gettext: uma extensão nativa e uma biblioteca separada. Neste tutorial, vamos aprender a usar a biblioteca separada.
A razão por escolher o php-gettext está em uma preferência pessoal. A extensão nativa do PHP é rápida e bem suportada pelo PHP, mas ela é uma extensão nativa! Isso quer dizer que o PHP precisa ter essa extensão compilada como módulo e habilitada nas configurações. Por experiência própria, esse tipo de dependência gera dificuldades na hora de instalar sistemas em, por exemplo, servidores de hostings compartilhados e distribuições Linux que não tem os pacotes já prontos. Enquanto isso, o php-gettext é totalmente auto-suficiente: é só colocar uns arquivos php no sistema, carregá-los via include/require e pronto, já está funcionando. Apesar da biblioteca ser um pouquinho (só um pouquinho) mais lenta que a extensão nativa, vale muito à pena pela praticidade.
Pra começar, baixe o pacote no site do gettext. Neste tutorial estou usando a versão 1.0.11.
A extensão mbstring é necessária para o php-gettext funcionar. Felizmente, ela é uma extensão que já vem na grande maioria das distribuições Linux e hostings.
Descompactando o arquivo, temos três arquivos principais:
- gettext.php: As funções de emulação do gettext;
- gettext.inc: Aliases de funções para você usar no seu sistema;
- streams.php: Classes e métodos para ler os arquivos do gettext.
Você só precisa desses 3 arquivos para usar o php-gettext.
Usando o php-gettext em um sistema
Vamos criar um sistema novo chamado hello-multi-world. Inicialmente, o sistema está com esses arquivos:
hello-multi-world/ | lib/ | | gettext.inc | | gettext.php | \ streams.php | | config.php | i18n.php \ index.php
Repare que coloquei os 3 arquivos dentro de um diretório lib. Além dos 3 arquivos do php-gettext, temos o config.php que vai carregar as configurações, o i18n.php que contém a inicialização do gettext e qualquer função que eu queira criar e o index.php que é a página em si, com as mensagens para tradução.
Então o que vamos fazer primeiro é configurar um idioma padrão, que será o inglês. É uma boa idéia criar o idioma padrão em inglês porque você vai programando o sistema e inglês, e a partir do inglês os tradutores traduzem para outros idiomas (como o inglês é o mais popular, é mais provável que o tradutor saiba inglês e outro idioma).
Começamos então alterando as linhas do config.php:
<?php define('LANG','en_US'); ?>
Inicializando o php-gettext
Uma vez definido o idioma padrão, é a vez da gente inicializar o php-gettext, preparar para o uso.
Antes de começar a codificar, tenha em mente dois conceitos:
- locale é uma string no formato xx_YY que identifica o idioma. Por exemplo: en_US para inglês dos Estados Unidos, pt_BR para português do Brasil, pt_PT para português de Portugal, e por aí vai. A página de Internacionalização de Software na Wikipedia contém uma lista com diversos locales.
- textdomain, ou domínio de texto, é um tipo de namespace onde as traduções são colocadas. Em um sistema complexo podem haver diversas traduções diferentes para vários módulos ou plugins, cada um tendo o seu próprio domínio de texto e arquivos potfiles. Muito útil para modularizar a tradução junto com o sistema. No nosso caso, como vamos mostrar algo simples, só usaremos um textdomain: hello_multi_world.
É a vez de editar o arquivo i18n.php com o seguinte conteúdo:
<?php require_once('config.php'); $locale = LANG; $textdomain = "hello_multi_world"; $locales_dir = dirname(__FILE__) . '/i18n'; if (isset($_GET['locale']) && !empty($_GET['locale'])) $locale = $_GET['locale']; putenv('LANGUAGE=' . $locale); putenv('LANG=' . $locale); putenv('LC_ALL=' . $locale); putenv('LC_MESSAGES=' . $locale); require_once('lib/gettext.inc'); _setlocale(LC_ALL, $locale); _setlocale(LC_CTYPE, $locale); _bindtextdomain($textdomain, $locales_dir); _bind_textdomain_codeset($textdomain, 'UTF-8'); _textdomain($textdomain); function _e($string) { echo __($string); } ?>
Explicando as linhas:
- 03 carrega a configuração de idioma padrão do config.php (constante LANG);
- 05-07 definem em variáveis o locale (de acordo com a constante LANG), o domínio de texto e o lugar onde vão estar os arquivos de tradução do gettext;
- 08-09 permitem que uma variável locale seja passada na URL para vermos diferentes idiomas que não são o padrão;
- 12-15 mudam também as variáveis de ambiente do sistema operacional para o nosso locale;
- 17 finalmente carrega a biblioteca php-gettext;
- 19-20 carregam o locale agora também para o php-gettext;
- 22-24 definem o textdomain, dizendo que a codificação usada nos arquivos é UTF-8 e que esses arquivos estão no diretório i18n (locales_dir);
- 26-28 correspondem à um alias de função que criei.
Em resumo: defini qual meu locale e o diretório que os arquivos vão ficar, carreguei a biblioteca e inicializei o domínio de texto. Com isso, as funções do php-gettext já podem ser usadas, e toda vez que forem usadas, vão procurar os arquivos nos lugares corretos.
Usando as funções
Agora edite o arquivo index.php:
<?php require_once('config.php'); require_once('i18n.php'); ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title><?php echo __('Hello Multi World!'); ?></title> </head> <body> <h1><?php _e('Hello Multi World!'); ?></h1> </body> </html>
Repare nas linhas 10 e 13. Na linha de título, usei o echo na função __ (dois underlines), que é a função do gettext para traduzir uma string. Ja na linha do h1, usei a minha função _e, que nada mais é que um alias para o “echo __()”. Ambas as linhas fazem a mesma coisa então: imprimem a mensagem traduzida.
Olha o resultado ao acessar o index.php no navegador:
Mas pera. Aí ainda não tem nada mágico…
Criando o potfile e traduzindo
Agora é hora de criar o arquivo que você vai mandar pros tradutores, o potfile (que nome sugestivo). Para fazer isso, você pode usar alguns comandos do próprio gettext no Linux ou usar um programa como o POEdit (muito bom!).
Utilizando a linha de comando:
ARQUIVO_TMP="/tmp/arquivos-hmw-$$.txt" find ./ -type f -name \*.php > $ARQUIVO_TMP xgettext -k_e -k__ -L PHP --from-code utf-8 --no-wrap -d hello_multi_world -o hello_multi_world.pot -f $ARQUIVO_TMP rm -f $ARQUIVO_TMP
O comando find procura todos os arquivos com extensão .php no diretório atual e manda pra o programa xgettext analisar. Essa ferramenta busca dentro do código fonte (-L PHP para linguagem em PHP) todas as funções que chamadas “_e” e “__” (parâmetro -k) e coloca no arquivo (parâmetro -o) hello_multi_world.pot.
Vamos dar uma olhada no arquivo:
# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-05-21 20:49-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <[email protected]>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: index.php:10 index.php:13 msgid "Hello Multi World!" msgstr ""
No arquivo potfile, as mensagens que devem ser traduzidas seguem esse formato:
msgid "Mensagem original" msgstr "Mensagem traduzida"
O comentário àcima dessas linhas corresponde ao lugar que as mensagens são usadas.
Agora os tradutores, com esse arquivo em mãos, traduzem as linhas com msgstr e mandam de volta pro desenvolvedor. No nosso caso, vamos traduzir para o Português do Brasil e para Português de Portugal a única mensagem que usamos.
Primeiro crie um diretório chamado i18n que ficará assim:
i18n/ | pt_BR/ | \ LC_MESSAGES/ | \ pt_BR.po | | pt_PT/ | \ LC_MESSAGES/ | \ pt_PT.po | \ hello_multi_world.pot
Ou seja, na raiz temos o hello_multi_world.pot original e sem traduções, para você enviar aos tradutores (ou iniciar uma tradução do zero). Depois temos diretórios com o locale, subdiretórios LC_MESSAGES (é o nome de diretório que o gettext procura os arquivos), e o arquivo potfile já traduzido (hello_mutli_world.po).
Depois dos diretórios LC_MESSAGES criados, use o msginit para iniciar a tradução:
# nova tradução para pt_BR msginit -l pt_BR --no-wrap --no-translator -o i18n/pt_BR/LC_MESSAGES/hello_multi_world.po -i i18n/hello_multi_world.pot # nova tradução para pt_PT msginit -l pt_PT --no-wrap --no-translator -o i18n/pt_PT/LC_MESSAGES/hello_multi_world.po -i i18n/hello_multi_world.pot
Agora é só editar estes arquivos de extensão .po e traduzir (ou usar o programa POEdit como dito anteriomente).
Na hora de traduzir o potfile, traduza também o seu cabeçalho, que contém as informações sobre o projeto, o time de tradução, a última vez que o arquivo foi traduzido, entre outros. No mínimo, substitua o PACKAGE_VERSION com a versão do pacote/tradução e CHARSET por UTF-8 (codificação usada no arquivo). Os exemplos a seguir já tem esses valores substituídos.
Então eu traduzo os arquivos, deixando um em i18n/pt_BR/LC_MESSAGES/hello_multi_world.po:
# Portuguese translations for PACKAGE package. # Copyright (C) 2013 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2013. # msgid "" msgstr "" "Project-Id-Version: hello_multi_world\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-05-21 20:49-0300\n" "PO-Revision-Date: 2013-05-21 20:49-0300\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: index.php:10 index.php:13 msgid "Hello Multi World!" msgstr "Olá Multi Mundo!"
E outro em i18n/pt_PT/LC_MESSAGES/hello_multi_world.po:
# Portuguese translations for PACKAGE package. # Copyright (C) 2013 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2013. # msgid "" msgstr "" "Project-Id-Version: hello_multi_world\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-05-21 20:49-0300\n" "PO-Revision-Date: 2013-05-21 20:49-0300\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: index.php:10 index.php:13 msgid "Hello Multi World!" msgstr "Olá Multi Mundo, ora pois!"
Esses arquivos tem o nome hello_multi_world.po porque esse é o nome do textdomain usado no nosso código.
Compilando os arquivos de tradução
Com os potfiles traduzidos em seus devidos lugares, agora falta só compilá-los para o formato binário/indexado do gettext, com o comando:
# compila o pt_BR msgfmt -c -o i18n/pt_BR/LC_MESSAGES/hello_multi_world.mo i18n/pt_BR/LC_MESSAGES/hello_multi_world.po # compila o pt_PT msgfmt -c -o i18n/pt_PT/LC_MESSAGES/hello_multi_world.mo i18n/pt_PT/LC_MESSAGES/hello_multi_world.po
Pronto! O msgfmt compila o potfile e cria um arquivo de extensão .mo, que já está pronto para ser usado!
Visualizando os diferentes idiomas
Agora você pode visualizar os idiomas de duas formas:
- Mudando o idioma padrão no config.php;
- Adicionando um ?locale=xx_YY na URL.
Veja como ficou nossa página traduzidas:
Pronto, pode sair traduzindo seus sistemas :)
Técnicas de código e tradução
Na hora de escrever os códigos, é importante separar totalmente as mensagens e ainda tomar cuidado com as diferenças entre um idioma e outro. Existem algumas técnicas úteis no gettext para que você consiga fazer as traduções no código de uma boa maneira.
Por exemplo, a frase:
Você tem 5 mensagens.
Num sistema que exibe a quantidade de mensagens, o número 5 pode variar. Pode ser 1, pode ser 1000. Como você iria traduzir isso com gettext?
Variáveis dentro da tradução
Nunca coloque variáveis dentro da tradução. Fazer isto é errado>:
<?php echo _("Você tem $n mensagens."); ?>
A variável $n pode mudar para qualquer número! Em outras palavras, no potfile você vai ter que ter várias linhas de tradução, cada um para um número. Insano não?
Ao invés de usar variáveis dentro da função de tradução, use a função sprintf para encapsular a sua mensagem, dessa forma:
<?php echo sprintf(_("Você tem %d mensagens."), $n); ?>
No potfile, o tradutor terá que traduzir apenas uma única frase:
msgid "Você tem %d mensagens."
E o sprintf se encarregará de substituir o %d por qualquer valor que $n representar. Simples não? Consulte a página de referência do sprintf para obter mais informações de como usá-lo (e o que diabos significa esse %d).
Plural
Ok, e se na frase anterior, o usuário tiver apenas UMA mensagem? A frase ficaria:
Você tem 1 mensagens.
Uma mensagens? Que horrível! Mataram o português!
Para usar plural, usamos a função _ngettext assim:
<?php echo sprintf(_ngettext("Você tem %d mensagem", "Você tem %d mensagens", $n), $n); ?>
O primeiro argumento do _ngettext é a forma singular, o segundo é a forma plural, e o terceiro é o número que diz qual dos dois vai ser usado. Se $n for 1, ele usa o singular (primeiro argumento), se for 2 ou mais, usa o plural (segundo argumento).
Ao usar a função _ngettext, lembre-se de especificar na hora de criar o potfile:
ARQUIVO_TMP="/tmp/arquivos-hmw-$$.txt" find ./ -type f -name \*.php > $ARQUIVO_TMP xgettext -k_e -k__ -k_ngettext:1,2 -L PHP --from-code utf-8 --no-wrap -d hello_multi_world -o hello_multi_world.pot -f $ARQUIVO_TMP rm -f $ARQUIVO_TMP
E as linhas de tradução do potfile vão ser geradas assim:
#: index.php:16 #, php-format msgid "Você tem %d mensagem" msgid_plural "Você tem %d mensagens" msgstr[0] "" msgstr[1] ""
Onde a linha msgstr[0] será a mensagem singular traduzida e a linha msgstr[1] será a forma plural traduzida.
Outras sugestões
Evite quebrar muito a tradução e tente sempre colocar mensagens com frases completas.
Isto seria errado:
<?php # $estado pode ser mal ou bem echo _("Você está") . $estado . _("consigo mesmo."); ?>
Isto seria o correto:
<?php # $estado pode ser mal ou bem echo sprintf(_("Você está %s consigo mesmo."), $estado); ?>
A razão é óbvia: você deixa um contexto melhor pro tradutor e ele traduz uma entrada ao invés de duas.
Outra coisa: tente não ficar usando elementos de código como HTML nas mensagens.
Isto seria errado:
<?php echo _("<h1>Bem vindo</h1>"); ?>
Isto seria certo:
<?php echo "<h1>" . _("Bem vindo") . "</h1>"; ?>
Além das tags HTML não fazerem sentido nenhum para a tradução, a própria mensagem pode ser usada em qualquer outro lugar da página, independente de estilo/posição.
Referências
- Arquivo README e diretório examples do pacote php-gettext
- Página de Referência da função PHP sprintf
- Internationalization: You’re probably doing it wrong