Português do Brasil English
Devin no Facebook  Devin no Twitter  RSS do Site 
Programação    

php-gettext: Múltiplos idiomas em um sistema PHP


Comentários  16
Visualizações  
565.642

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:

  1. Toda mensagem dentro de um sistema vai ser chamado via uma função específica, ao invés de apenas mostrar pro usuário;
  2. Um programa vasculha o código-fonte e compila um arquivo com todas as mensagens do sistema (esse arquivo se chama potfile);
  3. Os tradutores traduzem apenas este arquivo potfile para o idioma que quiserem;
  4. O desenvolvedor pega o arquivo traduzido e compila-o para o formato gettext, colocando num diretório dentro do sistema;
  5. 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.

NOTA
NOTA


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:

php-gettext hello world

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).

AVISO
AVISO


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:

php-gettext hello multi world pt_BR

php-gettext hello multi world pt_PT

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

565.642

Comentários  16
Visualizações  
565.642


TagsLeia também

Apaixonado por Linux e administração de sistemas. Viciado em Internet, servidores, e em passar conhecimento. Idealizador do Devin, tem como meta aprender e ensinar muito Linux, o que ele vem fazendo desde 1997 :-)


Leia também



Comentários

16 respostas para “php-gettext: Múltiplos idiomas em um sistema PHP”

  1. Gabriel disse:

    MARAVILHA de tutorial, simples e objetivo! Obrigado pela ajuda.

  2. Jonathan disse:

    Olá amigo possui algum sistema pronto php onde o usuário apenas clica no idioma e todo site muda o idioma?
    conhece alguns?

  3. carla disse:

    Olá, Hugo,

    Estou tendo um problema com caracteres com acento. Ao tentar traduzir a frase 'Olá, mundo!' apareceu a mensagem: 'invalid multibyte sequence'. Estou usando o UTF-8 e mesmo assim o erro aparece. Você saberia como resolver?
    Aliás, excelente tutorial. =]

  4. Pedro disse:

    É possível editar o url para ficar do género http://…/BR/pagina ?

    • Saulo Lins disse:

      Possivel é mas depende de onde seu site esta hospedado, se é em um servidor linux você vai criar uma regra no .htacess para definir que suindex.php?lang=BR vire /BR/

  5. gabriel disse:

    cara como eu faço para traduzir uma cms ingles ?

  6. Fran disse:

    Gostei!
    Você poderia me esclarecer uma dúvida? Tenho todos os textos em português e inglês no meu banco de dados, quero obter os dados do banco de dados e usar esse mesmo modelo é possível?
    Obrigada.

  7. masaki disse:

    Muito bom o artigo. Obrigado.

  8. Robson disse:

    Muito bom o artigo.. valeu por compartilhar!

  9. Israel disse:

    eitchugo pode me ajudar ? como traduzo um arquivo que está como texto 'AQUI O TEXTO' o texto está dentro de um if, dentro de um controller

  10. Luiz Carlos disse:

    Esse procedimento é pra traduzir sistemas ou posso traduzir um tema de um site desta forma?

  11. jhowsanttana disse:

    Super valeu pelo post… Estou fazendo um sistema e esse artigo foi super hiper mega últil. Ahh só uma dica, usei esse site para converter de .po para .mo – https://po2mo.net/

  12. Wassup, só queria mencionar , еu amei isto
    blog post. Foі prático . Continue postando lá!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *