Na web  atual os websites  e aplicações são acessados por um público distinto de usuários, cada um com sua cultura e idioma. Este fato pode ser percebido pela gama de serviços grandes, como Google e Twitter, que tendem a verificar o idioma nativo do usuário e adequar o conteúdo a este idioma. O Zend Framework é famoso por fornecer componentes para funcionalidades triviais de aplicações web  atuais, e a internacionalização e localização também é suportada nesta gama de componentes. Este artigo demonstrará os principais componentes envolvidos neste processo, com explanações detalhadas e exemplos práticos, incluindo duas abordagens de implementação no MVC do Zend Framework.

Introdução

Internacionalização (i18n) é o processo de construir uma aplicação que possa ser adaptada para diversos idiomas e regiões sem a necessidade de mudanças drásticas nos processos de engenharia do sistema. Localização (l10n) de um sistema é o processo de adaptar um sistema já internacionalizado para uma região ou idioma específico adicionando componentes para cada localização e traduzindo textos. Na localização são incluídos recursos para utilizar formatos de data/hora, moedas, convenções específicas para pluralização de palavras, além de tradução de idiomas.

Em suma, a internacionalização é a capacidade e flexibilidade de um sistema de mudar sua escrita para diversos idiomas utilizando algum mecanismo com essas capacidades, como, por exemplo, um bundle ou gettext. A localização fica com a responsabilidade de escolher e identificar o idioma e necessidades culturais exigidas pelo usuário, definindo qual o idioma internacionalizado deverá ser utilizado.

No Zend Framework o processo de localização e internacionalização é formado por diversos componentes, cada um com uma responsabilidade distinta:

  • Zend_Locale: Suporte de locais disponíveis para a localização dentro de outros componentes do Zend Framework;
  • Zend_Translate: Tradução de textos;
  • Zend_Date: Localização de data/hora;
  • Zend_Currency: Localização de moedas;
  • Zend_Locale_Format: Análise e geração de números localizados.
  • Zend_Locale_Data: Obtém textos padrões localizados como, por exemplo, nomes de países, nomes de linguagem e etc.

Zend_Locale

O Zend_Locale é o principal componente para identificar o idioma e a região de um usuário. Esta identificação é feita através de um locale, que é uma string padronizada que identifica estes parâmetros no formato idioma_REGIÃO. Por exemplo, o locale pt_BR representa o idioma Português do Brasil. Existe uma lista contendo combinações de locales que pode ser utilizada como referência para construir um locale. Todos os componentes do Zend Framework que suportam internacionalização e localização, utilizam o Zend_Locale, ou um locale, para fornecer normalização e formatação conforme o idioma/região do usuário.

Existem algumas formas de se configurar o Zend_Locale:

// Construtor padrão, pega o locale do browser
$locale = new Zend_Locale();
// Construtor com um locale específico
$forceLocale = new Zend_Locale('pt_BR');
// Pega o locale do browser
$browserLocale = new Zend_Locale(Zend_Locale::BROWSER);
// Pega o locale do servidor
$serverLocale = new Zend_Locale(Zend_Locale::ENVIRONMENT);
// Pega o locale do framework
$frameworkLocale = new Zend_Locale(Zend_Locale::FRAMEWORK);

Existem casos onde não é possível detectar automaticamente o locale do usuário (como, por exemplo, na linha de comando), e para isso é necessário configurar um locale padrão, conforme o código a seguir:

Zend_Locale::setDefault('pt_BR');

Após a configuração do Zend_Locale, existem duas formas de utilizá-lo nas classes locale-aware (que suportam l10n), a primeira é definir manualmente o locale a ser utilizado e a segunda é configurar o locale para todas os componentes do Zend Framework:

$ptBR = new Zend_Locale('pt_BR');
$enUS = new Zend_Locale('en_US');
// No componente
$date = new Zend_Date(null, null, $enUS);
echo $date;
// Para todos os componentes
Zend_Registry::set('Zend_Locale', $ptBR);
$currency = new Zend_Currency();
echo $currency;

A classe Zend_Locale fornece diversos métodos úteis para o processo de internacionalização de uma aplicação. Dentre estes métodos encontram-se:

Zend_Translate

Após os componentes do Zend Framework saberem exatamente o idioma/região do usuário, a próxima etapa é fornecer o conteúdo textual da aplicação traduzido. Essa etapa fica à cargo do componente Zend_Translate. Existem diversos adapters para fornecer as traduções da aplicação, alguns deles são listados a seguir:

  • Array - As string são fornecidas a partir de arrays PHP;
  • Gettext - Arquivos de string e traduções gettext;
  • Csv - Arquivos no formato CSV.

