Localizando um widget incorporado: idioma da página ou do navegador?

widget

Uma visitante de uma loja online espanhola encontra um botão de finalizar compra que não faz nada. Ela clica no pequeno ícone de bug no canto e recebe «What's on your mind?» e um botão que diz «Send». Ela consegue entender. Mas escreve três palavras em vez de três frases, porque descrever um problema numa segunda língua dá trabalho, e ninguém deve esse trabalho a você.

Até esta semana, era assim em todo site não anglófono que usava o BugDump. O widget falava inglês, e traduzi-lo significava passar você mesmo todas as 36 strings. A maioria das pessoas não fazia isso. Agora ele vem em inglês, russo, espanhol, francês e português, e escolhe um idioma sozinho. A escolha acabou sendo a parte interessante.

Atualização, 3 de outubro: o alemão chegou no SDK 0.0.42 como o sexto idioma. Ele também precisou de palavras mais curtas: o botão de anexar diz «Datei» (arquivo) e o selo de nota de voz, «Audio». A entrada do registro de alterações traz os detalhes.

O widget de relatório de bug do BugDump em francês, espanhol, português e russo

Três lugares onde um widget pode descobrir o idioma

Um widget incorporado roda na página de outra pessoa. Ele pode ler três sinais, e eles nem sempre concordam.

O atributo lang da página. <html lang="es"> é o site dizendo em que idioma está o conteúdo dele. Leitores de tela o usam para escolher a pronúncia, e os navegadores olham para ele quando oferecem traduzir uma página. Ele descreve a página, não a pessoa que a lê.

A lista de idiomas do navegador. navigator.languages são as preferências do visitante em ordem, algo como ["pt-BR", "pt", "en"]. navigator.language é só o primeiro item. Ela descreve a pessoa, não a página.

O que o site diz ao widget. Uma configuração explícita no código de incorporação. O mais confiável dos três, e o que quase ninguém preenche.

Existe também o cabeçalho Accept-Language, que um servidor vê em toda requisição. Um widget carregado como script a partir de uma CDN nunca consegue lê-lo, então ele fica de fora.

Por que a página vem primeiro

Widgets de chat de suporte costumam seguir o navegador, e, numa conversa com uma pessoa, isso faz sentido: você quer falar na sua própria língua. Um widget de relatório de bug é outra coisa. Ele fica dentro da interface de outra pessoa, ao lado dos botões e dos textos dela, e trata do produto dela. Um painel em francês numa página de checkout em inglês parece, por si só, um bug.

Por isso a ordem que escolhi é:

  1. <html lang>, se ele indicar um idioma que o widget traz.

  2. Senão, o primeiro dos idiomas do navegador do visitante que o widget traz.

  3. Senão, inglês.

A página vem primeiro para que um site em inglês continue em inglês, mesmo para um visitante com o navegador em russo. O navegador vem em segundo para que uma página sem nenhum atributo lang ainda receba algo razoável.

O furo óbvio é uma página com o lang errado. Muitos templates iniciais vêm com <html lang="en"> e ninguém nunca mexe nisso, então um site escrito inteiro em português pode continuar dizendo que é inglês. Decidi que a correção para isso é o atributo, não um palpite mais esperto. Um lang errado já prejudica quem usa leitor de tela naquele site. Um widget que contorna o erro em silêncio esconde o problema em vez de expô-lo.

Comparar tags sem esperteza

As tags de idioma aparecem em muitos formatos: fr, fr-CA, FR, ou pt_BR vindo de um backend que nunca ouviu falar de BCP 47. O widget traz um conjunto de strings por idioma, então a comparação olha só para a parte antes do primeiro - ou _, em minúsculas:

JavaScript
function findLocale(tag) {
  const base = tag.toLowerCase().split(/[-_]/)[0];
  return SHIPPED_LOCALES.find((locale) => locale === base);
}

Isso significa que um visitante em Portugal recebe o português do Brasil. Não é perfeito, e prefiro dizer isso a fingir que é.

