fbpx

Internacionalização com React Native

Tecnologia

Introdução

Na computação, a internacionalização e localização trata sobre os meios de adaptar um software para diferentes línguas, peculiaridades regionais ou requisitos técnicos do público alvo. A internacionalização é processo de criar uma aplicação que pode se adaptar a diferentes requisitos linguísticos sem quaisquer alterações na arquitetura geral do sistema, se mostrando uma ferramenta extremamente importante na escalabilidade. Já a localização é o processo de adaptar o software para diferentes meios linguísticos utilizando toda a arquitetura definida pela internacionalização.

 Sim,  ‘internacionalização’ é um nome gigantesco, por essa razão foi criada uma nomenclatura muito mais enxuta (e atualmente famosa) para definir esses processos: i18n. Onde ‘i’ é a primeira letra da palavra e o número 18 é a quantidade de letras entre o ‘i’ e o último ‘n’. A partir de agora vamos utilizar essa nomenclatura, porém existem outras terminologias adotadas pela comunidade – apesar de pouco utilizadas – como ‘g11n’ e ‘L12y’.

Além de ser escalável, a i18n melhora a UX final através da facilidade com que podem ser implementadas funcionalidades como a pluralização por exemplo, ninguém gosta de mensagens incorretas:

“Você tem 1 itens no carrinho”

Em termos de UX isso é grotesco, é um erro claro de concordância e não deve ser tolerado. Todavia, alguns podem argumentar que um simples “if” resolveria o problema, a resposta é: não. Perceba que para fazer isso o desenvolvedor terá que alterar a arquitetura, ou seja, fugindo dos princípios da i18n. Além disso, imagine que existam 5 frases que devem ser pluralizadas de formas diferentes, iremos ter que construir 5 funções para isso? Pouco prático e nada escalável e não dando suporte a multilinguagem.

Como funciona

De forma simplista, teremos arquivos que irão conter os textos respectivos de cada idioma. Imagine que temos uma tela de boas-vindas na nossa aplicação e que precisamos mostrar um texto de recepção. Nossa arquitetura irá carregar esses arquivos com as traduções assim como a língua atual do usuário, escolhendo qual desses arquivos deve ser utilizado, então a aplicação pode mostrar corretamente a mensagem de boas-vindas. Segue o pseudo-code:

Sem internacionalização

<div>
 Bem-vindo!
</div>

Com internacionalização

<div>
  {translate(‘home.welcome’)}
</div>

Nesse exemplo, perceba que não teremos mais um texto fixo na aplicação e sim uma função que irá buscar a mensagem no dicionário de palavras do sistema através de uma key, nesse caso ‘home.welcome’.

No React-Native

Se você ja utilizou o Instagram (ou qualquer outro app que adote a i18n) tenho certeza que reparou que o mesmo já identifica o idioma do seu celular e define-o como padrão da aplicação. Ainda assim ele permite que você mude o idioma internamente sem ter que mudar qualquer configuração do seu celular, sendo possível ter o sistema operacional com o Português Brasileiro, por exemplo, mas ter o Instagram rodando em Alemão.

Isso é possível porque as aplicações mobile conseguem identificar o idioma atual do celular e com isso salvar em um armazenamento local qual língua deve ser utilizada (assim como softwares Web fazem, apenas de forma diferente) e, dessa forma, permitir que o usuário troque o idioma dentro do app, dando uma liberdade maior para o consumidor.

Portanto, nesse artigo iremos implementar i18n em uma simples aplicação com RN mas que ira apresentar os conceitos necessários que podem ser utilizados posteriormente em qualquer projeto, já que é completamente escalável.

Vamos começar?!

A fim de facilitar o processo e levando em conta que você já tem um conhecimento básico do framework, tomei a liberdade de criar uma aplicação extremamente simples com apenas 2 telas: a Home onde mostraremos alguns textos e um exemplo de pluralização dinâmica e a tela de Idiomas onde vamos deixar o usuário alterar o idioma do app. Se você não sabe por onde começar, fique tranquilo, estamos aqui para aprendermos juntos. Temos aqui no blog e no YouTube conteúdos introdutórios do RN e Componentização , qualquer dúvida só dar uma olhadinha lá, beleza?

Vamos criar uma nova aplicação usando a versão do  RN > 0.60

react-native init nome_da_aplicação

