Internacionalização (i18n) e Localização (l10n) no Cursor: react-intl, i18next e Suporte RTL

Internacionalização (i18n) e localização (l10n) tornam seu aplicativo acessível para usuários em diferentes idiomas, regiões e convenções culturais. Cursor, um editor IDE alimentado por IA, acelera cada fase do fluxo de trabalho de i18n: extração de strings traduzíveis, configuração de bibliotecas de tradução, geração de arquivos de locale, implementação de layouts da direita para a esquerda (RTL) e automação de tradução com pipelines alimentados por IA. Este guia aborda react-intl, i18next, gerenciamento de arquivos de tradução, suporte RTL, formatação com consciência de locale e fluxos de trabalho de tradução assistidos por IA.
Pré-requisitos
Antes de começar, certifique-se de que você tem o seguinte:
- Cursor (versão mais recente recomendada) como seu editor IDE
- Node.js 18 ou superior
- Um projeto React (Vite ou Next.js recomendado)
- Familiaridade básica com JSON e padrões de componentes React
Planeje sua estratégia de i18n antes de escrever qualquer código de UI. Adaptar i18n a um aplicativo existente é significativamente mais difícil do que construí-lo desde o início. Peça ao Cursor para configurar sua infraestrutura de tradução na primeira sprint para que cada componente seja internacionalizado desde o primeiro dia.
Entendendo i18n vs l10n
- Internacionalização (i18n): O trabalho de engenharia de preparar seu aplicativo para suportar múltiplos idiomas. Isso inclui extrair strings, usar formatadores com consciência de locale, suportar layouts RTL e projetar componentes de UI flexíveis.
- Localização (l10n): A tradução e adaptação cultural real do conteúdo para um locale específico, incluindo tradução de strings, adaptação de formatos de data e moeda e ajuste de imagens para normas locais.
O Cursor ajuda com ambos: ele gera a infraestrutura de i18n e auxilia com l10n através de tradução alimentada por IA e geração de arquivos de locale.
Configurando o i18next
O i18next é a estrutura de i18n mais popular para JavaScript. Seu companheiro React, react-i18next, fornece hooks e componentes para integração perfeita.
Instalação e Configuração
Peça ao Cursor para configurar o i18next em seu projeto React:
Set up i18next with react-i18next in my Vite + React + TypeScript project.
Support English (en), Spanish (es), Japanese (ja), and Arabic (ar).
Include a language switcher component, lazy-loaded translation
namespaces, and locale-aware date formatting. Configure the
detector to remember the user's choice in localStorage.
O Cursor gera a configuração:
// src/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
export const supportedLanguages = [
{ code: 'en', name: 'English', dir: 'ltr' as const },
{ code: 'es', name: 'Espanol', dir: 'ltr' as const },
{ code: 'ja', name: '日本語', dir: 'ltr' as const },
{ code: 'ar', name: 'العربية', dir: 'rtl' as const },
];
export const defaultLanguage = 'en';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: defaultLanguage,
supportedLngs: supportedLanguages.map((l) => l.code),
ns: ['common', 'navigation', 'forms', 'errors'],
defaultNS: 'common',
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage'],
},
interpolation: { escapeValue: false },
react: { useSuspense: true, bindI18n: 'languageChanged loaded' },
});
export default i18n;
Estrutura de Arquivos de Tradução
public/
locales/
en/
common.json
navigation.json
forms.json
errors.json
es/
common.json
...
ja/
common.json
...
ar/
common.json
...
Exemplo de traduções em inglês:
// public/locales/en/common.json
{
"app": {
"title": "My Application",
"tagline": "Build better, ship faster"
},
"welcome": "Welcome, {{name}}!",
"items_count": "You have {{count}} item",
"items_count_other": "You have {{count}} items",
"lastUpdated": "Last updated: {{date}}"
}
// public/locales/en/forms.json
{
"labels": {
"email": "Email Address",
"password": "Password",
"firstName": "First Name",
"lastName": "Last Name"
},
"placeholders": {
"password": "Enter your password"
},
"buttons": {
"submit": "Submit",
"cancel": "Cancel"
},
"validation": {
"emailRequired": "Email is required",
"emailInvalid": "Please enter a valid email address",
"passwordMinLength": "Password must be at least {{min}} characters"
}
}
Organize as traduções em namespaces (common, navigation, forms, errors) em vez de um único arquivo monolítico. Namespaces permitem que você carregue preguiçosamente apenas as traduções necessárias para a página atual, reduzindo o tamanho do bundle inicial. Peça ao Cursor para sugerir limites de namespace com base na sua estrutura de rotas.
Nunca embuta strings visíveis ao usuário diretamente em seus componentes. Mesmo que você suporte apenas um idioma inicialmente, extrair strings para arquivos de tradução desde o início é um investimento de 10 minutos que economiza semanas de refatoração posterior. Use a função t() para cada string visível, incluindo rótulos de botões, mensagens de erro e dicas de ferramentas.
Usando Hooks do react-i18next
O hook useTranslation é a principal forma de acessar traduções em componentes de função.
Uso Básico
// src/components/LoginForm.tsx
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
export function LoginForm() {
const { t } = useTranslation(['forms', 'errors']);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const newErrors: Record<string, string> = {};
if (!email) {
newErrors.email = t('forms:validation.emailRequired');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
newErrors.email = t('forms:validation.emailInvalid');
}
if (password.length < 8) {
newErrors.password = t('forms:validation.passwordMinLength', { min: 8 });
}
setErrors(newErrors);
}
return (
<form onSubmit={handleSubmit} className="max-w-md space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
{t('forms:labels.email')}
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('forms:placeholders.email')}
className="mt-1 w-full rounded border px-3 py-2"
aria-invalid={!!errors.email}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600" role="alert">
{errors.email}
</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
{t('forms:labels.password')}
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t('forms:placeholders.password')}
className="mt-1 w-full rounded border px-3 py-2"
aria-invalid={!!errors.password}
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600" role="alert">
{errors.password}
</p>
)}
</div>
<button type="submit" className="w-full rounded bg-blue-600 px-4 py-2 text-white">
{t('forms:buttons.submit')}
</button>
</form>
);
}
Pluralização e Interpolação
O i18next lida com regras de pluralização automaticamente com base no locale. Diferentes idiomas têm diferentes formas de plural (inglês tem 2, árabe tem 6, japonês tem 1).
// src/components/ShoppingCart.tsx
import { useTranslation } from 'react-i18next';
export function ShoppingCart({ itemCount }: { itemCount: number }) {
const { t } = useTranslation('common');
return (
<div className="p-4">
<h2 className="text-lg font-bold">{t('shoppingCart')}</h2>
{/* i18next automatically selects singular or plural form */}
<p>{t('items_count', { count: itemCount })}</p>
</div>
);
}
O arquivo de tradução em árabe lida com todas as seis formas de plural:
// public/locales/ar/common.json (Arabic has 6 plural forms per CLDR rules)
{
"items_count_0": "لا توجد عناصر",
"items_count_1": "لديك عنصر واحد",
"items_count_2": "لديك عنصران",
"items_count_3": "لديك {{count}} عناصر",
"items_count_4": "لديك {{count}} عنصرًا",
"items_count_5": "لديك {{count}} عنصر"
}
Peça ao Cursor para gerar regras de pluralização para cada locale que você suporta. Um prompt como "Create the correct plural form keys for Arabic, Russian, and Polish for the items_count translation key, following CLDR plural rules" produz formas de plural corretas para o locale sem que você precise memorizar as regras CLDR.
Configurando o react-intl
O react-intl é uma biblioteca de i18n alternativa do conjunto FormatJS. É popular em aplicativos empresariais devido à sua rigorosa segurança de tipos e suporte ao ICU Message Format.
Configuração do Provider
// src/i18n/IntlProvider.tsx
import { IntlProvider } from 'react-intl';
import { useState, useEffect, ReactNode, createContext, useContext } from 'react';
export type Locale = 'en' | 'es' | 'ja' | 'ar';
const messages: Record<Locale, () => Promise<Record<string, string>>> = {
en: () => import('../locales/en.json').then((m) => m.default),
es: () => import('../locales/es.json').then((m) => m.default),
ja: () => import('../locales/ja.json').then((m) => m.default),
ar: () => import('../locales/ar.json').then((m) => m.default),
};
export function IntlAppProvider({ children }: { children: ReactNode }) {
const storedLocale = localStorage.getItem('locale') as Locale | null;
const browserLocale = navigator.language.split('-')[0] as Locale;
const [locale, setLocale] = useState<Locale>(storedLocale || browserLocale || 'en');
const [localeMessages, setLocaleMessages] = useState<Record<string, string>>({});
useEffect(() => {
messages[locale]().then((msgs) => {
setLocaleMessages(msgs);
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr';
document.documentElement.lang = locale;
localStorage.setItem('locale', locale);
});
}, [locale]);
return (
<IntlProvider
locale={locale}
messages={localeMessages}
defaultLocale="en"
onError={(err) => import.meta.env.DEV && console.warn('[Intl]', err.message)}
>
<LanguageContext.Provider value={{ locale, changeLocale: setLocale }}>
{children}
</LanguageContext.Provider>
</IntlProvider>
);
}
interface LanguageContextType { locale: Locale; changeLocale: (locale: Locale) => void; }
const LanguageContext = createContext<LanguageContextType | null>(null);
export function useLanguage() {
const ctx = useContext(LanguageContext);
if (!ctx) throw new Error('useLanguage must be used within IntlAppProvider');
return ctx;
}
Usando Componentes do react-intl
O react-intl fornece componentes declarativos para formatação. O componente FormattedMessage lida com o ICU Message Format, enquanto FormattedNumber, FormattedDate e FormattedTime se adaptam ao locale do usuário automaticamente.
// src/components/ProductCard.tsx
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
import { useLanguage } from '../i18n/IntlProvider';
export function ProductCard({ product }: { product: Product }) {
const { locale } = useLanguage();
return (
<div className="rounded-lg border p-4">
<h3 className="text-lg font-bold">{product.name}</h3>
{/* Currency adapts to locale automatically */}
<p className="text-xl text-green-600">
<FormattedNumber
value={product.price}
style="currency"
currency={locale === 'ja' ? 'JPY' : locale === 'ar' ? 'SAR' : 'USD'}
/>
</p>
<p className="text-sm text-gray-500">
<FormattedMessage
id="product.addedOn"
defaultMessage="Added on {date}"
values={{
date: <FormattedDate value={new Date(product.addedAt)} year="numeric" month="long" day="numeric" />,
}}
/>
</p>
<div className="mt-2">
<FormattedMessage
id="product.reviews"
defaultMessage="{count, plural, one {# review} other {# reviews}}"
values={{ count: product.reviewCount }}
/>
</div>
</div>
);
}
O react-intl usa o ICU Message Format, que incorpora pluralização, seleção e formatação diretamente na string de mensagem. Isso significa que os tradutores podem ajustar a estrutura de uma frase sem modificar o código. Peça ao Cursor para explicar a sintaxe do ICU Message Format com exemplos práticos se você estiver migrando de arquivos de tradução simples de chave-valor.
Suporte da Direita para a Esquerda (RTL)
O suporte RTL é essencial para idiomas como árabe, hebraico e persa. O Cursor ajuda você a implementar layouts com consciência de RTL que funcionam corretamente em ambas as direções.
Propriedades Lógicas de CSS
Use propriedades lógicas (margin-inline-start) em vez de propriedades físicas (margin-left) para que os layouts sejam invertidos automaticamente no modo RTL.
/* src/styles/rtl.css */
.card {
padding-inline-start: 1rem;
padding-inline-end: 1rem;
border-inline-start: 3px solid var(--accent);
}
.flex-container { display: flex; flex-direction: row; gap: 0.5rem; }
.nav-bar { inset-inline-start: 0; inset-inline-end: 0; }
/* Flip directional icons in RTL */
[dir="rtl"] .icon-rtl-flip { transform: scaleX(-1); }
Componente React com Consciência de RTL
// src/components/RTLNavigation.tsx
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
export function RTLNavigation({ items }: { items: NavItem[] }) {
const { i18n } = useTranslation();
const isRTL = i18n.dir() === 'rtl';
const flipIcons = useMemo(() => new Set(['arrow', 'chevron', 'back', 'forward']), []);
return (
<nav
className="flex items-center gap-4 px-4 py-2"
style={{ direction: isRTL ? 'rtl' : 'ltr' }}
aria-label="Main navigation"
>
{items.map((item) => (
<a key={item.id} href={item.href} className="flex items-center gap-2 rounded px-3 py-1 hover:bg-gray-100">
{item.icon && (
<span
className="inline-block"
style={{ transform: isRTL && flipIcons.has(item.icon) ? 'scaleX(-1)' : 'scaleX(1)' }}
>
<Icon name={item.icon} />
</span>
)}
<span>{i18n.t(item.labelKey)}</span>
</a>
))}
</nav>
);
}
Não use text-align: left ou text-align: right em seu CSS. Use text-align: start e text-align: end em vez disso. Propriedades físicas não são invertidas automaticamente no modo RTL, causando texto desalinhado e layouts quebrados. Peça ao Cursor para escanear seu CSS em busca de propriedades direcionais físicas e convertê-las para equivalentes lógicos.
Teste seu layout RTL adicionando árabe como um locale no início do desenvolvimento. Inverter seu layout para RTL revela propriedades físicas embutidas diretamente no código, layouts flex que não são espelhados e ícones que deveriam ser invertidos. Corrigir esses problemas durante o desenvolvimento é muito mais barato do que descobri-los após o lançamento.
Componente de Seletor de Idioma
// src/components/LanguageSwitcher.tsx
import { useTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
export function LanguageSwitcher() {
const { i18n } = useTranslation();
const currentLanguage = supportedLanguages.find((l) => l.code === i18n.language) || supportedLanguages[0];
function changeLanguage(code: string) {
i18n.changeLanguage(code);
const lang = supportedLanguages.find((l) => l.code === code);
document.documentElement.dir = lang?.dir || 'ltr';
document.documentElement.lang = code;
}
return (
<div className="flex items-center gap-2">
<select
id="language-select"
value={currentLanguage.code}
onChange={(e) => changeLanguage(e.target.value)}
className="rounded border px-2 py-1 text-sm"
aria-label="Select language"
>
{supportedLanguages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
</div>
);
}
Formatação com Consciência de Locale
A formatação de data, número e moeda deve se adaptar ao locale do usuário. Tanto o i18next quanto o react-intl fornecem utilitários de formatação.
Usando a API Intl Diretamente
// src/lib/locale-formatters.ts
export function formatDate(date: Date, locale: string, options?: Intl.DateTimeFormatOptions): string {
return new Intl.DateTimeFormat(locale, options || { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
}
export function formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string {
return new Intl.NumberFormat(locale, options).format(value);
}
export function formatCurrency(value: number, locale: string, currency: string): string {
return new Intl.NumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: 2 }).format(value);
}
export function formatRelativeTime(date: Date, locale: string): string {
const diff = (date.getTime() - Date.now()) / 1000;
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
['year', 60 * 60 * 24 * 365], ['month', 60 * 60 * 24 * 30],
['day', 60 * 60 * 24], ['hour', 60 * 60], ['minute', 60], ['second', 1],
];
for (const [unit, secs] of units) {
if (Math.abs(diff) >= secs) return rtf.format(Math.round(diff / secs), unit);
}
return rtf.format(0, 'second');
}
Exemplos de Formatação Entre Locales
// Dates adapt to locale conventions automatically:
formatDate(new Date('2026-09-02'), 'en-US'); // "September 2, 2026"
formatDate(new Date('2026-09-02'), 'es-ES'); // "2 de septiembre de 2026"
formatDate(new Date('2026-09-02'), 'ja-JP'); // "2026年9月2日"
formatDate(new Date('2026-09-02'), 'ar-SA'); // "٢ سبتمبر ٢٠٢٦"
// Currency formatting respects local symbols and decimal rules:
formatCurrency(1234.56, 'en-US', 'USD'); // "$1,234.56"
formatCurrency(1234.56, 'ja-JP', 'JPY'); // "¥1,235" (no decimals for JPY)
formatCurrency(1234.56, 'ar-SA', 'SAR'); // "١٬٢٣٤٫٥٦ ر.س"
// Relative time uses natural language per locale:
formatRelativeTime(new Date(Date.now() - 3600000), 'en'); // "1 hour ago"
formatRelativeTime(new Date(Date.now() - 3600000), 'ja'); // "1時間前"
formatRelativeTime(new Date(Date.now() - 3600000), 'ar'); // "منذ ساعة واحدة"
Sempre use a API nativa Intl para formatação em vez de escrever formatadores personalizados. A API Intl usa os dados CLDR (Common Locale Data Repository) incorporados ao motor JavaScript, que são mantidos pelo fornecedor da plataforma e lidam com casos especiais como calendários não-gregorianos (era japonesa, calendário islâmico) automaticamente.