O manual do Zend Framework sugere tipos diferentes de estrutura de diretórios para armazenar os textos internacionalizados, neste artigo será utilizada a estrutura single directory, onde existirá um arquivo para cada idioma suportado, dentro da pasta languages. Para maiores informações consulte as referências do artigo. Dentro deste diretório serão criados os arquivos en.php, es.php e pt.php, cada um contendo um array com três strings, e será utilizado o adapter Array. O conteúdo dos arquivos é apresentado abaixo:

//en.php
return array(
    'Exemplo' => 'Sample',
    'Internacionalizado' => 'Internationalized',
    'Texto 2' => 'Text 2'
);
// es.php
return array(
    'Exemplo' => 'Ejemplo',
    'Internacionalizado' => 'Internacionalizados',
    'Texto 2' => 'Texto 2'
);
// pt.php
return array(
    'Exemplo' => 'Exemplo',
    'Internacionalizado' => 'Internacionalizado',
    'Texto 2' => 'Texto 2'
);

Uma instância do Zend_Translate pode ser feita com o código a seguir:

$translate = new Zend_Translate(
    array(
        'adapter' => 'array',
        'content' => '/languages/en.php',
        'locale'  => 'en'
    )
);

O parâmetro adapter define qual adapter será utilizado no processo de tradução, content a fonte de dados onde se encontram as traduções e locale define o locale padrão para a fonte de dados informada. A próxima etapa é adicionar mais idiomas para o componente, conforme o código a seguir:

$translate->addTranslation(
    array(
        'content' => '/languages/pt.php',
        'locale' => 'pt'
    )
);
$translate->addTranslation(
    array(
        'content' => '/languages/es.php',
        'locale' => 'es'
    )
);

Com o Zend_Translate devidamente configurado, é possível utilizar strings internacionalizadas da seguinte forma:

// Retorna a string internacionalizada
$translate->_("Exemplo");
// Imprime a string internacionalizada
echo $translate->_("Internacionalizado");
// Obtém a string de acordo com o locale
echo $translate->_("Exemplo", "es");

O método _() recebe como parâmetro a chave que identifica o texto internacionalizado, e retorna o texto de acordo com a chave e o locale atual. Esta chave pode ser o texto em si, um índice numérico único, ou uma string única. Opcionalmente o método aceita como segundo parâmetro o locale a ser utilizado.

Além de obter uma string de acordo com o locale pelo método _(), é possível definir o locale atual da instância de Zend_Translate, com o método setLocale. Caso seu parâmetro seja um locale completo (en_US, por exemplo), e exista um arquivo somente identificado pelo nome do idioma, o componente procurará pela tradução mais próxima, no caso apresentado seria o en. E para verificar se uma determinada tradução existe, o método utilizado é o isAvailable. O código abaixo apresenta o uso de ambos os métodos.

// Mudando o locale, usará a tradução "pt"
$translate->setLocale("pt_BR");
echo $translate->_("Exemplo");
// Verifica se existe um locale
if ($translate->isAvailable("it")) {
    echo $translate->_("Pizza", "it");
}

Existem diversas opções e recursos adicionais para o componente Zend_Translate, um destes recursos é o de auto-detectar os arquivos de tradução de um diretório. Como dito anteriormente, o esquema de arquivos de tradução utilizado é o single directory, onde cada idioma suportado estará em seu arquivo específico, que terá como nome o idioma. Ao se configurar o parâmetro scan e content, na instanciação do Zend_Translate, o diretório em content será lido, e cada arquivo de tradução será adicionado automaticamente, sem a necessidade de chamar o método addTranslation. Existe dois valores possíveis para scan: Zend_Translate::LOCALE_FILENAME e Zend_Translate::LOCALE_DIRECTORY, cada um segue uma uma convenção diferente de estrutura de arquivos de internacionalização. Para este exemplo de single directory, o LOCALE_FILENAME é utilizado. O exemplo a seguir ilustra isso:

$translate2 = new Zend_Translate(
    array(
        'adapter' => 'array',
        'content' => 'languages/',
        'scan' => Zend_Translate::LOCALE_FILENAME
    )
);
echo $translate2->_("Exemplo") . "\n";
echo $translate2->_("Exemplo", "es") . "\n";
echo $translate2->_("Exemplo", "en") . "\n";

