Internacionalização no frontend com i18next e formatação de datas

1. Por que internacionalizar o frontend é essencial para produtos globais

1.1. O impacto da localização na experiência do usuário e retenção

Produtos digitais que alcançam audiências globais precisam falar a língua do usuário — literalmente. Estudos mostram que 75% dos consumidores preferem comprar em seu idioma nativo, e 60% raramente ou nunca compram em sites apenas em inglês. A internacionalização (i18n) não é apenas um detalhe técnico; é um fator decisivo para retenção e conversão.

1.2. Diferenças culturais além do idioma: formatos de data, moeda e números

Traduzir textos é apenas o começo. Formatos de data variam dramaticamente entre regiões: enquanto nos EUA escreve-se "12/31/2024", no Brasil usa-se "31/12/2024", e no Japão "2024年12月31日". Moedas, separadores decimais e fusos horários adicionam camadas de complexidade que exigem soluções robustas.

1.3. i18next como solução madura e modular para React, Vue e Angular

O i18next é a biblioteca de internacionalização mais popular do ecossistema JavaScript, com mais de 30 mil estrelas no GitHub. Sua arquitetura modular permite integração com React, Vue, Angular e até frameworks server-side, oferecendo detecção automática de idioma, lazy loading e suporte a pluralização complexa.

2. Configuração inicial do i18next no projeto frontend

2.1. Instalação e inicialização com i18next e react-i18next

npm install i18next react-i18next i18next-browser-languagedetector
// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import en from './locales/en.json';
import pt from './locales/pt.json';
import ja from './locales/ja.json';

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: en },
      pt: { translation: pt },
      ja: { translation: ja }
    },
    fallbackLng: 'en',
    interpolation: {
      escapeValue: false
    }
  });

export default i18n;

2.2. Estrutura de arquivos de tradução (JSON por namespace)

// locales/en.json
{
  "welcome": "Welcome, {{name}}!",
  "days_ago": "{{count}} day ago",
  "days_ago_plural": "{{count}} days ago",
  "date_format": "MM/DD/YYYY"
}

// locales/pt.json
{
  "welcome": "Bem-vindo, {{name}}!",
  "days_ago": "{{count}} dia atrás",
  "days_ago_plural": "{{count}} dias atrás",
  "date_format": "DD/MM/YYYY"
}

// locales/ja.json
{
  "welcome": "ようこそ、{{name}}!",
  "days_ago": "{{count}}日前",
  "days_ago_plural": "{{count}}日前",
  "date_format": "YYYY年MM月DD日"
}

2.3. Detecção automática de idioma do navegador com i18next-browser-languagedetector

O LanguageDetector identifica automaticamente o idioma preferido do usuário a partir do cabeçalho Accept-Language do navegador, podendo também consultar localStorage, cookies ou parâmetros de URL. Isso elimina a necessidade de configuração manual inicial.

3. Gerenciamento de traduções com chaves, interpolação e pluralização

3.1. Uso de chaves aninhadas e interpolação de variáveis ({{name}})

// Componente React
import { useTranslation } from 'react-i18next';

function Greeting({ userName }) {
  const { t } = useTranslation();
  return <h1>{t('welcome', { name: userName })}</h1>;
}

3.2. Pluralização inteligente com count e regras por idioma

function DaysAgo({ days }) {
  const { t } = useTranslation();
  return <span>{t('days_ago', { count: days })}</span>;
}

// Para count=1: "1 day ago" (en), "1 dia atrás" (pt), "1日前" (ja)
// Para count=5: "5 days ago" (en), "5 dias atrás" (pt), "5日前" (ja)

3.3. Contextos e gênero: adaptação para idiomas como francês e alemão

// locales/fr.json
{
  "friend": "ami",
  "friend_male": "ami",
  "friend_female": "amie"
}

// Uso com contexto
t('friend', { context: userGender }) // 'ami' ou 'amie'

4. Formatação de datas com i18next e bibliotecas complementares

4.1. Integração com Intl.DateTimeFormat para formatação nativa

function formatDate(date, locale) {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
}

// Exemplos:
// en-US: "December 31, 2024"
// pt-BR: "31 de dezembro de 2024"
// ja-JP: "2024年12月31日"

4.2. Uso de dayjs com plugins de localidade como alternativa leve

import dayjs from 'dayjs';
import 'dayjs/locale/pt';
import 'dayjs/locale/ja';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.extend(relativeTime);

function RelativeDate({ date, locale }) {
  dayjs.locale(locale);
  return <span>{dayjs(date).fromNow()}</span>;
}

// Exemplos:
// en: "3 days ago"
// pt: "há 3 dias"
// ja: "3日前"

4.3. Formatação de datas relativas (ex.: "há 3 dias") com Intl.RelativeTimeFormat

function relativeTimeAgo(date, locale) {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
  const diff = Math.floor((new Date() - new Date(date)) / (1000 * 60 * 60 * 24));

  if (diff === 0) return rtf.format(0, 'day');
  return rtf.format(-diff, 'day');
}

// en: "3 days ago"
// pt: "há 3 dias"
// ja: "3日前"

5. Personalização de formatos por região (locale-aware)

5.1. Diferenças entre en-US, pt-BR, de-DE e ja-JP no calendário

