Internacionalizando seu produto sem perder a qualidade de sua aplicação

Publicado por Diogo Busanello no dia dev

Nos último meses, o nosso Capybot andou aprendendo novos idiomas e, muito em breve, o nosso produto será poliglota. Nesse primeiro post, compartilharei um pouco da minha visão de qualidade da infra de tradução e alguns macetes do Rails que aprendi durante o processo de internacionalizar o RD Station.

RD Station will conquer the world

Caso você não esteja familiarizado como funciona a internacionalização(i18n para os mais íntimos ;) ) em Rails, tenha em mente que Rails suporta nativamente aplicações com mais de um idioma, evitando a utilização de strings hardcoded em seu código.

1
2
3
4
5
#Exemplo de como chamar a gema de tradução
I18n.translate('about.capybot')
I18n.t('about.capybot')
#ou
t('about.capybot')

O valor dessa tradução estará um em arquivo YAML na pasta /config/locales. No exemplo anterior, o valor daquela tradução estaria mapeada da seguinte forma num arquivo YAML.

1
2
3
en:
  about:
    capybot: 'Capybot Rocks'

A organização de arquivo .yml é semelhante ao de uma árvore k-ary. Onde a raiz detêm o idioma das traduções e os nós subsequentes são separados por . na chamada de tradução - sendo que apenas o nó folha possui o valor da tradução.

Organizando seus arquivos YAML

Em Rails, utilizamos arquivos para armazenar as traduções da nossa aplicação. Pense nos seus arquivos de tradução como uma grande biblioteca. Se não existir um sistema de catalogação dos livros, você poderá passar dias procurando por aquela edição especial de Silmarillion até eventualmente achá-la.

Samwell Tarly buscando a edição do Silmarillion

Samwell Tarly buscando a edição do Silmarillion é você buscando onde inserir sua nova tradução

Se o conjunto de arquivos de tradução são a biblioteca, pense que cada livro funciona como um dicionário. Prefira organizar suas chaves dentro dos .yml alfabeticamente. Sempre que achar que duas mensagens deveriam estar juntas, e a estrutura alfabética os afastou, crie uma nova chave e coloque-os como filhos dessa nova chave.

1
2
3
4
5
6
7
8
#chaves que deveriam estar próximas, separadas pela organização alfabética
...
lp_tour:
  description: 'Crie sua primeira Landing Page e comece a gerar Leads agora!'
  .
  .
  .
  title: 'Agora é a sua vez!'
1
2
3
4
5
6
#Adicione um novo nível ao seu chaveamento
...
lp_tour:
  congratulation:
    description: 'Crie sua primeira Landing Page e comece a gerar Leads agora!'
    title: 'Agora é a sua vez!'

Uma abordagem inicial para armazenar as traduções é criar arquivos baseados nos idiomas suportados na aplicação (en.yml, es.yml, pt.yml). Contudo, essa abordagem não escala com o crescimento de seu produto e logo você estará perdido dentro de seus arquivos YAML, procurando o lugar correto para inserir uma nova chave.

Um segundo nível de organização, é criar arquivos que reflitam as funcionalidades de sua aplicação. Funcionalidades menores ficam com um tamanho - em quantidade de linhas - ideal. Porém, funcionalidades grandes e cheias de views logo significarão uma quantidade enorme de chaves nos arquivos de tradução da funcionalidade. E se você não for metódico com a estrutura de chaves de seus arquivos .yml, logo você estará um nível organizacional equivalente ao do primeiro nível organizacional. Na próxima sessão veremos como organizar suas chaves para que suas chamadas na view não fiquem gigantesca.

Atualmente, na Resultados Digitais, encontramo-nos no segundo nível. Esse nível foi alcançado após o esforço coletivo de um time para mapear e chavear os arquivos que continham strings hardcoded.