Existem ainda mais opções, consulte as referências do artigo para a documentação completa e opções do componente.

Aplicação de Exemplo

Depois de um pouco de teoria e estudo sobre a base de i18n e l10n com o Zend Framework é hora de criar uma aplicação simples com suporte a três idiomas. Duas abordagens de implementação serão apresentadas, a primeira será a detecção do idioma do usuário através do browser e a segunda através de URLs da aplicação (rotas).

Primeiramente, é necessário criar uma aplicação Zend Framework:

zf create project internationalization-localization

Os idiomas suportados serão: inglês (en), português (pt), espanhol (es). O adapter utilizado será o array, e os arquivos ficarão na pasta languages. O conteúdo de cada arquivo é apresentado abaixo:

// en.php
return array(
    'Titulo da Pagina' => 'Page Title',
    'Texto Internacionalizado de Exemplo' => 'Internationalized Text Sample',
    "A data atual é: %1\$s" => "The current date is: %1\$s",
    'O valor monetário do locale é:' => 'The currency value with the locale is:',
    'O locale de tradução é:' => 'The translate locale is:',
    'O Zend_Locale é:' => 'The Zend_Locale is:'
);
// es.php
return array(
    'Titulo da Pagina' => 'Título de la Página',
    'Texto Internacionalizado de Exemplo' => 'Ejemplo de Texto Internacionalizado',
    "A data atual é: %1\$s" => "La fecha actual es: %1\$s",
    'O valor monetário do locale é:' => 'El valor monetario de lo locale es:',
    'O locale de tradução é:' => 'La traducción es:',
    'O Zend_Locale é:' => 'El Zend_Locale es:'
);
// pt.php
return array(
    'Titulo da Pagina' => 'Título da Página',
    'Texto Internacionalizado de Exemplo' => 'Texto Internacionalizado de Exemplo',
    "A data atual é: %1\$s" => "A data atual é: %1\$s",
    'O valor monetário do locale é:' => 'O valor monetário do locale é:',
    'O locale de tradução é:' => 'O locale de tradução é:',
    'O Zend_Locale é:' => 'O Zend_Locale é:'
);

Alguns parâmetros de configuração (application/configs/application.ini) podem ser aproveitados em ambas as abordagens. O primeiro passo é configurar o Zend_Locale, conforme apresentado abaixo:

resources.locale.default = "en_US"

Essa configuração, define que o locale padrão da aplicação será o en_US. Ao adicionar esse parâmetro de configuração, o Zend_Application_Resource_Locale configurará o componente Zend_Locale e, após definir as configurações, armazenará o objeto Zend_Locale no Zend_Registry com a chave "Zend_Locale", o que garante que todos os componentes locale-aware utilizarão o objeto definido.

O próximo passo é configurar o Zend_Translate, conforme os idiomas e arquivos-fonte da aplicação:

resources.translate.adapter = "array"
resources.translate.data = APPLICATION_PATH "/../languages"
resources.translate.options.scan = "filename"
resources.translate.options.disableNotices = true

Similar ao Zend_Locale, essa configuração irá definir uma instância de Zend_Translate para a aplicação através do Zend_Registry, neste caso com a chave "Zend_Translate". Para exemplificar a utilização de ambos os componentes, o arquivo application/views/scripts/index/index.phtml será modificado para conter o seguinte código:

<?php $locale = Zend_Registry::get('Zend_Locale'); ?>
<html>
    <head>
        <meta charset="UTF-8" />
    </head>
    <body>
        <h1><?php echo $this->translate("Titulo da Pagina"); ?></h1>
        <p><?php echo $this->translate("Texto Internacionalizado de Exemplo"); ?></p>
        <p><?php echo $this->translate("A data atual é: %1\$s", new Zend_Date()); ?>
        <p><?php echo $this->translate("O valor monetário do locale é:"); ?> <?php echo new Zend_Currency(); ?></p>
        <p><?php echo $this->translate("O locale de tradução é:"); ?> <?php echo $this->translate()->getLocale(); ?></p>
        <p><?php echo $this->translate("O Zend_Locale é:"); ?> <?php echo $locale; ?></p>
    </body>
</html>

É possível notar a chamada para $this->translate(), essa chamada irá executar uma instância de Zend_View_Helper_Translate, e internamente chamar o método Zend_Translate::_(), apresentado anteriormente. Outro ponto importante é o coringa "%1\$s", que define valor dinâmico à string traduzida. Neste caso o valor definido é uma nova instância de Zend_Date. Essa página irá demonstrar tanto a mudança dos valores de formatação dos componentes locale-aware quanto a apresentação dos textos de acordo com o idioma escolhido.

