Internacionalização (i18n) no React e Next.js

1. Fundamentos da Internacionalização (i18n)

Internacionalização (abreviada como i18n — "i" + 18 letras + "n") é o processo de projetar software para que ele possa ser adaptado a diferentes idiomas e regiões sem alterações no código-fonte. Enquanto a localização (l10n) adapta o conteúdo para um mercado específico, a globalização (g11n) combina ambos os processos para criar produtos verdadeiramente mundiais.

Os principais desafios do i18n incluem:
- Pluralização: regras como "1 item" vs "2 items" variam entre idiomas
- Formatação de datas e moedas: MM/DD/YYYY nos EUA vs DD/MM/YYYY no Brasil
- Direção de texto: idiomas como árabe e hebraico usam RTL (right-to-left)
- Gênero e contexto: em francês, "você" muda conforme o gênero do interlocutor

2. Configuração Inicial com Next.js (App Router)

O ecossistema Next.js oferece múltiplas bibliotecas para i18n. Para este artigo, usaremos next-intl, que se integra nativamente ao App Router e oferece performance otimizada.

Instalação e estrutura de pastas

npm install next-intl

Estrutura de diretórios recomendada:

src/
  messages/
    en.json
    pt-BR.json
    es.json
  app/
    [locale]/
      page.js
      layout.js
  i18n.js
middleware.js
next.config.js

Configuração do middleware

// middleware.js
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  locales: ['en', 'pt-BR', 'es'],
  defaultLocale: 'en'
});

export const config = {
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};

Configuração do next.config.js

// next.config.js
const withNextIntl = require('next-intl/plugin')();

module.exports = withNextIntl({
  // outras configurações do Next.js
});

3. Gerenciamento de Traduções com Arquivos JSON

Os arquivos de tradução seguem uma estrutura de chaves aninhadas. O carregamento assíncrono otimiza o bundle, carregando apenas o idioma ativo.

// src/messages/pt-BR.json
{
  "home": {
    "title": "Bem-vindo ao nosso site",
    "description": "Este é um exemplo de internacionalização",
    "items": "{count} {count, plural, one {item} other {itens}} encontrados"
  },
  "common": {
    "language": "Idioma",
    "save": "Salvar",
    "cancel": "Cancelar"
  }
}
// src/messages/en.json
{
  "home": {
    "title": "Welcome to our website",
    "description": "This is an internationalization example",
    "items": "{count} {count, plural, one {item} other {items}} found"
  },
  "common": {
    "language": "Language",
    "save": "Save",
    "cancel": "Cancel"
  }
}

Boas práticas:
- Use namespaces para organizar por contexto (ex: home, checkout, errors)
- Evite chaves muito longas; prefira hierarquia de até 3 níveis
- Mantenha arquivos de tradução sincronizados com scripts de validação

4. Componentes e Hooks para Tradução no React

O next-intl fornece o hook useTranslations() e o componente NextIntlClientProvider.

Configuração do provedor e layout

// src/app/[locale]/layout.js
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';

export default async function LocaleLayout({ children, params: { locale } }) {
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Uso do hook em componentes

// src/app/[locale]/page.js
import { useTranslations } from 'next-intl';
import { formatNumber, formatDate } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('home');

  const price = 1234.56;
  const date = new Date('2024-12-25');

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
      <p>{t('items', { count: 3 })}</p>

      <p>Preço: {formatNumber(price, { style: 'currency', currency: 'BRL' })}</p>
      <p>Data: {formatDate(date, { dateStyle: 'full' })}</p>
    </div>
  );
}

Interpolação de variáveis e pluralização

// Componente de carrinho
import { useTranslations } from 'next-intl';

export default function CartSummary({ items }) {
  const t = useTranslations('cart');

  return (
    <div>
      <h2>{t('title')}</h2>
      <p>
        {t('items_count', { 
          count: items.length,
          total: items.length 
        })}
      </p>
      {/* Exemplo de pluralização no JSON: 
          "items_count": "Você tem {count} {count, plural, one {item} other {itens}} no carrinho" 
      */}
    </div>
  );
}

5. Roteamento e Mudança de Idioma no Next.js

Seletor de idioma com persistência