Porém, desde que fechamos o mapeamento, vejo que há espaço para melhorar a estrutura de arquivos de tradução. Ao montar esse post me deparei com o post do Peter, que serviu como base para que eu pudesse definir o que seria um terceiro nível de estruturação.

O terceiro nível consiste em ir além de criar arquivos que reflitam o primeiro nível de pastas de sua view. Pense em como Rails cria arquivos quando rodamos um scaffold:

1
2
3
4
5
6
7
8
rails g scaffold landing_pages/page
    create    app/models/landing_pages/page.rb
    create    app/controllers/landing_pages/pages_controller.rb
    create    app/views/landing_pages/pages
    create    app/views/landing_pages/pages/index.html.erb
    create    app/views/landing_pages/pages/edit.html.erb
    create    app/views/landing_pages/pages/show.html.erb
    create    app/views/landing_pages/pages/new.html.erb

A estrutura ideal de organização é aquela similar ao que o scaffold faz na criação de models, controllers e views. O caminho até as traduções do arquivo app/views/landing_pages/pages/index.html.erb seria o equivalente ao exemplo a seguir:

1
2
3
4
5
6
|- config
|- - locales
|- - - landing_page
|- - - - index
|- - - - - index.en.yml
|- - - - - index.pt.yml

Caso opte por organizar as traduções em pastas e subpastas do caminho config/locale, é importante lembrar de mudar onde a aplicação buscará as traduções.

1
2
# config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]

Chamando o i18n.t

Uma boa prática ao criar as chaves de tradução, é a de especializá-la até que reflita o arquivo que ela será chamada. Na seção de truques entenderemos melhor os benefícios dessa especialização.

1
2
  <!-- app/views/lead_tracking/urls/index.html.erb -->
  <%= t('lead_tracking.urls.index.page_title') %>

Umk arquivo com muitas chamadas I18n pode ter um número extenso de folhas. Por isso podemos adaptar a regra de Sandi Metz e estipular que o número de folhas não deve ultrapassar 5 a 10 itens.

Assim melhoramos a organização do conteúdo de nossos arquivos YAML, não só a disposição deles. Porém, isso traz um problema: o tamanho da chave final da nossa chamada de tradução. Veremos como contornar isso na sequência.

Truques

A hiper especialização das suas chaves de tradução tendem a tornar as chamadas de tradução um pouco extensa, mas o framework de internacionalização possui algumas alternativas para deixar as chamadas menos verbosas.

Lazy Lookup

O Lazy lookup está disponível apenas nas views e controllers, e pode ser chamada em casos que o prefixo das chaves de tradução reflitam o caminho até o arquivo. Lembrando do caso anterior:

1
2
  <!-- app/views/lead_tracking/urls/index.html.erb -->
  <%= t('lead_tracking.urls.index.page_title') %>

O mesmo pode ser chamado da seguinte maneira:

1
2
  <!-- app/views/lead_tracking/urls/index.html.erb -->
  <%= t('.page_title') %>

Scope Looking

Outra forma de encurtar suas chamadas, ou de deixar seu código mais limpo, é através de chamadas contendo um scope. O scope lookup recebe um trecho da chave de tradução e o prefixo da chave é passado via scope:.

1
2
3
  <!-- app/views/lead_tracking/urls/index.html.erb -->
  <% index_scope = 'lead_tracking.urls.index' %>
  <%= t('page_title', scope: index_scope) %>

Chamando variáveis

Há casos em que um label pode conter um valor dinâmico, como por exemplo Olá, @account.name. Seja bem vindo ao RD Station. Nesse casos inserimos uma variável em nossa tradução e passamos o valor apropriado para ela.

1
2
3
#config/locales/index.pt.yml
pt:
  hello: 'Olá, %{name}. Seja bem vindo ao RD Station.'
1
2
  <!-- app/view/index.html.erb -->
  <h2> <%= t('.hello', name: @account.name) %> </h2>

Usando tags html.

