Neste artigo nós reunimos as melhores práticas para escrever apps Gaia localizáveis. Se você estiver fazendo o trabalho de desenvolvimento do projeto Gaia, estas práticas devem ser consideradas, sempre que possível. Elas também são úteis para quem cria seus próprios apps para o Firefox OS.
Nota: Se você precisar ler sobre os fundamentos para utilização de qualquer um dos recursos descritos abaixo, o conteúdo no link localização de apps para o Firefox OS fornece um bom guia de referência.
Nota: Existe também um outro documento que você deveria reservar um tempo para a leitura: Boas práticas de localização de conteúdo. Este é mais genérico (não específico para o Firefox OS), e abrange as melhores práticas para tornar as strings de conteúdo o mais localizáveis possíveis, enquanto que o seguinte é mais sobre como implementar aquelas strings em seu código.
Localização de UI
A melhor maneira de escrever código localizável é movendo o máximo de lógica l10n para HTML declaratível possível. Você deve sempre tentar marcar seus Elementos HTML com data-l10n-id
e-data-l10n-args
e apenas definir/remover/atualizar aqueles que usam JavaScript se necessário. Você também não precisa colocar mais o conteúdo original em HTML.
Utilizando a API declarativa
Abaixo está um exemplo de uma UI bem localizada com alguma localização do tipo JavaScript-driven. O código não é robusto, não necessita de guardas, irá trabalhar em qualquer local e reagir adequadamente à mudanças linguísticas. Observe que o HTML não tem qualquer conteúdo em Inglês. O L10n.js usa seu próprio mecanismo de retorno e qualquer conteúdo definido no HTML fonte será substituído de qualquer maneira no tempo de execução.
<h1 data-l10n-id="appName" /> <h2 data-l10n-id="summary" /> <article> <p id="author" /> <button id="actionButton" /> </article>
actionButtonElem.setAttribute('data-l10n-id', newArticle ? 'saveBtnLabel' : 'updateBtnLabel'); navigator.mozL10n.setAttributes(authorElem, 'articleAuthor', { 'name': 'John Smith' });
appName = My App saveBtnLabel = Save updateBtnLabel = Update articleAuthor = The author of this article is {{ name }}
Revertendo a tradução
Se você precisar de parar a tradução/reversão, você precisa remover o l10nId a partir do elemento.
document.getElementById('node').removeAttribute('data-l10n-id');
Uma limitação da abordagem atual é que não temos uma boa maneira de limpar o nó, por isso contamos com usuários removendo manualmente a tradução dos atributos de valor e localizados:
document.getElementById('node').textContent = null; document.getElementById('node').removeAttribute('placeholder');
No futuro, esperamos ser capazes de remover automaticamente a tradução de valor e os atributos quando o l10nId é desativado.
Não defina l10n-id em elementos com elementos filho
Uma das considerações importantes é que quando você definir a l10n-id em um elemento, o L10n.js assume a renderização deste elemento, de maneira que todos os nós filho serão sobrescritos em strings localizadas. No futuro, poderemos até ter algo semelhante à Shadow DOM, para renderizar as versões localizadas de um elemento.
Caso você precise localizar o Fragmento DOM, veja como fazer abaixo.
Não utilize o mozL10n.get
Um dos anti-padrões habitualmente utilizados de antigos paradigmas é um método síncrono utilizado para recuperar strings l10n de um pacote. Recomendamos evitar a utilização de mozL10n.get uma vez que este é síncrono e exige que você proteja o seu código com mozL10n.once ou mozL10n.ready; ele também não funciona com re-tradução. No futuro, uma vez que resolvermos os casos descritos na seção seguinte, planejamos remover este método completamente.
Ao invés de escrever isto:
// BAD var elem1 = document.createElement('p'); elem1.textContent = navigator.mozL10n.get('helloMsg'); var elem2 = document.createElement('input'); elem2.placeholder = navigator.mozL10n.get('msgPlaceholder'); var elem3 = document.createElement('button'); elem3.ariaLabel = navigator.mozL10n.get('volumeLabel'); // .properties helloMsg = Hello World msgPlaceholder = Enter password volumeLabel = Switch volume
Use isto:
// GOOD var elem1 = document.createElement('p'); elem1.setAttribute('data-l10n-id', 'helloMsg'); var elem2 = document.createElement('input'); elem2.setAttribute('data-l10n-id', 'passwordInput'); var elem3 = document.createElement('button'); elem3.setAttribute('data-l10n-id', 'volumeButton'); // .properties helloMsg = Hello World passwordInput.placeholder = Enter password volumeButton.ariaLabel = Switch volume
Em casos raros quando você não pode utilizar o setAttribut nem o mozL10n.setAttributes, você pode utilizar o método assíncrono não-robusto L10n.formatValue()
.
mozL10n.ready irá causar vazamentos de memória
Quando você fornece uma função de chamada de retorno para mozL10n.ready (), a função não teráo lixo coletado devidamente, se não for explicitamente removido. Mais importante, se é um método que está vinculado a um objeto, a instância do objeto não irá embora. Consulte bug 1135256 para obter mais detalhes. Por exemplo, se você tem um objeto JavaScript representando um item widget da lista e anexar um método como um callback com mozL10n.ready (), o objeto JavaScript nunca terá o lixo coletado. Um padrão que poderia ajudar a mitigar esse problema, é ter um ouvinte singleton em seu script que lida com mudanças de localidade para todas as instâncias de objetos com uma WeakMap, aqui está um exemplo parcial:
var instances = new WeakMap(); function MyThing(container) { // used for finding this instance in the DOM container.classList.add('my-thing'); instances.set(container, this); if (navigator.mozL10n.readyState === 'complete') { this.localize(); } } MyThing.prototype.localize = function() { /* ... */ }; navigator.mozL10n.ready(function() { // Look for all the containers of this object type, and retrieve instances. for (var container of document.querySelectorAll('.my-thing')) { var obj = instances.get(container); if (obj) obj.localize(); } });
Exceções quando a utilização de mozL10n.get é justificada
Existem três casos nos quais a utilização do mozL10n.get é neste momento necessária:
Exceção1: Passando strings fora do app
Quando o seu código precisa de enviar uma string para uma API externa — por exemplo alert()
, confirm()
, ou para navigator.mozMobileMessage
— é melhor resolver a string o mais tarde possível. Então ao invés de escrever:
// BAD function sendText(msg) { navigator.mozMobileMessage.send(number, msg); } sendText(navigator.mozL10n.get('confirmationMessage'));
Escreva isto:
// GOOD function sendText(l10nId) { var msg = navigator.mozL10n.get(l10nId); navigator.mozMobileMessage.send(number, msg); } sendText('confirmationMessage');
Isto tornará a sua API mais garantida no futuro, extensível e mais fácil de depurar (ver a seção Escrevendo APIs que operam em L10nIDs deste artigo).
Exceção 2: Localizando Fragmentos DOM
Uma área que ainda não é coberta por programação declarativa é a localização do Fragmento DOM. Imagine que você tenha um código como este:
<p>See our <a href="https://www.mozilla.org">website</a> for more information!</p>
A melhor maneira de tornar este código localizável é criar uma string l10n como esta:
.properties: seeLink = See our <a href="{{ url }}">website</a> for more information! html: <p data-l10n-id="seeLink" data-l10n-args='{"url": "https://www.mozilla.org"}'></p>
Isto irá funcionar, desde que você não tenha mais a marcação no fragmento. Considere este exemplo:
<p>See our <a href="https://www.mozilla.org" class="external big" id="foo">website</a> for more information!</p>
Neste exemplo, movendo-se atributos como class
ou id
para um l10n string não é o ideal, porque qualquer mudança para esta marcação vai exigir uma atualização para a string, em todas as traduções. Dividir a string em várias partes e concatená-las em JavaScript é um erro porque localizações podem ter que manipular a ordem dos elementos no Fragmento DOM.
No futuro o L10n.js irá fornecer uma estratégia de interpolação chamada sobreposições de DOM que fundem Fragmentos DOM fornecidos pelo desenvolvedor com suas traduções. Até que isso esteja disponível, precisamos de abrir mão de utilizar o mozL10n.get.
seeLink = See our {{ link }} for more information! linkText = website
<p data-l10n-id="seeLink"></p>
navigator.mozL10n.setAttributes(elem, 'seeLink', { 'link': '<a href="https://www.mozilla.org" class="external big" id="foo">' + navigator.mozL10n.get('linkText') + '</a>' });
O fato deste código usar mozL10n.get
tem que ser garantido para ser executado após os recursos l10n forem carregados, por isso use um invólucro mozL10n.once
sobre este código.
Exceção 3: Data / Formatação do tempo
Para Data/Formatação do tempo nós utilizamos atualmente um padrão como este:
var currentDate = new Date(); var f = new navigator.mozL10n.DateTimeFormat(); var format = navigator.mozL10n.get('shortTimeFormat'); elem.textContent = f.localeFormat(currentDate, format);
Mais uma vez, pelo fato de ele utilizar mozL10n.get
, certifique-se de proteger o seu código com o mozL10n.once
para evitar condições de corrida.
No futuro pretendemos expor data e hora formatados como macros. Isto permitirá que localizadores decidam como formatar datas em string l10n e remover este caso de uso juntamente.
Como escrever código que opera em strings fornecidas pelo usuário ou l10nIDs
Neste exemplo, podemos ter um título da canção que não é localizável, ou uma string de "Unknown Track" que vem de nossos recursos de localização.
Existem dois padrões para abordar esta questão:
Padrão 1:
<h1 id="titleElement />
function updateScreen(track) { var titleElement = document.getElementById('titleElement'); if (track.title) { navigator.mozL10n.setAttributes(titleElement, 'trackTitle', { 'title': track.title }); } else { navigator.mozL10n.setAttributes(titleElement, 'trackTitleUnknown'); } }
trackTitle = {{ title }} trackTitleUnknown = Unknown Track
Padrão 2:
<h1 id="titleElement />
function updateScreen(track) { var titleElement = document.getElementById('titleElement'); if (track.title) { titleElement.removeAttribute('data-l10n-id'); titleElement.textContent = track.title; } else { titleElement.setAttribute('data-l10n-id', 'trackTitleUnknown'); } }
trackTitleUnknown = Unknown Track
Essas abordagens são semelhantes, mas, no futuro, o Padrão 1 poderá se tornar o padrão à medida que avançamos em direção a árvores HTML totalmente localizáveis.
Em ambos os padrões, note que nós não definimos o l10n-id
ou l10n-args
em HTML porque nós só iremos definir o valor quando nós carregarmos primeiro a faixa em JavaScript. Então definindo isto em HTML é um desperdício de recursos (l10n.js tentará traduzí-lo caso você o adicione lá).
Também é importante notar que atualmente não fazemos nenhuma limpeza mágica quando você remove estes atributos, então você precisa limpá-los você mesmo. Isso deverá mudar no futuro.
Escrevendo código que itera sobre várias strings l10n
Se você tiver várias strings (por exemplo, códigos de erro), pode ser tentador utilizar mozL10n.get
para testar se houver uma tradução para a cadeia e se não for definida alguma resposta genérica. Isso não é um bom padrão, porque primeiro ele usa mozL10n.get
, e segundo, ele confunde strings em falta com as strings que não deveriam estar lá, criando bugs e casos extremos difíceis de reproduzir.
Em vez disso, você deve criar uma lista de strings localizadas e testá-la assim:
var l10nCodes = [ 'ERROR_MISSING', 'ERROR_UNKNOWN', 'ERROR_TIMEOUT' ]; if (l10nCodes.indexOf(code) !== -1) { elem.setAttribute('data-l10n-id', code); } else { elem.setAttribute('data-l10n-id', 'ERROR_UNKNOWN'); }
Escrevendo APIs que operam em L10nIDs
Um dos casos mais interessantes para lidar é quando você escreve uma API que supostamente recebe L10nIDs. No caso mais simples, ela se assemelha a essa:
function updateTitle(titleL10nId) { document.getElementById('titleElement').setAttribute('data-l10n-id', titleL10nId); }
mas se você tem um caso onde também pode precisar de l10nArgs e/ou strings de caracteres simples para casos como o exemplo acima, ou mesmo fragmentos de HTML para injetar (no futuro isso será substituído por Sobreposições DOM), o padrão completo que recomendamos é este:
// titleL10n may be: // a string -> l10nId // an object -> {id: l10nId, args: l10nArgs} // an object -> {raw: string} // an object -> {html: string} function updateTitle(titleL10n) { if (typeof(titleL10n) === 'string') { elem.textContent = ''; // not needed if you're not adding line 25-29 [1] elem.setAttribute('data-l10n-id', titleL10n); return; } if (titleL10n.id) { elem.textContent = ''; // not needed if you're not adding line 25-29 [1] navigator.mozL10n.setAttributes(elem, titleL10n.id, titleL10n.args); return; } if (titleL10n.raw) { elem.removeAttribute('data-l10n-id'); elem.textContent = titleL10n.raw; return; } if (titleL10n.html) { elem.removeAttribute('data-l10n-id'); elem.innerHTML = titleL10n.html; return; } }
[1] Se o seu código suporta fragmentos de HTML e um fluxo em que primeiro o nó pode receber {html: string}
e, posteriormente, deve ser transferido para tradução baseada em l10nId, você precisa limpar o textContent. Caso contrário L10n.js vai reclamar sobre a configuração l10nId em um nó com nós filhos. Consulte a seção "Untranslation" neste artigo para saber mais a fundo.
Claro que você não precisa de fornecer suporte a casos que você não precise. O caso HTML raramente é necessário, e o l10nArgs ou cru pode não ser necessário também. Mas seguindo este esquema para parâmetros l10n lhe permite fazê-lo funcionar e, posteriormente, estender os casos apoiados sem alterar a API.
Uma das conseqüências de nossas limitações atuais sobre desativação dp l10nId é que não temos uma maneira limpa para limpar o fragmento DOM a partir da tradução de idade. Se o API tem potencial para ter
Testando
Ao escrever testes, não recomendamos testar os valores dos nós. Isto torna o teste inútil em outros locais, não funcionando com tradução assíncrona, e no futuro não funcionando se alterarmos a forma como apresentamos a localização dos elementos DOM.
Gaia fornece um mock_l10n compartilhado que você poderá utilizar.
A melhor estratégia é isolar o seu teste para se certificar de que ele defina de acordo os atributos l10n-id
e l10n-args
:
assert.equal(elem.getAttribute('data-l10n-id'), 'myExpectedId');
ou:
var l10nAttrs = navigator.mozL10n.getAttributes(elem); assert.equal(l10nAttrs.id, 'myExpectedId'); assert.deepEqual(l10nAttrs.args, {'name': 'John'});
Notificação de API
Então, você deseja enviar uma notificação. A API W3C espera que você passe o título e o corpo como uma string. Gaia oferece-lhe uma NotificationHelper que funciona com o mozL10n.
Ao invés de:
var title = navigator.mozL10n.get('notification_title'); var body = navigator.mozL10n.get('notification_body', {user: "John"}); var notification = new Notification(title, { body: body, }); // set onclick handler for the notification notification.onclick = myCallback;
você deve escrever:
NotificationHelper.send('notification_title', { bodyL10n: {id: 'notification_body', args: {user: "John"}} }).then(function(notification) { notification.addEventListener('click', myCallback); });
O NotificationHelper lida com título e bodyL10n da mesma maneira como descrito em "Escrevendo APIs que operam em L10nIDs" deste artigo, para que possa passar uma string ou um objeto.