Precisamos instalar também 2 dependências que irão facilitar nossa vida com a internacionalização no futuro

yarn add i18next react-i18next

Além disso, também estou utilizando o react-navigation para criar a navegação da aplicação (se você nunca utilizou essa incrível lib agora é a chance de dar uma olhadinha nas Docs). Mas, sinta-se à vontade para utilizar a lib de navegação que mais lhe agrada.

Após os setups iniciais dos arquivos e de uma estilização básica a aplicação sem internacionalização ficou dessa forma:

Figura 1 – Página Home
Figura 2 – Página Idiomas

Clicando aqui você pode encontrar o código que estrutura cada uma dessas páginas e a estrutura de navegação por trás. Tudo é bem simples, temos a Home que exibe alguns textos estáticos e possui dois botões que incrementam e decrementam um contador que representa o número de fictício de “downloads” da aplicação, que como você pode perceber esta com a concordância incorreta (iremos consertar isso com i18n).

Temos também a páginas de Idiomas que exibe todos os idiomas disponíveis no app e marca com uma bola verde qual é o idioma atual do app. Como ainda não temos a estrutura de i18n, está sendo mostrado a marcação para todos os idiomas.

“Internacionalizando”

Vamos de fato começar a implementar a estrutura i18n. Primeiramente crie uma pasta chamada “locales” dentro da pasta “src” do projeto, dentro dela crie uma pasta “translations” que será responsável por armazenar os arquivos de tradução. Crie também um arquivo “index.js” dentro de “locales”, responsável por fazer o setup da nossa arquitetura.

Figura 3 – Estrutura dos arquivos

Dentro dos arquivos de tradução iremos criar as chaves com os valores que iremos mostrar nas telas.

pt_BR.json

Os outros arquivos de tradução, como o en_US.json, devem conter as mesmas chaves que arquivo pt_BR.json. Dessa maneira, todos os textos da sua aplicação estarão disponíveis nas línguas que você almeja. Nesse caso, estou utilizando apenas dois idiomas diferentes, mas fique a vontade para utilizar quantos forem necessários.

Vamos discutir um pouco sobre esse arquivo. Primeiramente, ele é um arquivo JSON, ou seja, iremos trabalhar com chave:valor, o que é ótimo já que estamos habituados com esse modelo no Javascript. Perceba, também, que separamos as chaves da tela Home, Idiomas e o nome das línguas em diferentes blocos, dessa forma o arquivo fica mais organizado. Essa organização recebe o nome de namespaces e iremos utilizá-los quando carregarmos os textos para exibição nas telas.

Powered by Rock Convert

Agora iremos trabalhar no setup da i18n, dentro do arquivo “index.js” da pasta “locales”.

import {NativeModules, Platform} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import i18n from 'i18next';
import {initReactI18next} from 'react-i18next';
import pt_br from './translations/pt_BR.json';
import en_us from './translations/en_US.json';

const resources = {
  ['pt-BR']: pt_br,
  ['en-US']: en_us,
};

const languageDetector = {
  type: 'languageDetector',
  async: true,
  detect: async callback => {
    // Primeiro vamos checar se ja salvamos o
    // idioma do usuario, caso contrario vamos
    // detectar utilizando o NativeModules do RN
    const storedLanguage = await AsyncStorage.getItem('language');
    if (storedLanguage) {
      return callback(storedLanguage);
    }

    let phoneLanguage = null;
    if (Platform.OS === 'android') {
      phoneLanguage = NativeModules.I18Manager.localeIdentifier;
    } else {
      phoneLanguage = NativeModules.SettingsManager.settings.AppleLocale;
    }

    phoneLanguage = phoneLanguage.replace('_', '-');

    return callback(phoneLanguage);
  },
  init: () => {},
  cacheUserLanguage: language => {
    // Essa função sera chamada assim que o callback
    // da função 'detect' for executado. Aqui podemos
    // salvar o idioma do usuario no AsyncStorage para
    // persistirmos sua escolha nas próximas execuçōes do app
    AsyncStorage.setItem('language', language);
  },
};

i18n
  .use(languageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: 'en-US',
    debug: true,
    interpolation: {
      escapeValue: false,
    },
  });

export default i18n;

