Qual a melhor forma de traduzir seu app Flutter para outros idiomas?

Tempo de leitura: 4 minutes

Se você tem um widget com algum texto nele:

Text("Olá, como vai você")

Para traduzir basta escrever .i18n depois do texto:

Text("Olá, como vai você".i18n)

Se o locale do aplicativo é en_US (inglês americano) então o texto na tela vai aparecer traduzido para "Hello, how are you". E assim por diante para todos os locales que você quiser suportar.

Isso é possível através do meu pacote i18n_extension, que para minha surpresa foi citado pelo Google no anúncio do Dart 2.7.

Você também pode definir diferentes traduções dependendo de modificadores, por exemplo usando plural() para descrever diferentes quantidades de algo:

print("Tem 1 item".plural(0)); // Escreve: Não tem itens
print("Tem 1 item".plural(1)); // Escreve: Tem 1 item
print("Tem 1 item".plural(2)); // Escreve: Tem 2 itens

E você pode inventar os seus próprios modificadores. Por exemplo, algumas linguagens como o português e o francês tem diferentes traduções para diferentes gêneros (masculino/feminino/neutro).
Então você poderia criar versões gender() para modificadores Gender:

print("É uma pessoa".gender(Gender.male)); // É um homem
print("É uma pessoa".gender(Gender.female)); // É uma mulher
print("É uma pessoa".gender(Gender.they)); // É uma pessoa

Preparação

Coloque a sua árvore de widgets dentro do widget I18n, abaixo do MaterialApp:

import "package:i18n_extension/i18n_widget.dart";
...
@override
Widget build(BuildContext context) {
   return MaterialApp(
      home: I18n(child: ...)
   );
}

O código acima vai traduzir seus textos para o locale atual do sistema.

Ou você pode escolher o seu próprio locale, assim:

