Internacionalização com React usando Create react app i18n

Quando falamos em internacionalização em React, a primeira lib que nos vem a mente é a react-intl, uma das bibliotecas mais populares do mercado, criada pelo Yahoo para internacionalizar aplicações feitas em React.

Por que outra solução de internacionalização para React?

Para respondermos esta pergunta, precisamos primeiro entender como o react-intl funciona.

Como o react-intl funciona?

O react-intl decora seu component react com um wrapper component, onde as mensagens internacionalizadas são injetadas para que os dados do idioma possam ser carregados dinamicamente sem recarregar a página. A seguir está o código de exemplo usando reac-intl.

import { injectIntl } from 'react-intl';
class MyComponent extends Component {
  render() {
    const intl = this.props;
    const titulo = intl.formatMessage({ id: 'titulo' });
    return (<div>{titulo}</div>);
  }
};
export default injectIntl(MyComponent);

No entanto, com essa abordagem nos deparamos com dois problemas novos:

1 - A internacionalização pode ser aplicada apenas na camada de visualizaçâo, em um component react. Em um arquivo com Vanilla JS não é possível internacionalizar, como por exemplo, imaginamos o código abaixo, um validador genérico que é usado por diversos componentes em nossa aplicação.

export default const rules = {
  noSpace(value) {
    if (value.includes(' ')) {
      return 'O campo não pode conter espaços.';
    }
  }
};

2 - Como o seu componente react é envolvido por outra classe, o comportamento não é o esperado. Para obter a instância do componente, não poderiamos usar da maneira tradicional:

class App {
  render() {
    <MyComponent ref="my"/>
  }
  getMyInstance() {
    console.log('Minha instância', this.refs.my);
  }
}

Neste caso, precisaríamos exportar nosso componente, passando como parâmetro {withRef: true}:

class MyComponent {
  // Código omitido
}
export default injectIntl(MyComponent, {withRef: true});

E usar o método getWrappedInstance() para a instância do componente:

  // Código omitido

  getMyInstance() {
    console.log('Minha instância', this.refs.my.getWrappedInstance());
  }

  // Código omitido

Além disso, as propriedades do seu componente react não são herdadas na subclasse, pois o componente é injetado pelo react-intl.

Devido aos empecilhos citados acima, o pessoal do Alibaba criou o react-intl-universal para internacionalizar aplicações em React.

Setup do projeto

Vamos a prática! Iniciamos criando nosso app com o create-react-app. Caso não tenha instalado, confira o início deste post onde ensino como instalar.

create-react-app react-intl-universal-exemple

Nossa aplicação foi criada, vamos instalar o react-intl-universal:

npm install react-intl-universal --save

Após isso, vamos criar uma pasta locales, dentro da pasta src do nosso projeto. O nome desta pasta é você quem escolhe, caso queira definir um nome ou localização diferente para ela, sinta-se à vontade.

Na sequência, podemos criar nossos arquivos que contém os textos que serão traduzidos. Aqui neste exemplo, vamos criar 2 arquivos .json dentro da pasta que acabamos de criar. Um para o idioma português e outro para o idioma inglês.

pt-BR.json

{
  "header": {
    "title": "Bem-vindo ao React"
  },
  "home": {
    "description": "Para começar, edite o arquivo <code>src/App.js</code> e salve para atualizar."
  }
}

en-US.json

{
  "header": {
    "title": "Welcome to React"
  },
  "home": {
    "description": "To get started, edit <code>{arquivo}</code> and save to reload."
  }
}

Vamos editar nosso App.js, adicionando o import do react-intl-universal.

import React, { Component } from 'react';
import intl from 'react-intl-universal';
// Código omitido

Vamos criar uma constante referenciando nossos arquivos de tradução.

// Código omitido
const locales = {
  'pt-BR': require('./locales/pt-BR.json'),
  'en-US': require('./locales/en-US.json')
};
// Código omitido

E finalizando o setup do projeto, vamos inicializar o react-intl-universal, passando para ele nossos arquivos de tradução e qual é o idioma padrão. Vamos pegar o idioma do navegador do usuário e caso não exista tradução para ele, colocaremos como padrão pt-BR.

// Código omitido
constructor() {
  super();

  const currentLocale = locales[navigator.language]
                        ? navigator.language
                        : 'pt-BR';

  intl.init({
    currentLocale,
    locales
  });
}
// Código omitido

O arquivo App.js, após as mudanças ficou desta forma. Note que ainda estamos passando os textos fixos dentro do método render.

import React, { Component } from 'react';
import intl from 'react-intl-universal';