// src/components/LanguageSwitcher.js
'use client';

import { useLocale } from 'next-intl';
import { useRouter, usePathname } from 'next/navigation';
import { setCookie } from 'cookies-next';

export default function LanguageSwitcher() {
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  const handleChange = (newLocale) => {
    setCookie('NEXT_LOCALE', newLocale, { maxAge: 60 * 60 * 24 * 365 });
    router.push(`/${newLocale}${pathname}`);
  };

  return (
    <select 
      value={locale} 
      onChange={(e) => handleChange(e.target.value)}
    >
      <option value="en">English</option>
      <option value="pt-BR">Português (Brasil)</option>
      <option value="es">Español</option>
    </select>
  );
}

Redirecionamento automático baseado no navegador

// middleware.js (versão estendida)
import createMiddleware from 'next-intl/middleware';
import { NextResponse } from 'next/server';

const i18nMiddleware = createMiddleware({
  locales: ['en', 'pt-BR', 'es'],
  defaultLocale: 'en',
  localeDetection: true
});

export default function middleware(request) {
  const { pathname } = request.nextUrl;
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;

  // Se o usuário já escolheu um idioma, respeita a escolha
  if (cookieLocale && !pathname.startsWith(`/${cookieLocale}`)) {
    return NextResponse.redirect(new URL(`/${cookieLocale}${pathname}`, request.url));
  }

  return i18nMiddleware(request);
}

6. Internacionalização de Metadados e SEO

Metadados dinâmicos por locale

// src/app/[locale]/layout.js (versão completa)
import { getTranslations } from 'next-intl/server';

export async function generateMetadata({ params: { locale } }) {
  const t = await getTranslations({ locale, namespace: 'metadata' });

  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      languages: {
        'en': '/en',
        'pt-BR': '/pt-BR',
        'es': '/es'
      }
    }
  };
}

Sitemap com alternativas de idioma

// src/app/sitemap.js
import { locales } from '../i18n';

export default async function sitemap() {
  const baseUrl = 'https://seusite.com';

  const entries = locales.flatMap(locale => [
    {
      url: `${baseUrl}/${locale}`,
      lastModified: new Date(),
      alternates: {
        languages: Object.fromEntries(
          locales.map(l => [l, `${baseUrl}/${l}`])
        )
      }
    }
  ]);

  return entries;
}

7. Testes e Ferramentas Avançadas

Testes unitários com Jest e Testing Library

// __tests__/HomePage.test.js
import { render, screen } from '@testing-library/react';
import { NextIntlClientProvider } from 'next-intl';
import HomePage from '../src/app/[locale]/page';

const messages = {
  home: {
    title: 'Bem-vindo',
    description: 'Descrição de teste',
    items: '{count} itens'
  }
};

describe('HomePage i18n', () => {
  it('renderiza título traduzido', () => {
    render(
      <NextIntlClientProvider locale="pt-BR" messages={messages}>
        <HomePage />
      </NextIntlClientProvider>
    );

    expect(screen.getByText('Bem-vindo')).toBeInTheDocument();
  });

  it('renderiza mensagem de itens com pluralização', () => {
    render(
      <NextIntlClientProvider locale="pt-BR" messages={messages}>
        <HomePage />
      </NextIntlClientProvider>
    );

    expect(screen.getByText('3 itens')).toBeInTheDocument();
  });
});

Ferramentas de tradução colaborativa

Para projetos maiores, ferramentas como Crowdin e Lokalise oferecem:
- Interface visual para tradutores
- Integração CI/CD para atualização automática de arquivos JSON
- Controle de versão e aprovação de traduções
- Detecção de chaves ausentes

Debugging de chaves ausentes

// next.config.js (com configuração de debug)
const withNextIntl = require('next-intl/plugin')();

module.exports = withNextIntl({
  experimental: {
    // Ativa logs de chaves ausentes em desenvolvimento
    missingKeys: process.env.NODE_ENV === 'development' ? 'warn' : 'ignore'
  }
});

A internacionalização é um investimento que paga dividendos à medida que sua aplicação alcança novos mercados. Com Next.js e next-intl, você obtém uma base sólida para construir experiências verdadeiramente globais, mantendo performance e SEO otimizados.

Referências