Internationalisation (i18n) et localisation (l10n) dans Cursor : react-intl, i18next et support RTL

L'internationalisation (i18n) et la localisation (l10n) rendent votre application accessible aux utilisateurs dans différentes langues, régions et conventions culturelles. Cursor, un éditeur IDE alimenté par IA, accélère chaque phase du flux de travail i18n : extraction des chaînes traduisibles, configuration des bibliothèques de traduction, génération de fichiers de paramètres régionaux, implémentation de mises en page de droite à gauche (RTL) et automatisation de la traduction avec des pipelines alimentés par IA. Ce guide couvre react-intl, i18next, la gestion des fichiers de traduction, le support RTL, le formatage sensible aux paramètres régionaux et les flux de traduction assistés par IA.
Prérequis
Avant de commencer, assurez-vous de disposer des éléments suivants :
- Cursor (version la plus récente recommandée) comme éditeur IDE
- Node.js 18 ou supérieur
- Un projet React (Vite ou Next.js recommandé)
- Connaissance de base de JSON et des modèles de composants React
Planifiez votre stratégie i18n avant d'écrire du code UI. Rétrofitter l'i18n dans une application existante est significativement plus difficile que de l'intégrer dès le départ. Demandez à Cursor de configurer votre infrastructure de traduction dans le premier sprint pour que chaque composant soit internationalisé dès le premier jour.
Comprendre i18n vs l10n
- Internationalisation (i18n) : Le travail d'ingénierie consistant à préparer votre application à prendre en charge plusieurs langues. Cela inclut l'extraction de chaînes, l'utilisation de formateurs sensibles aux paramètres régionaux, le support des mises en page RTL et la conception de composants UI flexibles.
- Localisation (l10n) : La traduction et l'adaptation culturelle réelles du contenu pour des paramètres régionaux spécifiques, y compris la traduction de chaînes, l'adaptation des formats de date et de devise, et l'ajustement des images aux normes locales.
Cursor aide avec les deux : il génère l'infrastructure i18n et aide avec la l10n via la traduction et la génération de fichiers de paramètres régionaux alimentées par IA.
Configuration d'i18next
i18next est le framework i18n le plus populaire pour JavaScript. Son compagnon React, react-i18next, fournit des hooks et des composants pour une intégration transparente.
Installation et configuration
Demandez à Cursor de configurer i18next dans votre projet 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.
Cursor génère la configuration :
// 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;
Structure des fichiers de traduction
public/
locales/
en/
common.json
navigation.json
forms.json
errors.json
es/
common.json
...
ja/
common.json
...
ar/
common.json
...
Exemple de traductions en anglais :
// 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"
}
}
Organisez les traductions en espaces de noms (common, navigation, forms, errors) plutôt qu'en un seul fichier monolithique. Les espaces de noms vous permettent de charger paresseusement uniquement les traductions nécessaires pour la page actuelle, réduisant la taille du bundle initial. Demandez à Cursor de suggérer des limites d'espaces de noms basées sur votre structure de routes.
Ne codez jamais en dur les chaînes destinées aux utilisateurs dans vos composants. Même si vous ne supportez qu'une seule langue initialement, extraire les chaînes dans des fichiers de traduction dès le départ est un investissement de 10 minutes qui fait économiser des semaines de refactorisation plus tard. Utilisez la fonction t() pour chaque chaîne visible, y compris les libellés de boutons, les messages d'erreur et les info-bulles.
Utilisation des hooks react-i18next
Le hook useTranslation est le principal moyen d'accéder aux traductions dans les composants fonctionnels.
Utilisation de base
// 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>
);
}
Pluriel et interpolation
i18next gère automatiquement les règles de pluriel en fonction des paramètres régionaux. Les différentes langues ont différentes formes de pluriel (l'anglais en a 2, l'arabe en a 6, le japonais en a 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>
);
}
Le fichier de traduction arabe gère les six formes de pluriel :
// 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}} عنصر"
}
Demandez à Cursor de générer des règles de pluriel pour chaque paramètre régional que vous supportez. Une invite comme « Créez les clés de forme plurielle correctes pour l'arabe, le russe et le polonais pour la clé de traduction items_count, en suivant les règles de pluriel CLDR » produit des formes plurielles correctes sans que vous ayez besoin de mémoriser les règles CLDR.
Configuration de react-intl
react-intl est une bibliothèque i18n alternative de la suite FormatJS. Elle est populaire dans les applications d'entreprise en raison de sa sécurité de type stricte et de son support ICU Message Format.
Configuration du 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;
}
Utilisation des composants react-intl
react-intl fournit des composants déclaratifs pour le formatage. Le composant FormattedMessage gère ICU Message Format, tandis que FormattedNumber, FormattedDate et FormattedTime s'adaptent automatiquement aux paramètres régionaux de l'utilisateur.
// 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>
);
}
react-intl utilise ICU Message Format, qui intègre le pluriel, la sélection et le formatage directement dans la chaîne de message. Cela signifie que les traducteurs peuvent ajuster la structure d'une phrase sans modifier le code. Si vous migrez depuis de simples fichiers de traduction clé-valeur, demandez à Cursor d'expliquer la syntaxe ICU Message Format avec des exemples pratiques.
Support droite-à-gauche (RTL)
Le support RTL est essentiel pour les langues comme l'arabe, l'hébreu et le persan. Cursor vous aide à implémenter des mises en page sensibles au RTL qui fonctionnent correctement dans les deux sens.
Propriétés logiques CSS
Utilisez des propriétés logiques (margin-inline-start) au lieu de propriétés physiques (margin-left) pour que les mises en page se retournent automatiquement en mode 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); }
Composant React sensible au 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'utilisez pas text-align: left ou text-align: right dans votre CSS. Utilisez plutôt text-align: start et text-align: end. Les propriétés physiques ne se retournent pas automatiquement en mode RTL, causant un texte mal aligné et des mises en page cassées. Demandez à Cursor de scanner votre CSS pour les propriétés directionnelles physiques et de les convertir en équivalents logiques.
Testez votre mise en page RTL en ajoutant l'arabe comme paramètre régional tôt dans le développement. Retourner votre mise en page en RTL révèle les propriétés physiques codées en dur, les mises en page flex qui ne se reflètent pas, et les icônes qui devraient se retourner. Corriger ces problèmes pendant le développement est bien moins coûteux que de les découvrir après le lancement.
Composant de changement de langue
// 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>
);
}
Formatage sensible aux paramètres régionaux
Le formatage des dates, des nombres et des devises doit s'adapter aux paramètres régionaux de l'utilisateur. i18next et react-intl fournissent tous deux des utilitaires de formatage.
Utilisation directe de l'API Intl
// 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');
}
Exemples de formatage selon les paramètres régionaux
// 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'); // "منذ ساعة واحدة"
Utilisez toujours l'API Intl native pour le formatage au lieu d'écrire des formateurs personnalisés. L'API Intl utilise les données CLDR (Common Locale Data Repository) intégrées dans le moteur JavaScript, qui sont maintenues par le fournisseur de plateforme et gère automatiquement les cas limites comme les calendriers non grégoriens (ère japonaise, calendrier hégirien).
Flux de traduction assistés par IA
Cursor peut générer des traductions pour des fichiers de paramètres régionaux entiers en utilisant l'IA. Ce n'est pas un remplacement pour les traducteurs humains professionnels, mais cela produit des premiers brouillons de haute qualité qu'un traducteur peut réviser et affiner.
Génération de traductions depuis un fichier source
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.
Cursor lit le fichier source et produit trois fichiers JSON traduits. L'IA préserve les variables ICU Message Format, les suffixes de pluriel et la structure JSON.
Extraction de chaînes depuis le code source
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.
Cursor effectue une refactorisation à l'échelle de la base de code : il identifie les littéraux de chaîne dans JSX, génère un fichier de traduction structuré et remplace chaque occurrence par l'appel t() approprié.
Les traductions générées par IA devraient toujours être révisées par un locuteur natif avant d'être publiées en production. L'IA peut mal traduire des termes dépendants du contexte, manquer des nuances culturelles et produire des formulations grammaticalement correctes mais non naturelles. Utilisez les traductions IA comme brouillon, pas comme produit final. Marquez tous les fichiers traduits par IA avec un statut « à réviser » dans votre flux de gestion de traduction.
Script de traduction par lot
// 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);
Ajoutez le script de traduction par lot à votre pipeline CI. Quand un développeur ajoute de nouvelles chaînes en anglais, la tâche CI génère automatiquement des brouillons de traduction pour tous les paramètres régionaux cibles et crée une pull request avec les nouvelles clés marquées comme nécessitant une révision. Cela garde tous les fichiers de paramètres régionaux synchronisés sans coordination manuelle.
Détection des traductions manquantes
Rendezz ce composant une fois à la racine de votre application. En développement, il enregistre les clés manquantes dans la console. En production, il signale les erreurs à un service de suivi.
// 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;
}
Résumé
L'internationalisation et la localisation dans Cursor vous donnent les outils pour créer des applications véritablement mondiales. En suivant les modèles de ce guide — configuration i18next ou react-intl, fichiers de traduction basés sur des espaces de noms, pluriel sensible à ICU, propriétés CSS logiques RTL, formatage via l'API Intl native et pipelines de traduction assistés par IA — vous pouvez supporter n'importe quelle langue et n'importe quel paramètre régional dès le premier jour. Planifiez votre stratégie i18n tôt, utilisez des propriétés CSS logiques plutôt que physiques, laissez l'IA générer des brouillons de traduction, et faites toujours réviser par un locuteur natif avant la publication en production.