Internacionalização de ficheiros de JavaScript
August 29th, 2010Os últimos posts no meu blog têm sido acerca do tema da internacionalização de aplicações, nomeadamente:
- ASP.Net – Método alternativo de internacionalização (i18n)
- Localizar Enumerações
- Adicionar novas línguas ao Windows (edições Home)
O post de hoje contínua o sobre o método alternativo com ASP.NET, recorrendo a ao GetText do GNU. Como vimos, a aplicação do método _() para substituir texto localizável tornava-se um recurso simples e eficiente na internacionalização de ASP.NET. Em especial, é um método adoptado pela restantes linguagens e evita o esforço extra necessário para introduzir o texto em ficheiros .resx.
Procurei uma solução semelhante para Javascript, uma vez que a aplicação que estou a internacionalizar recorre muito a funcionalidade em javascript. Em especial, há mensagens de erro específicos e mensagens de confirmação contextualizadas. Especifico a ASP.Net, não encontrei nada de útil. Até achei muito estranho não detectar nada de muito útil nesse campo.
De qualquer forma, há alguns métodos genéricos a aplicar, incluíndo plugins em jQuery (http://plugins.jquery.com/project/gettext), mas gostei da descrição dada em 24ways.org. Simplesmente introduzi a função para _() no meu ficheiro core de javascript (é uma pequena biblioteca e funções e abstracções comuns a todas as páginas das minhas aplicações, e incluído em todas as páginas):
function _(s) { if (typeof(i18n)!='undefined' && i18n[s]) { return i18n[s]; } return s; }
Com isto é esperado existir uma variável i18n definido com a lista de traduções em pares chave-valor em JSON, do tipo:
var i18n = { "" : "", "Hello" : "Olá", "Goodbye" : "Adeus", //(...) }
O método em 24ways.oprg refere ainda funções para formatar strings que podem ser uteis. Eu no entanto já tinha uma implementação de string.format para javascript (http://www.geekdaily.net/2007/06/21/cs-stringformat-for-javascript/).
Portanto, com a função no core, e a variável i18n adicionada, nos diversos ficheiros de javascript, onde fosse usado uma string, este deveria ser englobado por _():
if (confirm(_('Confirma a alteração de estado nos processos seleccionados?') )) { //... }
Assim, quando executado, e porque o método _() está sempre presente, o texto será substituído pelo existente no dicionário ou se não encontrado, o próprio texto usado como argumento da função.
Resta apenas dois passos – gerar os ficheiros .po, que deve seguir um esquema semelhante ao mencionado no artigo de ASP.Net, usando instruções póst-build e o gettext, e depois transformar os registos dos ficheiros .po em json. O primeiro dos passos é relativamente directa, apenas necessitando de construir uma nova lista de ficheiros (apenas de .js) e adicionar as entradas envolvidas em _() no .pot.
O segundo passo é mais “dificil”. Não há nada para o fazer directamentem senão um script em perl (http://jsgettext.berlios.de/doc/html/po2json.html), e como não me servia, decidi construir um script T4 simples, para usar no processo.
O script assume alguns pressupostos, pelo que, se utilizar, deve de os ter em conta e alterar consoante o necessário. O primeiro é que os ficheiros resultantes, quer os .po, quer os .js vão estar cada um numa pasta dedicada à língua específica como por exemplo:
/Scripts/locale/xx/messages.po
/Scripts/locale/xx/i18n.js
Também, considero que o ficheiro .po será gerado e unido com recurso a msgmerge, e portanto apenas devo transformar os ficheiros .po em ficheiros .js
Assim, o po2json.tt, colocado na pasta (/Scripts/locale/) pode ser executado após o build. Ele procurará o ficheiro .po de cada língua no array, e transformarará em um ficheiro i18n.js. Eu decidi definir as linguas manualmente, mas com pequenas alterações, podem ser detectadas automáticamente.
O T4 é o seguinte:
<#@ Template Language="C#v3.5" Hostspecific="True" #> <#@ Output Extension=".js" #> <#@ Import Namespace="System.IO" #> <#@ Import Namespace="System.Collections.Generic" #> <#@ Import Namespace="System.Text.RegularExpressions" #> <#@ Include File="MultiOutput.t4" #> var i18n = { <# string localeFolderPath = @"D:\Codigo\ProjectoWeb\Scripts\locale\"; string[] suportedLang = {"pt"}; string pofileName = "messages.po"; string msgid = "msgid"; string msgstr = "msgstr"; string txtREGEX = "\"(.*)\""; foreach (string lang in suportedLang) { string[] lines = File.ReadAllLines(localeFolderPath + "\\" + lang + "\\" + pofileName); List<KeyValuePair<string, string>> entries = new List<KeyValuePair<string, string>>(); int currentLine = 0; while (currentLine < lines.Length) { string line = lines[currentLine]; if (line.StartsWith(msgid)) { string keyString = ""; keyString += Regex.Match(line, txtREGEX).Groups[1].Value; //iterate while still a msgid line = lines[++currentLine]; while(line.StartsWith("\"")) { keyString += Regex.Match(line, txtREGEX).Groups[1].Value; line = lines[++currentLine]; } if (line.StartsWith(msgstr)) { string valueString = ""; valueString += Regex.Match(line, txtREGEX).Groups[1].Value; if (++currentLine < lines.Length) { line = lines[currentLine]; while (line.StartsWith("\"")) { valueString += Regex.Match(line, txtREGEX).Groups[1].Value; line = lines[++currentLine]; } } //add pair to entries list entries.Add(new KeyValuePair<string, string>(keyString, valueString)); } } currentLine++; } //build outoput for( int i = 0; i< entries.Count; i++) { KeyValuePair<string, string> entry = entries[i]; #> "<#= entry.Key #>" : "<#= entry.Value #>"<#= i<entries.Count-1 ? "," : "" #> <# } SaveOutput(localeFolderPath + "\\" + lang + "\\" + "i18n.js"); } #> };
Procura cada “msgid” e processa as línguas seguintes convenientemente. Se houver erros, o processo será abandonado. O tt recorre ainda a um MultiOutput.t4, para gerar mais que um ficheiro a partir do .tt, convenientemente localizado. Não necessita de estar incluído no projecto (para não obrigar a incluir as dependências de TextTempleting).
Para incluir o ficheiro na lista de scripts a puxar, basta regista-lo no ScriptManager, no Page_Load da MasterPage :
ScriptManager.RegisterClientScriptInclude(this, typeof(string), "i18n", Page.ResolveUrl("~/Scripts/locale/" + lang + "/i18n.js"));
onde a variável lang é uma string com as letras da língua, para identificar a pasta onde será armazenado. Eu obtenho a a língua para a aplicação (transversal), mas outra possibilidade seria definir a língua pelas preferências do utilizador. Também, o caminho poderia ser proveniente de uma chave de configuração no web.config, se necessário.
Creio que está é uma solução simples, que pode, se necessário ser melhorado com mais funções (especialmente de formatações e plural / singular) como descrito no artigo do 24ways.org. Para já, esta é me suficiente.