Esse arquivo é um pouco grande mas nada complicado. Primeiramente, importamos tudo o que iremos precisar, inclusive os arquivos de tradução que acabamos de criar. Caso você tenha dúvidas a respeito de como o i18next funciona por trás ou por que temos que importar esses módulos, vale a pena dar uma olhadinha mais a fundo na documentação.

Criamos um objeto resources que é apenas a junção de todas os idiomas disponíveis (atenção para os nomes, são importantes para a pluralização). Em seguida, criamos o objeto languageDetector que é responsável por detectar e armazenar o idioma no RN. Vamos ver passo a passo como isso foi feito:

  1. Seguindo o guia de criação de plugins do i18next, podemos escrever nosso proprio detector de idiomas (perfeito!). Seu “type” será ‘languageDetector’, já que será responsável por detectar o idioma e será assíncrono pois precisamos carregar o idioma do AsyncStorage.
  2. A propriedade detect é a responsável por receber uma função que deve retornar o idioma do usuário. Buscamos no AsyncStorage se o usuário já tem um idioma salvo, caso tenha paramos por aí e o retornamos, caso contrário detectamos o idioma do celular do usuário e padronizamos ele com a função replace, para que o nome siga o mesmo padrão do objeto resources, em seguida retornamos o idioma detectado.
  3. A propriedade cacheUserLanguage recebe uma função que será responsável por armazenar de alguma forma o idioma do usuário. Nesse caso, estamos usando o AsyncStorage.

Com o detector pronto podemos partir para a inicialização do contexto do i18next. Utilizando o i18n iremos passar atraves do metodo use nosso detector e o initReactI18next, que será responsável por criar os contextos que permitem a utilização dos hooks nos processos de internacionalização.

Em seguida, chamamos o método init passando as configurações que inicais. Se precisar de mais detalhes sobre as opções que podem ser utilizadas de uma olhadinha aqui.

Pronto, certo? Quase. Precisamos importar esse arquivo em algum lugar para executá-lo, todavia, perceba, que como a inicialização é assíncrona devemos esperar esse processo ser completado antes que qualquer funcionalidade do react-i18next seja utilizada. Vamos fazer tudo isso no arquivo App.js

import React, {Fragment, Suspense} from 'react';
import {StatusBar, ActivityIndicator} from 'react-native';
import AppContainer from './src/navigation';
import './src/locales';

const App = () => {
  return (
    <Fragment>
      <StatusBar barStyle="dark-content" />
      <Suspense fallback={<ActivityIndicator />}>
        <AppContainer />
      </Suspense>
    </Fragment>
  );
};

export default App;

Importamos o index.js da pasta “locales” para executar o método init e no retorno do componente funcional utilizamos o Suspense para agurdar a inicialização. Pronto, agora podemos dizer que estamos com a arquitetura montada, vamos começar a implementá-la nas telas!

Utilizando hooks

Chegou a hora de realmente utilizar o que construímos até agora. Os hooks disponibilizados pelo React em conjunto com o react-i18next facilita muito esse processo. Basta importar o useTranslation do react-i18next e ser feliz.

function Home() {
  const [state, dispatch] = useReducer(
    downloadedReducer,
    downloadedInitialState,
  );

  const {t} = useTranslation('home');

  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.container}>
        <Text style={styles.welcome}>{t('welcome')}</Text>
        <Text style={styles.downloaded}>
          {t('downloadedStart')}
          <Text style={styles.downloadedCount}>{state.count}</Text>
          {t('downloadedEnd', {count: state.count})}
        </Text>
        <Text style={styles.description}>{t('description')}</Text>
        <Button
          title={t('button_increment')}
          onPress={() => dispatch({type: 'INCREMENT'})}
        />
        {state.count > 0 ? (
          <Button
            style={styles.button}
            title={t('button_decrement')}
            onPress={() => dispatch({type: 'DECREMENT'})}
          />
        ) : null}
      </View>
    </SafeAreaView>
  );
}

Perceba que a desestruturação da funcao t retornada pelo useTranslation, nos permite acessar aquelas chaves que criamos nos arquivos de tradução, e claro, retornando os textos baseado no idioma detectado. Note também, que quando inicializamos o hook passamos um namespace, nesse caso “home”, como parâmetro, dessa maneira temos acesso a todas as traduções dentro do namespace especificado. Se caso precisar de mais de um namespace basta passar um array de namespaces ;).