const locales = ['en-US', 'pt-BR', 'de-DE', 'ja-JP'];
const date = new Date(2024, 11, 31);

locales.forEach(locale => {
  console.log(locale, new Intl.DateTimeFormat(locale).format(date));
});

// en-US: "12/31/2024"
// pt-BR: "31/12/2024"
// de-DE: "31.12.2024"
// ja-JP: "2024/12/31"

5.2. Configuração de fuso horário e timezone no frontend

function formatWithTimezone(date, locale, timezone) {
  return new Intl.DateTimeFormat(locale, {
    timeZone: timezone,
    hour: '2-digit',
    minute: '2-digit'
  }).format(date);
}

// Exemplo: formatWithTimezone(new Date(), 'pt-BR', 'America/Sao_Paulo')

5.3. Formatação de números e moedas com Intl.NumberFormat integrado

function formatCurrency(value, locale, currency) {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency
  }).format(value);
}

// pt-BR: "R$ 1.234,56"
// en-US: "$1,234.56"
// ja-JP: "¥1,235"

6. Troca dinâmica de idioma e persistência da preferência

6.1. Implementação de seletor de idioma com i18n.changeLanguage()

import { useTranslation } from 'react-i18next';

function LanguageSwitcher() {
  const { i18n } = useTranslation();

  const changeLanguage = (lng) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div>
      <button onClick={() => changeLanguage('en')}>English</button>
      <button onClick={() => changeLanguage('pt')}>Português</button>
      <button onClick={() => changeLanguage('ja')}>日本語</button>
    </div>
  );
}

6.2. Persistência em localStorage e cookies com languageDetector

// Configuração do LanguageDetector para persistir em localStorage
const detectorOptions = {
  order: ['localStorage', 'navigator', 'htmlTag'],
  lookupLocalStorage: 'i18nextLng',
  caches: ['localStorage']
};

6.3. Atualização reativa de componentes sem perda de estado

O react-i18next garante que todos os componentes que usam o hook useTranslation sejam re-renderizados automaticamente quando o idioma muda, preservando o estado interno dos componentes.

7. Boas práticas e armadilhas comuns na internacionalização

7.1. Evitar concatenação manual de strings e hardcoding de datas

// ❌ Errado
const message = 'Olá, ' + userName + '!';

// ✅ Correto
t('greeting', { name: userName })

7.2. Testes automatizados de tradução com snapshot e falso locale

// Jest test example
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n-test';

test('renders welcome message in Portuguese', () => {
  i18n.changeLanguage('pt');
  const { getByText } = render(
    <I18nextProvider i18n={i18n}>
      <Greeting userName="Maria" />
    </I18nextProvider>
  );
  expect(getByText('Bem-vindo, Maria!')).toBeInTheDocument();
});

7.3. Performance: lazy loading de namespaces e fallback de idioma

// Carregamento sob demanda de namespaces
i18n.loadNamespaces(['dashboard', 'admin'], (err) => {
  if (!err) {
    // Namespaces carregados
  }
});

8. Exemplo prático: aplicação multi-idioma com datas localizadas

8.1. Estrutura de componentes: Header, DatePicker e Timeline

// App.js
import { Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import Header from './Header';
import Timeline from './Timeline';

function App() {
  const { i18n } = useTranslation();

  return (
    <div>
      <Header />
      <Timeline events={sampleEvents} />
    </div>
  );
}

8.2. Código completo de configuração do i18next com dayjs/locale

// i18n.js (configuração completa)
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import dayjs from 'dayjs';
import 'dayjs/locale/pt';
import 'dayjs/locale/ja';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.extend(relativeTime);

const resources = {
  en: {
    translation: {
      title: "Event Timeline",
      createEvent: "Create Event",
      daysAgo: "{{count}} day ago",
      daysAgo_plural: "{{count}} days ago"
    }
  },
  pt: {
    translation: {
      title: "Linha do Tempo",
      createEvent: "Criar Evento",
      daysAgo: "{{count}} dia atrás",
      daysAgo_plural: "{{count}} dias atrás"
    }
  },
  ja: {
    translation: {
      title: "イベントタイムライン",
      createEvent: "イベント作成",
      daysAgo: "{{count}}日前"
    }
  }
};

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: 'en',
    interpolation: { escapeValue: false }
  });

export default i18n;

8.3. Demonstração de troca entre en, pt e ja com datas reativas

// Timeline.js
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.extend(relativeTime);

function Timeline({ events }) {
  const { t, i18n } = useTranslation();

  dayjs.locale(i18n.language);

  return (
    <div>
      <h1>{t('title')}</h1>
      {events.map((event, index) => (
        <div key={index}>
          <h3>{event.title}</h3>
          <p>{dayjs(event.date).fromNow()}</p>
          <p>{new Intl.DateTimeFormat(i18n.language).format(new Date(event.date))}</p>
        </div>
      ))}
    </div>
  );
}

A internacionalização no frontend é um investimento que paga dividendos em alcance global, satisfação do usuário e conformidade cultural. Com i18next e as APIs nativas de formatação, é possível construir aplicações verdadeiramente globais sem sacrificar performance ou experiência do desenvolvedor.

Referências