Boas Práticas de Internacionalização (i18n) em React e Next.js em 2025
Em um mundo digital cada vez mais conectado, desenvolver aplicações web que atendam a usuários globais não é mais um diferencial, mas uma necessidade. A internacionalização (i18n) tornou-se um componente crítico no desenvolvimento de aplicações modernas, permitindo que seu produto alcance públicos diversos, independentemente de idioma, região ou preferências culturais.
Neste artigo, exploraremos as melhores práticas, ferramentas e estratégias para implementar internacionalização eficaz em aplicações React e Next.js em 2025, fornecendo exemplos práticos e soluções para desafios comuns.
Fundamentos da Internacionalização
O que é i18n e por que é importante?
O termo “i18n” é uma abreviação para “internacionalização” (a letra “i” seguida por 18 letras e terminando com “n”). É o processo de projetar e desenvolver aplicações de software que podem ser adaptadas para diferentes idiomas e regiões sem alterações de engenharia ou código.
Em 2025, a importância da i18n é amplificada por vários fatores:
- Alcance global: Aplicações com suporte multilíngue podem atingir mercados internacionais e expandir sua base de usuários.
- Experiência do usuário: Usuários se sentem mais confortáveis e engajados quando interagem com aplicações em seu idioma nativo.
- Conformidade regulatória: Muitos países têm requisitos legais para sistemas digitais oferecerem suporte a idiomas locais.
- Vantagem competitiva: Aplicações bem internacionalizadas se destacam em mercados globais competitivos.
Diferença entre i18n, l10n e g11n
É importante compreender a distinção entre termos frequentemente usados neste domínio:
- Internacionalização (i18n): Processo de design e desenvolvimento de um produto para que possa ser adaptado a diferentes idiomas e regiões.
- Localização (l10n): Processo de adaptar um produto internacionalizado para um local específico ou mercado, incluindo tradução de textos e adaptação de elementos culturais.
- Globalização (g11n): Estratégia de negócios que abrange tanto i18n quanto l10n, considerando todos os aspectos de levar um produto a mercados globais.
Bibliotecas e Ferramentas Modernas para i18n em React
Ecossistema de i18n em 2025
O ecossistema de internacionalização para React evoluiu significativamente nos últimos anos. Aqui estão as bibliotecas mais populares e avançadas em 2025:
1. React-i18next
O react-i18next
continua sendo uma das soluções mais robustas e populares, evoluindo para atender às necessidades modernas:
// Configuração básica do react-i18next em 2025
import i18n from 'i18next';
import { initReactI18next, useTranslation } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
i18n
// Carregamento sob demanda de traduções
.use(Backend)
// Detecção automática de idioma
.use(LanguageDetector)
// Integração com React
.use(initReactI18next)
.init({
fallbackLng: 'pt-BR',
supportedLngs: ['pt-BR', 'en-US', 'es', 'fr', 'zh-CN'],
// Novo em 2025: Detecção avançada de idioma com preferências de usuário
detection: {
order: ['localStorage', 'navigator', 'querystring', 'cookie'],
caches: ['localStorage'],
cookieExpirationDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
lookupQuerystring: 'lng',
lookupCookie: 'i18n',
},
// Novo em 2025: Cache inteligente com estratégia adaptativa
backend: {
loadPath: '/locales//.json',
requestOptions: {
cache: 'smart-default', // Nova estratégia de cache adaptativa
},
},
interpolation: {
escapeValue: false, // React já escapa por padrão
format: function(value, format, lng) {
// Novo suporte para formatação avançada
if (format === 'uppercase') return value.toUpperCase();
if (format === 'currency') return new Intl.NumberFormat(lng, { style: 'currency', currency: 'BRL' }).format(value);
return value;
}
},
// Novo em 2025: Análise de uso de traduções para otimização
telemetry: {
enabled: process.env.NODE_ENV === 'development',
endpoint: '/api/i18n-telemetry',
sampleRate: 0.1
}
});
export default i18n;
2. Formato ICU com react-intl
O formato ICU (International Components for Unicode) tornou-se o padrão predominante para manipulação de textos internacionalizados:
// Exemplo usando react-intl com sintaxe ICU moderna
import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
function ProductDetails({ product, inventory, lastUpdated }) {
const intl = useIntl();
return (
<div className="product-card">
<h2>{product.name}</h2>
{/* Pluralização avançada */}
<FormattedMessage
id="product.inventory"
defaultMessage="{inventory, plural, =0 {Fora de estoque} one {Última unidade disponível!} other {# unidades em estoque}}"
values=
/>
{/* Formatação de data relativa */}
<FormattedMessage
id="product.lastUpdated"
defaultMessage="Atualizado {lastUpdated, relativeTime, style=long}"
values=
/>
{/* Formatação de valores variáveis dependentes de idioma */}
<p>
{intl.formatMessage(
{
id: 'product.price',
defaultMessage: 'Preço: {price, number, currency}'
},
{ price: product.price }
)}
</p>
{/* Formatação condicional com seleção */}
<FormattedMessage
id="product.status"
defaultMessage="{status, select, new {Novo} sale {Promoção} limited {Edição limitada} other {Regular}}"
values={{ status: product.status }}
/>
</div>
);
}
3. LinguiJS: Uma alternativa poderosa
LinguiJS ganhou popularidade significativa por sua simplicidade e desempenho:
// Exemplo com Lingui v5 (versão 2025)
import React from 'react';
import { Trans, Plural, t } from '@lingui/macro';
function ShoppingCart({ items, totalPrice, lastUpdated }) {
return (
<div className="shopping-cart">
<h2><Trans id="cart.title">Seu Carrinho</Trans></h2>
<Plural
value={items.length}
zero={<Trans id="cart.empty">Seu carrinho está vazio</Trans>}
one={<Trans id="cart.oneItem">1 item no carrinho</Trans>}
other={<Trans id="cart.items">{items.length} itens no carrinho</Trans>}
/>
{items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name}</span>
<span>{t({
id: 'cart.item.price',
message: 'Preço: {price, number, currency}',
values: { price: item.price }
})}</span>
</div>
))}
<div className="cart-footer">
<div className="total">
<Trans id="cart.total" values={{ total: totalPrice }}>
Total: {totalPrice, number, currency}
</Trans>
</div>
<div className="updated">
<Trans id="cart.updated" values={{ date: lastUpdated }}>
Atualizado em {lastUpdated, date, long}
</Trans>
</div>
</div>
</div>
);
}
Implementação de i18n em Next.js
Next.js se consolidou como um dos frameworks React mais populares, e suas capacidades de internacionalização evoluíram consideravelmente em 2025.
Configuração Moderna de i18n no Next.js
A abordagem integrada do Next.js para i18n se tornou mais robusta:
// next.config.js em 2025
/** @type {import('next').NextConfig} */
const nextConfig = {
i18n: {
// Idiomas suportados
locales: ['pt-BR', 'en-US', 'es', 'fr', 'zh-CN'],
// Idioma padrão
defaultLocale: 'pt-BR',
// Novos recursos em 2025
localeDetection: true, // Detecção automática de idioma
automaticLocalePrefix: true, // Novo em 2025 - prefixos de URL simplificados
// Domínios específicos por idioma (para SEO otimizado)
domains: [
{
domain: 'meuapp.com.br',
defaultLocale: 'pt-BR',
},
{
domain: 'myapp.com',
defaultLocale: 'en-US',
},
{
domain: 'miapp.es',
defaultLocale: 'es',
},
],
// Novo em 2025: estratégia de fallback para conteúdo parcialmente traduzido
fallbackStrategy: 'partial',
// Padrões de URL que não devem ser traduzidos
excludeFromTranslation: [
'/api/*',
'/admin/*',
'/static/*',
],
},
// Outras configurações do Next.js
};
module.exports = nextConfig;
Utilizando Traduções em Componentes Server e Client
O Next.js App Router introduziu uma clara separação entre componentes de servidor e cliente, que requer abordagens específicas para i18n:
// Exemplo para componentes Server no Next.js App Router
// Em /app/[lang]/layout.tsx
import { Locale } from '@/i18n/config';
import { getTranslations } from '@/i18n/server';
export default async function RootLayout({
children,
params: { lang }
}: {
children: React.ReactNode;
params: { lang: Locale };
}) {
// Obter traduções no servidor
const { t } = await getTranslations(lang, 'common');
return (
<html lang={lang}>
<body>
<header>
<h1>{t('site.title')}</h1>
<nav>
<ul>
<li>{t('nav.home')}</li>
<li>{t('nav.products')}</li>
<li>{t('nav.contact')}</li>
</ul>
</nav>
</header>
<main>{children}</main>
<footer>{t('site.footer')}</footer>
</body>
</html>
);
}
// Para componentes Client
'use client';
import { useTranslation } from '@/i18n/client';
import { useParams } from 'next/navigation';
export default function LanguageSwitcher() {
const params = useParams();
const lang = params.lang as Locale;
const { t } = useTranslation(lang, 'common');
return (
<div className="language-selector">
<p>{t('language.select')}</p>
<select>
<option value="pt-BR">{t('language.portuguese')}</option>
<option value="en-US">{t('language.english')}</option>
<option value="es">{t('language.spanish')}</option>
</select>
</div>
);
}
Implementação de uma solução completa
Para mostrar como tudo se junta, aqui está uma implementação mais completa no App Router do Next.js:
// Em /i18n/config.ts
export const defaultLocale = 'pt-BR';
export const locales = ['pt-BR', 'en-US', 'es', 'fr', 'zh-CN'] as const;
export type Locale = typeof locales[number];
// Em /i18n/server.ts
import { createInstance } from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next/initReactI18next';
import { Locale, defaultLocale } from './config';
export async function getTranslations(locale: Locale, namespace: string) {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((language: string, ns: string) =>
import(`./locales/${language}/${ns}.json`)))
.init({
lng: locale,
fallbackLng: defaultLocale,
supportedLngs: locales,
defaultNS: 'common',
ns: namespace,
fallbackNS: 'common',
});
return {
t: i18nInstance.getFixedT(locale, namespace),
i18n: i18nInstance
};
}
// Em /middleware.ts - para roteamento e redirecionamento baseado em idioma
import { NextRequest, NextResponse } from 'next/server';
import { match as matchLocale } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { locales, defaultLocale } from './i18n/config';
function getLocale(request: NextRequest): string {
// Simulando cabeçalhos para o negotiator
const headers = {
'accept-language': request.headers.get('accept-language') || defaultLocale
};
const languages = new Negotiator({ headers }).languages();
// Usar @formatjs/intl-localematcher para escolher o melhor idioma
const locale = matchLocale(languages, locales, defaultLocale);
return locale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Verificar se a URL já inclui uma localidade
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return NextResponse.next();
// Redirecionar se a localidade não estiver na URL
const locale = getLocale(request);
const newUrl = new URL(`/${locale}${pathname}`, request.url);
return NextResponse.redirect(newUrl);
}
export const config = {
matcher: [
// Excluir arquivos estáticos e API
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Desenvolvimento
Adicione o conteúdo principal aqui.
Conclusão
Finalize o post com suas conclusões.