Se por alguma razão o nome do usuário conectado precisa ser apresentado negritado na frase de boas vindas, a alteração é relativamente simples de ser feita. Porém tome cuidado e evite usar a chamada html_safe para escapar essas tags html. Ao invés disso, modifique a chave para que a mesma termine em _html, dessa maneira a biblioteca de internacionalização saberá que o valor traduzido contém tags html que precisam ser escapadas.

1
2
3
#config/locale/view/index.pt.yml
pt:
  hello_html: 'Olá, <strong>%{name}</strong>. Seja bem vindo ao RD Station.'
1
2
  <!-- app/view/index.html.erb -->
  <h2> <%= t('.hello_html', name: @account.name) %> </h2>

Lidando com plural

Uma forma nada prática de lidar com plurais é através de um condicional na view que verifica o número de ocorrências de algum tipo de lista. A condicional mostra uma mensagem caso ocorra uma vez, ou outra mensagem para outras ocorrências. O exemplo a seguir demonstra como ficaria essa forma de pluralização.

1
2
one_email: 'Você tem um novo email'
more_emails: 'Você tem %{count} novos emails'
1
2
3
4
5
  <% if @account.emails.count == 1 %>
    <%= t("one_email") %>
  <% else %>
    <%= t("more_emails", count: @account.emails.count %>
  <% end %>

Apesar de funcional, fica uma impressão de smell no código. Por sorte a gema de internacionalização já suporta nativamente uma maneira mais elegante de lidar com pluralização de mensagems. Utilizando a option count:, você passa o número de ocorrência de uma lista e a gema determina qual é a mensagem correta para o valor.

1
2
3
4
email:
  zero: 'Você não possui novos emails'
  one: 'Você tem um novo email'
  more: 'Você tem %{count} novos emails'
1
  <%= t("email", count: @account.emails.count %>

Prefira Rails Helpers

O exemplo anterior mostra um bom uso de chaves _html. Mas é fácil acabar colocando tags html extensas e difusas dentro de sua traduções. Isso polui sua biblioteca de traduções, além de interferir no princípio da responsabilidade ao atribuir elementos de view em arquivos especializados em chaveamentos de strings e traduções.

1
2
#RUIM
learn_more_html: 'O tipo de fluxo não pode ser alterado durante a edição. <a href="%{url}" target="_blank">Saiba mais.</a>'
1
<%= t('.learn_more_html', url: 'http://ajuda.rdstation.com.br/hc/pt-br/articles/205718199-Beta-Como-troco-um-tipo-de-fluxo-') %>
1
2
3
4
#BOM
cant_change_on_edit: 'O tipo de fluxo não pode ser alterado durante a edição. %{learn_more_link}'
learn_more: 'Saiba mais'
learn_more_url: 'http://ajuda.rdstation.com.br/hc/pt-br/articles/205718199-Beta-Como-troco-um-tipo-de-fluxo-'
1
2
<%= t('.cant_change_on_edit', learn_more_link: link_to(t('.learn_more'), t('learn_more_url'),
                                                       target: '_blank') )  %>

Mostrando traduções em outros idiomas

É possível explicitamente mostrar uma mensagem em um idioma diferente do atribuído na sessão (I18n.locale). Para isso basta utilizar o option locale:

1
2
  <!-- app/views/lead_tracking/urls/index.html.erb -->
  <%= t('.page_title', locale: 'es') %>

Bônus

Por último, deixo como bônus a leitura do post da Thoughtbot sobre como melhorar seus testes através da internacionalização. Ao utilizar chamadas I18n.t os testes não ficarão sujeitos a quebrar se houver alguma mudança no texto de alguma view.

No próximo post veremos as configurações possíveis de serem feitas com relação a mudança de idioma e de fuso horário da sua aplicação Rails.

Se você já passou pela internacionalização e achou uma maneira diferente de organizar e chamar sua traduções? Deixe nos comentários como você fez!

Diogo Busanello

Diogo Busanello

Full Stack Developer

Comentários