import logo from './logo.svg';
import './App.css';

const locales = {
  'pt-BR': require('./locales/pt-BR.json'),
  'en-US': require('./locales/en-US.json')
};

class App extends Component {

  constructor() {
    super();

    const currentLocale = locales[navigator.language]
                            ? navigator.language
                            : 'pt-BR';

    intl.init({
      currentLocale,
      locales
    });
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

Com isso, a internacionalização do nosso projeto está configurada e podemos partir para as traduções.

Como aplicamos as traduções

O react-intl-universal disponibiliza 2 métodos bem simples mas bastante poderosos para nós: o intl.get() e o intl.getHTML().

intl.get() – Usamos ele para buscar uma string simples ou uma string que tenha algum trecho que precisamos fazer um replace.

intl.getHTML() – Usamos para buscar uma string que também contenha blocos de HTML.

Vamos ao exemplo:

pt-BR.json

{
  "text": "Lorem ipsum dolor sit amet",
  "textHTML": "Lorem ipsum <span style='color:red'>dolor</span> sit amet"
}

nosso-javascript.js

intl.get('text');
intl.getHTML('text');

Podemos também definir um texto padrão quando a texto que buscamos não existir:

// Padrão
intl
  .get('um-texto-inexistente')
  .default('Esta tradução não existe'); // Esta tradução não existe

// Reduzida
intl
  .get('um-texto-inexistente')
  .d('Esta tradução não existe'); // Esta tradução não existe

// Com HTML
intl
  .getHTML('um-texto-inexistente')
  .d('<p>Esta tradução não existe</p>'); // <p>Esta tradução não existe</p>

Textos com variáveis

Se a mensagem contiver variáveis, o {nome_da_variavel} será substituído diretamente na string. No exemplo abaixo, temos duas variáveis {nome} e {local}, o segundo argumento representando as variáveis no método get é substituído na string.

pt-BR.json

{
  "bemvindo": "Olá, {name}. Bem vindo a {where}!"
}

nosso-javascript.js

intl.get('bemvindo', {name:'Everton', where:'Meu blog'})
// "Olá, Everton. Bem vindo ao Meu blog!"

A lib também tem suporte para Plural, números, separadores, moeda, datas, horas, etc. Os textos no Plural, suportam o padrão ICU Message syntax.

Você pode conferir mais no GitHub do Alibaba.

Aplicando as traduções ao nosso projeto

Vamos aplicar as traduções ao nosso projeto, seguindo o que aprendemos até agora vamos alterar nossos arquivos .json para adicionarmos uma variável.

pt-BR.json

{
  "header": {
    "title": "Olá {nome}, Bem-vindo ao React"
  },
  "home": {
    "description": "Para começar, edite o arquivo <code>{arquivo}</code> e salve para atualizar."
  }
}

en-US.json

{
  "header": {
    "title": "Hello {nome}, Welcome to React"
  },
  "home": {
    "description": "To get started, edit <code>{arquivo}</code> and save to reload."
  }
}

E vamos alterar o método render do nosso App.js, para renderizar nossos textos conforme o idioma e as variáveis que passarmos.

render() {
  const nome = 'Everton';
  const arquivo = 'src/App.js';
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h1 className="App-title">
          {intl.get('header.title', {nome: nome})}
        </h1>
      </header>
      <p className="App-intro">
        {intl.getHTML('home.description', {arquivo: arquivo})}
      </p>
    </div>
  );
}

E pronto, temos nossa aplicação rodando e internacionalizada.

React intl universal exemplo

Próximos passos

O bacana no react-intl-universal é que ele não se limita somente ao react, mas você pode usar ele para tradução de mensagens em suas aplicações que rodam Vanilla JS, ou como no exemplo que citei, em uma função javascript que não é um componente do React. O que facilita bastante, pois não nos prende a usar o contexto do react.

Na hora dos testes unitários, também é bem fácil de usar. Apenas chamamos o arquivo com a localização e iniciamos o react-intl-universal.

Como próximo passo poderíamos adicionar um seletor de idioma na nossa aplicação, para alterar de um idioma para o selecionado.

Conclusão

Tenho trabalhado bastante com react nos últimos tempos, e passei semanas atrás de uma solução menos engessada para internacionalizar a aplicação que estou construindo, o react-intl-universal supriu essa minha necessidade de uma forma simples e menos intrusiva.

Dê uma conferida no react-intl-universal.

O código que desenvolvemos aqui está GithHub, para ver o código completo é só acessar.

Até mais e obrigado pelos peixes.