in Code & IT

ASP.Net – Método alternativo de internacionalização (i18n)

A internacionalização de aplicações (internationalization – i18n) para posterior localização (localization – l10n) é um processo multi-passo, que envolve a detecção de texto que deve ser apresentado em múltiplas línguas, bem como a preparação da base de código para que o texto possa ser correctamente traduzido.

Cada tecnologia tem a sua própria forma de implementar mecanismos que auxiliam a tradução. .Net recorre a recursos compilados na forma de ficheiros .resx . Os ficheiros .resx são de uma forma geral pares chave-valor, estruturados em XML, que são compilados para dlls carregados no inicio da aplicação.

Apesar de uma solução interessante, é extremamente tediante usa-los em ASP.Net. Com as bibliotecas de uma API, ok. O suporte a nível de código C# (ou VB ) torna o uso relativamente simples. Já com ASP.Net, não é possível dizer o mesmo. As razões estão bem descritas em “Why Internationalization is hopelessly Broken in ASP.Net“, mas resumindo, tudo tem de ser controlos ASP.Net com o runat=”server”, não há suporte simplificado a texto estático (escrito directamente na página), a referência de chaves das strings obriga a inserir meta:resoursekey=”” em todas os controlos, e os ficheiros .resx não são facilmente legíveis por pessoal não técnico.

A alternativa? Seguir o método do “resto do mundo” e usar ficheiros de strings em formatos simplificados, nomeadamente os ficheiros .po. Quem já explorou um projecto open-source ou projectos aplicacionais com add-ins linguísticos, muito provavelmente já se cruzaram com este formato de ficheiros. São ficheiros de texto simples, com texto assoiado a uma chave (msgid) e texto associado à tradução (msgstr). Por exemplo:

msgid "Pesquisar Candidatos"
msgstr "Search Candidates"

De notar neste exemplo que a chave está em PT e a tradução em EN. Não é recomendado esta abordagem, uma vez que as línguas latinas tem acentos que não são óptimos para chaves.

Continuando, o uso de ficheiros .PO, noutras frameworks, é suportado por por uma função gettext(), onde é passo a chave (geralmente o próprio texto, na língua base) e retorna a versão traduzida, conforme as preferências de utilizador. Melhor, há geralmente uma forma simplificada da função escrito _(). Assim, cada string a analisar chama a função gettext(), obtendo a versão traduzida do texto. Bem mais simples que os .resx e meta:resourcekey do .NET.

Usando Fairlylocal

O FailyLocal é um projecto opensource iniciado por Jason Kester, e que implementa a funcionalidade do gettext e ficheiros PO para ASP.Net. Como descrito no link do projecto, para pedir a tradução, na página basta utilizar:

<%= _("texto a traduzir") %>

ou no codebehind:

controlo.InnerHtml = _("texto a traduzir");

Tão simples e eficaz. Em termos de biblioteca, basta adicionar os ficheiros ao projecto da aplicação (têm de ser um projecto de aplicação web, em vez de um projecto de website, uma vez que o segundo não permite evento post-build, que o primeiro permite, e que constrói dinamicamente os ficheiros PO).

Em termos de execução, os ficheiros PO são carregados no arranque da aplicação, para um dicionário estático em memória, onde depois são realizadas as consultas do gettext(). Neste caso, todas as paginas devem herdar de InternationalPage ou InternationalMaster (ou a página base da aplicação derivar destas) em vez de Page directamente, para que _() fique disponível.

Para as minhas necessidades, efectuei pequenas alterações. Primeiro, para facilitar o uso em mutlipos projectos, adicionai os ficheiros ao projecto da minha framework, para que tenha sempre disponível. Segundo, porque necessito de aceder aos métodos, quer para a página instanciada, quer quando estou a usar WebMethods, tornei a propriedade LanguageCode e o método _() static. Finalmente, porque necessito de definir a língua ao nível da aplicação e não para cada utilizador, modifiquei o getter de LanguageCode para obter o código de lingua directamente da thread, em vez da sessão do utilizador:

public static string LanguageCode
{
   get
   {
       if (_languageCode == null)
       {
           _languageCode = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName.ToLower();
	}
	return _languageCode;
    }
    set
    {
	_languageCode = value;
    }
}

Incluíndo os scripts post-build, os ficheiros .PO são actualizados após cada build, podendo ser traduzidos de seguida, ou directamente ou recorrendo a programas como o Poedit. Ferramentas como o Pepipopum permitem traduzir automaticamente (via Google translate API) o ficheiro PO. As chaves devem estar em inglês para funcionar correctamente.

Finalmente, convém referir que o sistema tem fallback automático para o texto da própria chave – se o testo não estiver traduzido no ficheiro .po, o texto a usar é o próprio argumento do _(), o que ajuda muito se tiver a internacionalizar uma aplicação já existente.

Uma macro util

Ainda assim, com esta simplificação, internacionalizar uma aplicação existente não deixa de ser um trabalho tediante. Localizar o texto a traduzir e envolver no método _() é “chato”.

Para ajudar, criei uma macro:

Public Module GetTextSurrond

    '' surround text with <%= _(" and ") $>
    ''
    Sub ASPXSurround()
        Dim textSelection As EnvDTE.TextSelection

        textSelection = DTE.ActiveDocument.Selection()
        textSelection.Text = "<%= _(""" + textSelection.Text + """) " ''%>"
    End Sub

End Module

Associando esta Macro a um atalho de teclas, por exemplo Ctrl+G, Ctrl+T, ajudará a usar a macro, bastando seleccionar o texto e executar o comando (nos ficheiros .aspx).

Conclusão

Pela curta experiência que tenho com esta alternativa de tradução, não deixa de me parecer N vezes mais vantajoso que o uso de ficheiros .resx no ASP.Net. Os ficheiros .PO são apenas strings, portanto para recursos binários outra estratégia deve ser adoptada.

Write a Comment

Comment

 

WP-SpamFree by Pole Position Marketing