A busca na lista é proposital. base in LOCALES parece quase a mesma coisa, até chegar uma tag como constructor e casar com uma propriedade que todo objeto JavaScript herda. Ninguém define lang="constructor" de propósito, mas, do ponto de vista de um widget, um atributo lang é entrada vinda de um estranho.

O que quebrou no caminho

Um teste que dependia de qual laptop o rodava. As versões recentes do Node têm um navigator global, e o languages dele segue o locale da máquina. Os testes de configuração rodam sem DOM, então a primeira versão teria resolvido «auto» para russo numa máquina configurada em russo e para inglês no CI. Agora a detecção não retorna nada a menos que também exista um document, que é como um ambiente fora do navegador deveria se parecer de qualquer forma.

O russo é longo. «Прикрепить» (anexar) tem dez letras onde o inglês tem seis, e empurrou o terceiro botão da barra de ações para uma segunda linha. A correção não foi CSS, foi uma palavra mais curta: o botão em russo agora diz «Файл» (arquivo) e deixa o ícone de clipe fazer o resto. O selo de nota de voz nos anexos tinha o mesmo problema. É um bloco de 72 pixels, e «Nota de voz» termina em reticências ali, então espanhol e francês agora dizem «Audio», e português, «Áudio».

Você só encontra essas coisas olhando. Renderizei o painel em todos os idiomas antes de lançar, e todos, menos o inglês, precisaram de pelo menos uma palavra mais curta.

Strings coladas no código. «Uploading 1/3… 40%» era montada com a palavra embutida, assim como «Microphone 2» e «Powered by Bugdump». Em vez de inventar uma sintaxe de template, a parte traduzível virou só a palavra, e o código acrescenta os números e o nome depois dela. Isso funciona porque todos os idiomas que o widget traz põem a palavra primeiro. O japonês não poria, e, no dia em que eu o adicionar, essas três strings viram templates.

Leitores de tela. O widget é renderizado num shadow root na página hospedeira, então herda o lang da página. Um painel em francês numa página em inglês seria lido em voz alta com pronúncia inglesa. Agora o widget define lang no próprio elemento.

Quem decide ainda é o site

A detecção automática é um padrão, não uma regra. Um site pode indicar o idioma no código de incorporação:

HTML
<script
  src="https://bugdump.com/sdk/latest.js"
  async
  data-api-key="your-api-key"
  data-locale="fr"
></script>

translations continua substituindo qualquer string por cima do idioma escolhido, então renomear «Send feedback» para «Report a bug» funciona igual em francês e em inglês. Para um idioma que o widget não traz, locale: 'nl' mais as suas próprias strings em holandês também funciona, e tudo o que você deixar de fora cai no inglês.

Na página Widget do BugDump, a mesma escolha é uma lista suspensa, e a prévia ao vivo troca de idioma conforme você escolhe:

A configuração Idioma na página Widget do BugDump, aberta em Automático, English, Русский, Español, Français, Português e Deutsch

Quanto custou

Os cinco idiomas vão empacotados no script. Os quatro extras acrescentaram cerca de 2,5 KB com gzip, e o widget agora pesa cerca de 32 KB. Pensei em carregá-los sob demanda, mas isso significa mais uma requisição e um painel que só pode ser renderizado depois que ela volta. Um estado de carregamento para economizar dois kilobytes e meio é uma troca ruim.

O que não fiz: trocar o idioma de um widget que já está na tela e layouts da direita para a esquerda. Nenhum dos idiomas que o widget traz é escrito da direita para a esquerda, e um app de página única que troca de idioma pode desmontar o widget e iniciá-lo de novo até alguém precisar de mais.

Se você está construindo um widget

  • Leia <html lang> antes de navigator.languages. O seu widget faz parte da página.

  • Compare pelo idioma base e declare o idioma que você realmente usou com um atributo lang próprio.

  • Deixe o site sobrescrever tudo, e faça essa sobrescrita caber num atributo da tag script.

  • Renderize cada string em cada idioma antes de lançar. As traduções são mais longas do que você espera.

O widget do BugDump faz tudo isso a partir do SDK 0.0.41, e quem usa a tag script já tem. Traduzir e mudar a marca do widget cobre todas as opções, e a entrada do registro de alterações traz a versão curta.

Leia também