跳到主要内容

在 Cursor 中进行国际化(i18n)与本地化(l10n):react-intl、i18next 与 RTL 支持

在 Cursor 中进行国际化

国际化(i18n)和本地化(l10n)使你的应用能够面向不同语言、地区和文化习惯的用户。Cursor 是一款 AI 驱动的 IDE 编辑器,加速 i18n 工作流的每个阶段:提取可翻译字符串、配置翻译库、生成语言环境文件、实现从右到左(RTL)布局以及使用 AI 驱动的流水线自动化翻译。本指南涵盖 react-intl、i18next、翻译文件管理、RTL 支持、本地化感知格式化以及 AI 辅助的翻译工作流。

前提条件​

在开始之前,请确保你具备以下条件:

  • Cursor(建议使用最新版本)作为你的 IDE 编辑器
  • Node.js 18 或更高版本
  • 一个 React 项目(建议使用 Vite 或 Next.js)
  • 对 JSON 和 React 组件模式有基本了解
提示

在编写任何 UI 代码之前规划你的 i18n 策略。将 i18n 改造到现有应用中比从一开始就内建要困难得多。让 Cursor 在第一个迭代中设置你的翻译基础设施,这样每个组件从第一天起就是国际化的。

理解 i18n 与 l10n 的区别​

  • 国际化(i18n):准备应用以支持多种语言的工程工作。包括提取字符串、使用本地化感知格式化器、支持 RTL 布局以及设计灵活的 UI 组件。
  • 本地化(l10n):针对特定语言环境的实际内容翻译和文化适配,包括翻译字符串、适配日期和货币格式以及为当地规范调整图像。

Cursor 在两方面都能帮助你:它生成 i18n 基础设施,并通过 AI 驱动的翻译和语言环境文件生成来辅助 l10n。

设置 i18next​

i18next 是 JavaScript 最流行的 i18n 框架。其 React 配套库 react-i18next 提供了无缝集成的 hooks 和组件。

安装与配置​

让 Cursor 在你的 React 项目中设置 i18next:

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 生成配置:

// 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;

翻译文件结构​

public/
locales/
en/
common.json
navigation.json
forms.json
errors.json
es/
common.json
...
ja/
common.json
...
ar/
common.json
...

英文翻译示例:

// 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"
}
}
提示

将翻译组织到命名空间(common、navigation、forms、errors)中,而不是单个大型文件中。命名空间让你可以仅延迟加载当前页面所需的翻译,减小初始包大小。让 Cursor 根据你的路由结构建议命名空间边界。

注意

切勿在组件中硬编码面向用户的字符串。即使最初只支持一种语言,从一开始就将字符串提取到翻译文件中是一项 10 分钟的投资,可以节省后续数周的重构工作。对每个可见字符串使用 t() 函数,包括按钮标签、错误消息和工具提示。

使用 react-i18next Hooks​

useTranslation hook 是在函数组件中访问翻译的主要方式。

基本用法​

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

复数形式与插值​

i18next 根据语言环境自动处理复数规则。不同语言有不同的复数形式(英语有 2 种,阿拉伯语有 6 种,日语有 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>
);
}

阿拉伯语翻译文件处理所有六种复数形式:

// 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}} عنصر"
}
提示

让 Cursor 为你支持的每个语言环境生成复数规则。类似"按照 CLDR 复数规则,为 items_count 翻译键创建阿拉伯语、俄语和波兰语的正确复数形式键"的提示可以生成符合语言环境的复数形式,无需你记忆 CLDR 规则。

设置 react-intl​

react-intl 是 FormatJS 套件中的另一个 i18n 库。它因严格的类型安全和 ICU Message Format 支持而在企业应用中很受欢迎。

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

使用 react-intl 组件​

react-intl 提供了声明式格式化组件。FormattedMessage 组件处理 ICU Message Format,而 FormattedNumber、FormattedDate 和 FormattedTime 会自动适应用户的语言环境。

// 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 使用 ICU Message Format,将复数、选择和格式化嵌入到消息字符串中。这意味着翻译者可以在不修改代码的情况下调整句子结构。如果你正在从简单的键值翻译文件迁移,让 Cursor 用实际示例解释 ICU Message Format 语法。

从右到左(RTL)支持​

RTL 支持对于阿拉伯语、希伯来语和波斯语等语言至关重要。Cursor 帮助你实现两个方向都能正确工作的 RTL 感知布局。

CSS 逻辑属性​

使用逻辑属性(margin-inline-start)而非物理属性(margin-left),这样布局在 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); }

RTL 感知的 React 组件​

// 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>
);
}
注意

不要在 CSS 中使用 text-align: left 或 text-align: right。请改用 text-align: start 和 text-align: end。物理属性在 RTL 模式下不会自动翻转,导致文本错位和布局损坏。让 Cursor 扫描你的 CSS 中的物理方向属性并将它们转换为逻辑等价物。

提示

通过在开发早期将阿拉伯语添加为语言环境来测试你的 RTL 布局。将布局翻转为 RTL 会暴露硬编码的物理属性、不镜像的 flex 布局以及应该翻转的图标。在开发期间修复这些问题比在发布后发现要便宜得多。

语言切换器组件​

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

本地化感知格式化​

日期、数字和货币格式化必须适应用户的语言环境。i18next 和 react-intl 都提供了格式化工具。

直接使用 Intl API​

// 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');
}

各语言环境的格式化示例​

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

始终使用原生 Intl API 进行格式化,而不是编写自定义格式化器。Intl API 使用内置于 JavaScript 引擎中的 CLDR(通用语言环境数据仓库)数据,该数据由平台供应商维护,并自动处理非公历日历(日本年号、伊斯兰历)等边缘情况。

AI 辅助的翻译工作流​

Cursor 可以使用 AI 为整个语言环境文件生成翻译。这不能替代专业人工翻译,但可以生成翻译者可以审查和优化的高质量初稿。

从源文件生成翻译​

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 读取源文件并生成三个翻译后的 JSON 文件。AI 会保留 ICU Message Format 变量、复数后缀和 JSON 结构。

从源代码中提取字符串​

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 执行全代码库重构:它识别 JSX 中的字符串字面量,生成结构化的翻译文件,并用适当的 t() 调用替换每个出现位置。

注意

AI 生成的翻译在发布到生产环境之前,应始终由母语人士审查。AI 可能误译依赖上下文的术语,遗漏文化细微差异,并产生语法正确但不自然的措辞。将 AI 翻译用作初稿,而非最终产品。在翻译管理工作流中用"需要审查"状态标记任何 AI 翻译的文件。

批量翻译脚本​

// 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);
提示

将批量翻译脚本添加到你的 CI 流水线中。当开发者添加新的英文字符串时,CI 作业会自动为所有目标语言环境生成初稿翻译,并创建一个 pull request,将新键标记为需要审查。这可以在无需手动协调的情况下保持所有语言环境文件同步。

缺失翻译检测​

在应用根目录渲染此组件一次。在开发环境中,它会将缺失的键记录到控制台。在生产环境中,将报告路由到错误跟踪服务。

// 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;
}

总结​

在 Cursor 中进行国际化和本地化为你提供了构建真正全球化应用的工具。遵循本指南中的模式——i18next 或 react-intl 配置、基于命名空间的翻译文件、ICU 感知的复数处理、RTL 逻辑 CSS 属性、原生 Intl API 格式化以及 AI 辅助的翻译流水线——你可以从第一天起支持任何语言和语言环境。尽早规划你的 i18n 策略,使用逻辑 CSS 属性而非物理属性,让 AI 生成初稿翻译,并在发布到生产环境之前始终由母语人士审查。