Uma premissa será definida para ambas as abordagens: um locale só será suportado caso existam as traduções correspondentes. Isso significa que, caso não exista uma determinada tradução para o locale, o mesmo não será configurado, sendo escolhido então o locale padrão (para este caso en_US).

Através do Browser do Usuário

Obter os parâmetros de locale a partir do browser do usuário não é uma tarefa complicada, conforme apresentado no início do artigo. Para essa abordagem o único requisito é a implementação de um Zend_Controller_Plugin para detectar qual o locale do usuário, verificar se existem traduções para o mesmo, e configurar corretamente o Zend_Locale e Zend_Translate, seguindo a premissa definida anteriormente.

O seguinte código define este plugin, devendo estar em library/FernandoMantoan/Plugin/Internationalization.php:

class FernandoMantoan_Plugin_Internationalization extends Zend_Controller_Plugin_Abstract
{
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $locale = Zend_Registry::get('Zend_Locale');
        $translate = Zend_Registry::get('Zend_Translate');
        if (!$translate->isAvailable($locale->getLanguage())) {
            $locale->setLocale("en_US");
            $translate->setLocale("en");
        }
        Zend_Registry::set('Zend_Locale', $locale);
        Zend_Registry::set('Zend_Translate', $translate);
    }
}

Primeiramente os objetos Zend_Translate e Zend_Locale são obtidos do container Zend_Registry, é feita a verificação da tradução para o locale, caso não ela não existe, o locale padrão é definido. Por último, os objetos são atualizados no container Zend_Registry. Para adicionar o plugin no processo de despacho do Zend Framework, os seguintes parâmetros de configuração são necessários no arquivo application/configs/application.ini:

autoloaderNamespaces[] = "FernandoMantoan"
resources.frontController.plugins.internationalization = "FernandoMantoan_Plugin_Internationalization"

Para testar o código é necessário modificar o idioma preferido no browser utilizado. No Mozilla Firefox, por exemplo, esta opção encontra-se em: Editar > Preferências > Idiomas (idioma preferencial). Ao se definir en-US, a página resultante é a seguinte:

Página em Inglês

Mudando para pt-BR o resultado é o seguinte:

Página em Português

Mudando para es-ES o resultado é:

Página em Espanhol

E, caso seja definido um locale não suportado, como fr-FR, o resultado é:

Página em Inglês

Uma abordagem simplória, mas que funciona muito bem.

Através de Rotas

A segunda abordagem é através de rotas. Dentro da gama de componentes MVC do Zend Framework existe a família de componentes Zend_Controller_Router_*, que são utilizados para ler e definir rotas da aplicação. Uma rota é uma URI (Unified Resource Identifier) escrita após a URL base (endereço do servidor ou da aplicação, por exemplo), que é mapeada pelos componentes do Zend Framework para achar o Controller e Action correspondentes, assim como definir parâmetros dinâmicos. Para o exemplo em questão será definido um novo padrão de rotas, composto por: idioma, módulo, controlador, action e parâmetros adicionais. Um exemplo disso seria: http://servidor/en/products/list, que representa a listagem de produtos no idioma inglês.

Para essa configuração, será definido o método _initRoutes na classe application/Bootstrap.php, e o mesmo terá o seguinte conteúdo:

protected function _initRoutes()
{
    $frontController = Zend_Controller_Front::getInstance();
    $router = $frontController->getRouter();
    $router->removeDefaultRoutes();
    $router->addRoute(
        'fullRoute',
        new Zend_Controller_Router_Route('/:lang/:module/:controller/:action',
            array('lang' => ':lang')
        )
    );
    $router->addRoute(
        'languageControllerAction',
        new Zend_Controller_Router_Route('/:lang/:controller/:action',
            array('lang' => ':lang')
        )
    );
    $router->addRoute(
        'language',
        new Zend_Controller_Router_Route('/:lang',
            array('lang' => 'en',
                'module' => 'default',
                'controller' => 'index',
                'action' => 'index'
            )
        )
    );
    $router->addRoute(
        'languageController',
        new Zend_Controller_Router_Route('/:lang/:controller',
            array('lang' => 'en',
                'module' => 'default',
                'controller' => 'index',
                'action' => 'index'
            )
        )
    );
}

