Pular para o conteúdo principal

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

Internacionalização no Cursor

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
dica

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": {
"email": "[email protected]",
"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"
}
}
dica

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.

aviso

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}} عنصر"
}
dica

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>
);
}
dica

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>
);
}
aviso

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.

dica

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'); // "منذ ساعة واحدة"
dica

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.

Fluxos de Trabalho de Tradução Assistidos por IA​

O Cursor pode gerar traduções para arquivos de locale inteiros usando IA. Isso não é um substituto para tradutores humanos profissionais, mas produz primeiros rascunhos de alta qualidade que um tradutor pode revisar e refinar.

Gerando Traduções a Partir de um Arquivo Fonte​

Read my English translation file at src/locales/en/common.json and
generate equivalent translations for Spanish (es), Japanese (ja),
and Arabic (ar). Preserve all interpolation variables like {{name}},
pluralization keys, and namespace structure exactly. Output three
separate JSON files in the correct locale directories.

O Cursor lê o arquivo fonte e produz três arquivos JSON traduzidos. A IA preserva variáveis do ICU Message Format, sufixos de pluralização e estrutura JSON.

Extraindo Strings do Código Fonte​

Scan all .tsx files in my src/ directory and extract every hard-coded
user-facing string into a translation key. Create a new common.json
file with the extracted strings organized by component. Then replace
each hard-coded string in the source with a useTranslation() call.

O Cursor realiza uma refatoração em todo o codebase: identifica strings literais em JSX, gera um arquivo de tradução estruturado e substitui cada ocorrência pela chamada t() apropriada.

aviso

Traduções geradas por IA devem sempre ser revisadas por um falante nativo antes de serem enviadas para produção. A IA pode traduzir incorretamente termos dependentes de contexto, perder nuances culturais e produzir frases gramaticalmente corretas, mas não naturais. Use traduções de IA como um rascunho, não como um produto final. Marque quaisquer arquivos traduzidos por IA com um status de "precisa de revisão" em seu fluxo de trabalho de gerenciamento de tradução.

Script de Tradução em Lote​

// scripts/translate.ts
import * as fs from 'fs/promises';
import * as path from 'path';

interface TranslationFile {
[key: string]: string | TranslationFile;
}

const SOURCE_LOCALE = 'en';
const TARGET_LOCALES = ['es', 'ja', 'ar'];
const SOURCE_DIR = path.join(__dirname, '../public/locales', SOURCE_LOCALE);
const OUTPUT_BASE = path.join(__dirname, '../public/locales');

async function translateFile(
source: TranslationFile,
targetLocale: string
): Promise<TranslationFile> {
const result: TranslationFile = {};
for (const [key, value] of Object.entries(source)) {
if (typeof value === 'string') {
// Replace with actual AI translation API call
result[key] = `[${targetLocale}] ${value}`;
} else {
result[key] = await translateFile(value, targetLocale);
}
}
return result;
}

async function runBatchTranslation() {
const namespaces = await fs.readdir(SOURCE_DIR);

for (const locale of TARGET_LOCALES) {
const outputDir = path.join(OUTPUT_BASE, locale);
await fs.mkdir(outputDir, { recursive: true });

for (const nsFile of namespaces.filter((f) => f.endsWith('.json'))) {
const sourceData = JSON.parse(
await fs.readFile(path.join(SOURCE_DIR, nsFile), 'utf-8')
) as TranslationFile;

const translated = await translateFile(sourceData, locale);
const outputPath = path.join(outputDir, nsFile);
await fs.writeFile(outputPath, JSON.stringify(translated, null, 2), 'utf-8');
console.log(`Translated ${nsFile} for ${locale} -> ${outputPath}`);
}
}
console.log('Batch translation complete.');
}

runBatchTranslation().catch(console.error);
dica

Adicione o script de tradução em lote ao seu pipeline de CI. Quando um desenvolvedor adiciona novas strings em inglês, o trabalho de CI gera automaticamente rascunhos de tradução para todos os locales de destino e cria um pull request com as novas chaves marcadas como precisando de revisão. Isso mantém todos os arquivos de locale sincronizados sem coordenação manual.

Detecção de Traduções Ausentes​

Renderize este componente uma vez na raiz do seu aplicativo. Em desenvolvimento, ele registra chaves ausentes no console. Em produção, roteia relatórios para um serviço de rastreamento de erros.

// src/components/MissingTranslationLogger.tsx
import { useTranslation } from 'react-i18next';
import { useEffect, useRef } from 'react';

export function MissingTranslationLogger() {
const { i18n } = useTranslation();
const reportedKeys = useRef(new Set<string>());

useEffect(() => {
i18n.on('missingKey', (lngs: string[], ns: string, key: string) => {
const fullKey = `${ns}:${key}`;
if (reportedKeys.current.has(fullKey)) return;
reportedKeys.current.add(fullKey);

if (import.meta.env.DEV) {
console.warn(`[i18n] Missing: ${fullKey} for ${lngs.join(', ')}`);
}
// In production: reportToErrorService({ type: 'missing_translation', key: fullKey, locales: lngs });
});
}, [i18n]);

return null;
}

Resumo​

Internacionalização e localização no Cursor fornecem as ferramentas para construir aplicativos verdadeiramente globais. Seguindo os padrões deste guia, configuração do i18next ou react-intl, arquivos de tradução baseados em namespaces, pluralização com consciência de ICU, propriedades CSS lógicas para RTL, formatação com a API Intl nativa e pipelines de tradução assistidos por IA, você pode suportar qualquer idioma e locale desde o primeiro dia. Planeje sua estratégia de i18n antecipadamente, use propriedades CSS lógicas em vez de físicas, deixe a IA gerar primeiros rascunhos de tradução e sempre tenha um falante nativo revisando antes de enviar para produção.