🌐 在 React 中实现国际化:基于 i18next 的完整方案
🔍 介绍
本文将详细介绍如何在 React 项目中实现完善的国际化解决方案,包括语言检测、切换、持久化和高级格式化功能。国际化(i18n)是使应用能够适应不同语言和地区的过程,是拓展全球用户市场的关键步骤。
🛠️ 技术栈
本项目使用的国际化相关技术栈:
库 | 用途 | 版本 |
---|---|---|
i18next | 核心国际化框架 | ^21.8.0 |
react-i18next | React 绑定库 | ^11.16.0 |
i18next-browser-languagedetector | 自动检测浏览器语言 | ^6.1.4 |
i18next-icu | 支持 ICU 消息格式(复数、性别等) | ^2.0.3 |
i18next-http-backend | 动态加载翻译文件 | ^1.4.0 |
📂 项目结构
国际化相关文件组织:
src/
└── i18n/
├── index.ts # i18next 配置文件
├── locales/ # 翻译资源目录
│ ├── en-US/ # 英文资源
│ │ ├── common.json # 通用翻译
│ │ ├── home.json # 首页翻译
│ │ └── user.json # 用户相关翻译
│ └── zh-CN/ # 中文资源
│ ├── common.json # 通用翻译
│ ├── home.json # 首页翻译
│ └── user.json # 用户相关翻译
└── types.ts # 类型定义(TypeScript)
⚙️ 核心配置
i18n/index.ts
文件是国际化的核心配置:
typescript
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import ICU from 'i18next-icu';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
// 导入翻译资源
import enCommon from './locales/en-US/common.json';
import enHome from './locales/en-US/home.json';
import zhCommon from './locales/zh-CN/common.json';
import zhHome from './locales/zh-CN/home.json';
i18n
// 动态加载翻译
.use(Backend)
// 自动检测用户语言
.use(LanguageDetector)
// 支持ICU格式
.use(new ICU())
// 集成React
.use(initReactI18next)
.init({
// 预加载资源
resources: {
'en-US': {
common: enCommon,
home: enHome,
},
'zh-CN': {
common: zhCommon,
home: zhHome,
},
},
// 默认命名空间
defaultNS: 'common',
// React已经防止XSS
interpolation: {
escapeValue: false,
},
// 回退语言
fallbackLng: 'en-US',
// 支持的语言列表
supportedLngs: ['en-US', 'zh-CN'],
// 语言检测配置
detection: {
// 检测顺序
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
// URL参数名
lookupQuerystring: 'locale',
// Cookie名
lookupCookie: 'locale',
// localStorage键名
lookupLocalStorage: 'locale',
// 缓存位置
caches: ['cookie', 'localStorage'],
// Cookie选项
cookieOptions: {
path: '/',
sameSite: 'strict',
expires: 365,
},
},
// 调试模式(生产环境应关闭)
debug: process.env.NODE_ENV === 'development',
});
export default i18n;
📝 翻译资源文件
翻译资源采用 JSON 格式,按命名空间组织:
locales/en-US/common.json:
json
{
"welcome": "Welcome",
"message": "Hello",
"buttons": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
},
"errors": {
"required": "This field is required",
"invalid": "Invalid value"
}
}
locales/en-US/home.json:
json
{
"title": "Home",
"subtitle": "Welcome to our application",
"item": "{count, plural, =0 {no projects} one {# project} other {# projects}}",
"genderSentence": "{gender, select, male {He avoids bugs.} female {She avoids bugs.} other {They avoid bugs.}}",
"dateFormat": "Last updated on {date, date, medium}"
}
🧩 在组件中使用
基本翻译
jsx
import { useTranslation } from 'react-i18next';
function WelcomeComponent() {
const { t } = useTranslation(['common', 'home']);
return (
<div>
<h1>{t('common:welcome')}</h1>
<p>{t('home:subtitle')}</p>
<button>{t('common:buttons.save')}</button>
</div>
);
}
高级格式化
支持 ICU 消息格式,包括:
- 复数形式:
jsx
function ProjectList({ count }) {
const { t } = useTranslation('home');
return (
<div>
<h2>{t('title')}</h2>
<p>{t('item', { count })}</p>
</div>
);
}
// 输出示例:
// count=0: "no projects"
// count=1: "1 project"
// count=5: "5 projects"
- 性别选择:
jsx
function DeveloperInfo({ developer }) {
const { t } = useTranslation('home');
return (
<div>
<h3>{developer.name}</h3>
<p>{t('genderSentence', { gender: developer.gender })}</p>
</div>
);
}
// 输出示例:
// gender="male": "He avoids bugs."
// gender="female": "She avoids bugs."
// gender="other": "They avoid bugs."
- 日期格式化:
jsx
function LastUpdated({ date }) {
const { t } = useTranslation('home');
return (
<div>
<p>{t('dateFormat', { date: new Date(date) })}</p>
</div>
);
}
// 输出示例: "Last updated on Feb 18, 2023"
🔄 语言切换实现
本项目使用 URL 查询参数来实现语言切换,同时也保存在 localStorage 和 cookie 中:
jsx
import { useTranslation } from 'react-i18next';
import { useNavigate, useLocation } from 'react-router-dom';
import { useCallback, useEffect } from 'react';
function LanguageSwitcher() {
const { i18n } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
// 切换语言函数
const handleChangeLocale = useCallback(
(locale) => {
const params = new URLSearchParams(location.search);
params.set('locale', locale);
navigate({ search: params.toString() }, { replace: true });
document.documentElement.lang = locale; // 更新HTML lang属性
},
[navigate, location.search]
);
// 从URL同步语言设置
useEffect(() => {
const params = new URLSearchParams(location.search);
const locale = params.get('locale');
if (locale && locale !== i18n.language) {
i18n.changeLanguage(locale);
} else if (!locale && i18n.language) {
handleChangeLocale(i18n.language);
}
}, [location.search, i18n, handleChangeLocale]);
return (
<div className="language-switcher">
<button
className={i18n.language === 'zh-CN' ? 'active' : ''}
onClick={() => handleChangeLocale('zh-CN')}
>
中文
</button>
<button
className={i18n.language === 'en-US' ? 'active' : ''}
onClick={() => handleChangeLocale('en-US')}
>
English
</button>
<span>当前语言: {i18n.language}</span>
</div>
);
}
export default LanguageSwitcher;
🚀 高级用法
1. 懒加载翻译资源
针对大型应用,可以按需加载翻译资源,提高初始加载速度:
jsx
import { Suspense } from 'react';
import { useTranslation } from 'react-i18next';
// 配置i18next-http-backend
// 在i18n/index.ts中:
i18n.use(Backend).init({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
// ...其他配置
});
// 在应用中使用
function App() {
return (
<Suspense fallback={<div>Loading translations...</div>}>
<MyComponent />
</Suspense>
);
}
function MyComponent() {
// 会触发按需加载翻译文件
const { t } = useTranslation('admin', { useSuspense: true });
return <div>{t('title')}</div>;
}
2. 使用HOC包装组件
针对类组件或需要在多个组件重用翻译逻辑时:
jsx
import { withTranslation } from 'react-i18next';
class LegacyComponent extends React.Component {
render() {
const { t } = this.props;
return <h1>{t('common:welcome')}</h1>;
}
}
export default withTranslation(['common'])(LegacyComponent);
3. 使用Trans组件处理嵌套HTML
当翻译内容包含HTML或React组件时:
jsx
import { Trans } from 'react-i18next';
// 翻译文件
// "welcome": "Welcome to <bold>{{name}}</bold>, click <link>here</link> to learn more"
function WelcomeWithHtml() {
return (
<Trans
i18nKey="common:welcome"
values={{ name: 'React' }}
components={{
bold: <strong />,
link: <a href="/docs" />,
}}
/>
);
}
⚡ 性能优化
1. 使用命名空间分割文件
jsx
// 只加载需要的命名空间
const { t } = useTranslation(['common']);
2. 避免不必要的重新渲染
jsx
// 使用 react-i18next 的 t 函数缓存
const MyComponent = React.memo(({ item }) => {
const { t } = useTranslation();
return (
<div>
{t('label')}: {item.name}
</div>
);
});
3. 预加载关键翻译
jsx
// 预先加载核心翻译
i18n.loadNamespaces(['common', 'auth']).then(() => {
// 应用初始化
});
4. 使用服务端渲染
jsx
// Next.js中使用SSR
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'home'])),
},
};
}
📱 实际项目案例
多语言电子商务网站
jsx
// 商品详情页
function ProductDetail({ product }) {
const { t, i18n } = useTranslation(['products', 'common']);
const currentLocale = i18n.language;
// 格式化价格和货币
const formattedPrice = new Intl.NumberFormat(currentLocale, {
style: 'currency',
currency: currentLocale === 'zh-CN' ? 'CNY' : 'USD',
}).format(product.price);
return (
<div className="product-detail">
<h1>{product.name[currentLocale]}</h1>
<p className="description">{product.description[currentLocale]}</p>
<div className="price">{formattedPrice}</div>
<button className="add-to-cart">{t('common:buttons.addToCart')}</button>
<div className="shipping">
{t('products:shipping.info', { days: product.shippingDays })}
</div>
</div>
);
}
语言感知路由(Next.js)
jsx
// pages/[locale]/products/[id].js
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
export default function ProductPage() {
const router = useRouter();
const { locale, id } = router.query;
const { t } = useTranslation();
// 语言切换函数
const changeLanguage = (newLocale) => {
router.push('/[locale]/products/[id]', `/${newLocale}/products/${id}`, {
locale: newLocale,
});
};
return (
<div>
{/* 页面内容 */}
<LanguageSwitcher onChange={changeLanguage} />
</div>
);
}
❓ 常见问题与解决方案
1. 富文本内容的翻译
问题: 需要翻译包含HTML的富文本内容
解决方案: 使用Trans
组件和dangerouslySetInnerHTML
(谨慎使用)
jsx
// 使用Trans组件
<Trans i18nKey="content.html" components={{ em: <em /> }} />;
// 使用dangerouslySetInnerHTML(确保内容安全)
function RichTextContent() {
const { t } = useTranslation();
return <div dangerouslySetInnerHTML={{ __html: t('content.html') }} />;
}
2. 数字和日期本地化
问题: 不同区域的数字和日期格式不同
解决方案: 使用Intl
API结合i18next
jsx
function FormattedDate({ date }) {
const { i18n } = useTranslation();
// 根据当前语言格式化日期
const formattedDate = new Intl.DateTimeFormat(i18n.language, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date(date));
return <span>{formattedDate}</span>;
}
3. 动态键名翻译
问题: 翻译键需要动态生成
解决方案: 使用模板字符串
jsx
function DynamicTranslation({ section, key }) {
const { t } = useTranslation();
const translationKey = `${section}.${key}`;
return <span>{t(translationKey)}</span>;
}
4. 处理翻译缺失
问题: 某些翻译在某语言中缺失
解决方案: 设置回退机制和调试工具
jsx
// i18n配置中
i18n.init({
fallbackLng: 'en-US',
saveMissing: true, // 开发模式下保存缺失的翻译
missingKeyHandler: (lng, ns, key) => {
console.warn(`Missing translation: ${key} in ${ns} for ${lng}`);
// 可以将缺失的键发送到后端收集
},
});
✅ 最佳实践
- 结构化翻译:使用命名空间组织翻译文件,便于维护
- URL 持久化:通过 URL 参数保存语言选择,便于分享和 SEO
- 本地存储:同时保存在 cookie 和 localStorage 中,提高用户体验
- 回退机制:设置默认语言作为回退选项,避免显示空白内容
- ICU 格式支持:处理复数、性别等复杂语言情况
- 按需加载:大型应用使用懒加载优化性能
- 类型安全:结合TypeScript定义翻译类型
- 翻译管理:使用专业翻译管理工具或服务,如Lokalise、Crowdin
- 自动化:集成到CI/CD流程,自动检测缺失翻译
- 本地化测试:针对不同语言环境进行测试
📝 总结
通过结合 i18next 生态系统,我们可以在 React 项目中实现功能完善的国际化方案。这种方案不仅支持基本的文本翻译,还能处理复杂的语言特性,提供良好的用户体验。
合理的项目结构和开发实践可以使国际化工作变得更加高效和可维护。随着应用的全球化,投入国际化的工作将带来显著的用户增长和市场扩展收益。