I18n(
  initialLocale: Locale("pt", "BR"),
  child: ...

Note que o locale é uma configuração de idioma/região do aplicativo. Ele é composto de um idioma tal como en (inglês), pt (português) ou fr (francês); e mais uma região tal como US (Estados Unidos), BR (Brasil) ou FR (França).

Traduzindo um widget

Para cada widget (ou grupo de widgets relacionados) você deve criar um arquivo de traduções, assim:

import "package:i18n_extension/i18n_extension.dart";
extension Localization on String {
String get i18n => localize(this, t);
static var t = Translations("pt_br") +
   {
   "en_us": "Hello, how are you?",
   "pt_br": "Olá, como vai você?",
   "es": "¿Hola! Cómo estás?",
   "fr": "Salut, comment ca va?",
   "de": "Hallo, wie geht es dir?",
   };
}

O exemplo acima mostra uma única “string traduzível”, traduzida para inglês americano, português do Brasil, além de espanhol, francês e alemão genéricos.

Mas você pode traduzir quantos textos quiser, simplesmente adicionando mais mapas de tradução:

import "package:i18n_extension/i18n_extension.dart";
extension Localization on String {
String get i18n => localize(this, t);
static var t = Translations("pt_br") +
   {
     "en_us": "Hello, how are you?",
     "pt_br": "Olá, como vai você?",
   } +
   {
     "en_us": "Hi",
     "pt_br": "Olá",
   } +
   {
     "en_us": "Goodbye",
     "pt_br": "Adeus",
   };
}

As próprias strings são as chaves de tradução

O locale que você passa no construtor Translations("pt_br") é chamado de locale padrão do arquivo de tradução. Todas as strings traduzíveis usadas dentro do widget em si devem estar no idioma desse locale.

As strings em si funcionam como chaves para buscar as traduções para outros locales. Por exemplo, no Text abaixo, "Oi, tudo bem?" é tanto a tradução para Português quanto também a chave que será usada pelo pacote ao buscar as demais traduções:

Text("Oi, tudo bem?".i18n)

Gerenciando chaves

Outros pacotes de tradução pedem que você defina e use identificadores como chaves para cada tradução. Por exemplo, um identificador para o texto acima poderia ser oiTudoBem ou simpesmente saudacao. E daí você poderia usá-lo assim: MyLocalizations.of(context).saudacao.

Acontece que ter que definir identificadores não é apenas uma tarefa chata, mas também força você a navegar para a tradução sempre que precisar lembrar qual o exato texto do widget. Com o pacote i18n_extension você pode simplesmente digitar o texto que você quer e pronto.

Se algum texto já estiver traduzido e você depois alterar o texto original dentro do widget, isso vai quebrar o link entre a chave e o mapa de traduções. Entretanto, o pacote é esperto o suficiente para avisar você quando isso acontecer, de modo que é fácil consertar. Você pode adicionar essa checagem aos seus testes, para ter certeza sempre que suas traduções estão linkadas e completas. Ou você pode lançar um erro, ou escrever o problema no log, ou mandar um email automático para o responsável pelas traduções.

Definindo traduções por idioma

Como explicado, ao usar o construtor Translations() você define uma chave e daí fornece as traduções para outros idiomas neste mesmo momento. Essa é a maneira mais fácil quando você está fazendo traduções manuais. Por exemplo, quando você fala português e inglês e está criando você mesmo as traduções para o seu app.

Entretanto, se você vai contratar tradutores profissionais, pode ser mais fácil fornecer as traduções separadas por locale, em vez de por chave. Você pode fazer isso usando o construtor Translations.byLocale():

static var t = Translations.byLocale("en_us") +
   {
   "en_us": {
       "Hi": "Hi",
       "Goodbye": "Goodbye",
       },   "es_es": {
       "Hi": "Hola",
       "Goodbye": "Adiós",
       }
   };

Modificadores

Às vezes você pode querer ter diferentes traduções dependendo de uma quantidade numérica. Em vez de .i18n você pode usar .plural() e passar um modificador numérico. Por exemplo:

int numDeItens = 3;
return Text("Você clicou o botão %d vezes".plural(numDeItens));

Isso será primeiro traduzido, e se a string traduzida contiver %d isso será trocado pelo número.

O seu arquivo de traduções pode conter algo assim:

static var t = Translations("pt_br") +
   {
   "en_us": "You clicked the button %d times"
       .zero("You haven’t clicked the button")
       .one("You clicked it once")
       .two("You clicked a couple times")
       .many("You clicked %d times")
       .times(12, "You clicked a dozen times"),   
   "pt_br": "Você clicou o botão %d vezes"
       .zero("Você não clicou no botão")
       .one("Você clicou uma única vez")
       .two("Você clicou um par de vezes")
       .many("Você clicou %d vezes")
       .times(12, "Você clicou uma dúzia de vezes"),
   };

 

Modificadores Customizados

Você pode, na verdade, criar os modificadores que você quiser. Por exemplo, você pode criar .gender() que aceita modificadores do tipo Gender:

enum Gender {they, female, male}
int gnd = Gender.female;
return Text("Uma pessoa".gender(gnd));

Então, o seu arquivo de traduções pode usar .modifier() e localizeVersion() assim:

static var t = Translations("pt_br") +
   {
   "pt_br": "Uma pessoa"
       .modifier(Gender.male, "Um homem")
       .modifier(Gender.female, "Uma mulher")
       .modifier(Gender.they, "Uma pessoa"),
   "en_us": "A person"
       .modifier(Gender.male, "A man")
       .modifier(Gender.female, "A woman")
       .modifier(Gender.they, "A person"),      
   };
String gender(Gender gnd) => localizeVersion(gnd, this, t);

 

Importando e exportando

Este pacote é otimizado de modo que você pode facilmente criar e gerenciar todas as suas traduções você mesmo, manualmente, em projetos pequenos. Mas para projetos grandes, quando contratar tradutores profissionais, você vai precisar importar/exportar de/para formatos externos tais como .arb.po.icu.json.yaml.csv, ou .xliff.

Isso é fácil de fazer, pois os construtores dos objetos do pacote recebem mapas comuns de Dart como input. Então você pode simplesmente gerar mapas a partir de qualquer formato e daí usar os construtores Translation() ou Translation.byLocale() para criar os objetos de tradução.