A primeira etapa do método é apagar as rotas padrões definidas pelo Zend Framework, através do método removeDefaultRoutes(). As linhas que seguem adicionam rotas para suportar as seguintes URIs:

  • /idioma/modulo/controlador/acao
  • /idioma/controlador/acao
  • /idioma/controlador
  • /idioma

E, define um valor padrão para cada parâmetro, caso algum deles seja omitido:

  • Idioma: en
  • Módulo: default
  • Controlador: IndexController
  • Action: indexAction

Com as rotas configuradas, é necessário criar um Zend_Controller_Plugin, localizado em library/FernandoMantoan/Plugin/LanguageRouteDetector.php, que ficará responsável por ler os parâmetros definidos nas rotas e configurar o Zend_Locale e Zend_Translate adequadamente:

class FernandoMantoan_Plugin_LanguageRouteDetector extends Zend_Controller_Plugin_Abstract
{
    protected $_supportedLanguages = array('pt', 'en', 'es');
    protected $_regions = array('pt' => 'BR', 'en' => 'US', 'es' => 'ES');

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $lang = $request->getParam('lang', '');

        $zendLocale = Zend_Registry::get('Zend_Locale');
        $zendTranslate = Zend_Registry::get('Zend_Translate');

        $locale = '';

        if (!in_array($lang, $this->_supportedLanguages)) {
            $locale = 'en_US';
        } else {
            $locale = $lang . '_' . $this->_regions[$lang];
        }

        $zendLocale->setLocale($locale);
        $zendTranslate->setLocale($locale);

        Zend_Registry::set('Zend_Locale', $zendLocale);
        Zend_Registry::set('Zend_Translate', $zendTranslate);
    }
}

Os dois primeiros arrays definem idiomas e regiões, e serão utilizados para verificar se a rota acessada contém um idioma válido da aplicação. A primeira etapa é verificar se o parâmetro lang definido na rota corresponde a um idioma válido, caso não seja um idioma válido o utilizado será en_US. Caso o idioma seja válido, obtém a região do idioma em questão e constrói um locale para configurar corretamente os objetos Zend_Translate e Zend_Locale. Esse plugin deve ser configurado no application/configs/application.ini, com as seguintes linhas:

autoloaderNamespaces[] = "FernandoMantoan"
resources.frontController.plugins.internationalization = "FernandoMantoan_Plugin_LanguageRouteDetector"

Para testar essa abordagem, é necessário modificar os parâmetros da URL conforme o idioma escolhido. Os websites e aplicações web que utilizam rotas diferentes para cada idioma fornecem links para elas, identificando-as através de bandeiras. Para isso, será modificado o arquivo application/views/scripts/index.phtml para adicionar o seguinte código:

<ul>
    <li><a href="<?php echo $this->url(array('lang' => 'pt')); ?>">pt</a></li>
    <li><a href="<?php echo $this->url(array('lang' => 'en')); ?>">en</a></li>
    <li><a href="<?php echo $this->url(array('lang' => 'es')); ?>">es</a></li>
</ul>

Ao clicar em cada um dos links, os textos e o idioma da página serão atualizados, e é possível notar que a URL apresentada na barra de endereços reflete o idioma escolhido em cada um dos links. A nível de teste, ao acessar "/fr" ou "/it", o idioma é verificado e o padrão “en” é adotado, pelo fato de “fr” e “it” não serem suportados pela aplicação.

Bandeiras

Conclusões

Atualmente o suporte a múltiplos idiomas é um ponto trivial de uma aplicação web, principalmente pela gama de usuários espalhados pelo mundo que podemo ter acesso às diversas aplicações da internet. O Zend Framework provê os componentes necessários para essa tarefa, e fornece a flexibilidade na implementação desse suporte, como demonstrado no artigo. Existem diversas formas de implementar a i18n e l10n, nesse artigo foram apresentadas duas formas simples, se você leitor utiliza outra abordagem, compartilhe nos comentários ou faça fork do repositório e um pull request da sua implementação.

Apesar a da lista de componentes localizáveis ser extensa, foram apresentados mais detalhadamente os componentes Zend_LocaleZend_Translate por serem específicos para a configuração de todo o processo de internacionalização dos demais componentes. Além deste fato, componentes como o Zend_Date, fornecem funcionalidades que vão além da formatação de acordo com a localização do usuário, como, por exemplo, comparação de datas, diferença de datas, e outros recursos. Para maiores informações consulte as referências do artigo.

Código-Fonte

Todo o código-fonte apresentado no artigo encontra-se no repositório zf-i18n-l10n-samples no GitHub.

Referências