Como estava rodando no simulador do iPhone, que por padrão vem com o idioma inglês, a aplicação já respondeu às alterações mostrando os textos na língua inglesa.

Outro ponto muito importante é o parâmetro count enviado na chave dowloadedEnd. Lembra que podemos pluralizar utilizando i18n? Então, enviando o atributo count podemos definir, nos arquivos de tradução, uma chave igual mas com o sufixo _plural para que a chave em questão possa ter variações. Fizemos isso nos arquivos de tradução, dessa forma conseguimos retornar diferentes textos baseado na quantidade do parâmetro count. Se quiser entender mais como isso funciona e como você pode explorar mais detalhadamente essa feature de uma olhadinha aqui.

Alterando o Idioma

Agora que ja temos a pagina Home “traduzida” podemos ir para o próximo passo. Quando o usuário clicar em algum dos idiomas disponíveis na tela “Idiomas”, toda a aplicação deve responder alterando o idioma exibido.

Como temos toda a arquitetura pronta, esse processo se torna relativamente simples. Primeiramente, vamos alterar o useMemo que monta a lista de idiomas a serem mostrados, temos que nos certificar que toda vez que o idioma seja alterado essa lista sofra um update para mostrar o nome das línguas corretamente.

const {t, i18n} = useTranslation('language');

  const languages = useMemo(() => {
    return [
      {name: t('portuguese'), id: 'pt-BR'},
      {name: t('english'), id: 'en-US'},
    ];
  }, [i18n.language]);

Estamos novamente utilizando o useTranslation para desestruturar a função t, mas, agora também vamos desestruturar o objeto i18n que nos permite acessar diversas propriedades do contexto do i18next, alem de expor algumas funçōes bem uteis, como por exemplo, alterar o idioma.

Alterando função que renderiza os itens da lista, apenas mostraremos o indicador verde no idioma atual do app. Ainda, iremos adicionar uma funcionalidade verdadeira ao touch do texto, fazendo com que assim que o usuário pressione o Touchable, o idioma seja alterado para aquele escolhido.

const onPressLanguage = useCallback(language => {
    i18n.changeLanguage(language);
  }, []);

  const renderItem = ({item}) => {
    const isSelected = item.id === i18n.language;
    return (
      <View style={styles.item}>
        <View style={styles.nameContainer}>
          <TouchableOpacity onPress={() => onPressLanguage(item.id)}>
            <Text style={styles.languageName}>{item.name}</Text>
          </TouchableOpacity>
        </View>
        {isSelected ? <View style={styles.selected} /> : null}
      </View>
    );
  };

Portanto, o codigo final da tela de Idioma ficou assim (sem estilizacao):

import React, {useMemo, useEffect, useCallback} from 'react';
import {
  SafeAreaView,
  View,
  Text,
  StyleSheet,
  FlatList,
  TouchableOpacity,
} from 'react-native';
import {useTranslation} from 'react-i18next';

function Language() {
  const {t, i18n} = useTranslation('language');

  const languages = useMemo(() => {
    return [
      {name: t('portuguese'), id: 'pt-BR'},
      {name: t('english'), id: 'en-US'},
    ];
  }, [i18n.language]);

  const onPressLanguage = useCallback(language => {
    i18n.changeLanguage(language);
  }, []);

  const renderItem = ({item}) => {
    const isSelected = item.id === i18n.language;
    return (
      <View style={styles.item}>
        <View style={styles.nameContainer}>
          <TouchableOpacity onPress={() => onPressLanguage(item.id)}>
            <Text style={styles.languageName}>{item.name}</Text>
          </TouchableOpacity>
        </View>
        {isSelected ? <View style={styles.selected} /> : null}
      </View>
    );
  };

  return (
    <SafeAreaView style={styles.safeArea}>
      <FlatList
        style={styles.list}
        data={languages}
        key={(item, index) => String(index)}
        renderItem={renderItem}
      />
    </SafeAreaView>
  );
}

Finalizando

Apesar de simples e enxuta, a aplicação utiliza a maioria dos recursos que são necessário durante o processo de internacionalização. Sempre que for dar inicio a um novo projeto cogite fortemente na implementação da i18n, os benefícios são excelentes para a qualidade do produto e sua escalabilidade.

Demonstração do projeto final

Entre com seus dados para a ligação.