如何使用国际化 (i18n) 构建多语言 React 应用
Source: Dev.to
什么是国际化 (i18n)?
国际化 (i18n) 是指在设计和开发应用时,使其能够轻松适配不同语言和地区,而无需进行工程上的更改的过程。
i18n 这个缩写来源于单词 “internationalization” 中首字母 i 与末字母 n 之间恰好有 18 个字母。
i18n 的关键要素
- 翻译 – 将文本字符串转换为不同语言
- 本地化 – 为特定地区调整内容(日期、数字、货币)
- 复数化 – 正确处理每种语言的单数/复数形式
为什么 i18n 对你的 React 应用很重要
- 全球覆盖 – 用用户的母语触达他们,打开新市场
- 更佳用户体验 – 用户更倾向于使用自己语言的应用(研究显示 75 % 的用户更愿意用母语购买产品)
- 竞争优势 – 许多应用缺乏完善的国际化,这为你提供了优势
- 法律要求 – 某些国家要求应用提供本地语言版本
- SEO 效果 – 多语言内容提升不同地区的搜索排名
- 转化率提升 – 当内容使用用户语言时,用户更可能完成操作
前置条件
- 基础的 React 与 React Hooks 知识
- 已在机器上安装 Node.js 与 npm
- 一个代码编辑器,如 VS Code
项目初始化
使用 Vite 创建一个新的 React 应用:
npm create vite@latest react-i18n-demo -- --template react
cd react-i18n-demo
安装所需的 i18n 包:
npm install i18next react-i18next i18next-browser-languagedetector
启动开发服务器:
npm run dev
你的应用现在应该已经在 (此处会显示地址) 运行。
注意: 本教程不使用任何样式库。为简化起见,样式写在内联,但在真实项目中应将 CSS 拆分到独立文件,或使用 Tailwind CSS 等框架。
替换默认 CSS
/* src/index.css */
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
}
问题:硬编码字符串
典型的 React 组件中硬编码了英文字符串:
import { useState } from 'react';
function ProductCard({ product }) {
return (
<>
{/* ❌ Hardcoded English strings */}
<h3>{product.name}</h3>
<p>Price: ${product.price}</p>
<p>In stock: {product.stock} items</p>
{product.stock === 0 && (
<p>Out of stock</p>
)}
{product.stock === 1 && (
<p>Only 1 item left!</p>
)}
{product.stock > 1 && (
<p>In stock</p>
)}
<button>Add to cart</button>
</>
);
}
export default ProductCard;
这种写法的问题
- 没有翻译支持 – 所有文字仅为英文
- 维护困难 – 修改文字需要编辑多个组件
- 缺少复数化 – “1 item” 与 “2 items” 写死了
- 没有本地化 – 日期和数字格式固定
- 难以扩展 – 添加新语言必须修改每个组件
解决方案:react-i18next
react-i18next 是 React 国际化最流行、最强大的方案,提供:
- 简单的翻译 API
- 自动语言检测
- 复数化支持
- 嵌套翻译
- 日期、数字、货币格式化
- 翻译文件的懒加载
- TypeScript 支持
如何创建翻译文件
在 utils/locales 文件夹下,以独立的 JavaScript 文件组织翻译:
utils/
locales/
en/
translation.js
es/
translation.js
fr/
translation.js
英文 (utils/locales/en/translation.js)
export default {
en: {
translation: {
"product.price": "Price",
"product.inStock": "In stock: {{count}} items",
"product.outOfStock": "Out of stock",
"product.onlyOneLeft": "Only 1 item left!",
"product.addToCart": "Add to cart",
"profile.welcome": "Welcome, {{name}}!",
"profile.memberSince": "Member since: {{date}}",
"profile.loyaltyPoints": "You have {{points}} loyalty points",
"profile.orders_zero": "You have no orders",
"profile.orders_one": "You have 1 order",
"profile.orders_other": "You have {{count}} orders",
"profile.editProfile": "Edit profile",
},
},
};
西班牙文 (utils/locales/es/translation.js)
export default {
es: {
translation: {
"product.price": "Precio",
"product.inStock": "En stock: {{count}} artículos",
"product.outOfStock": "Agotado",
"product.onlyOneLeft": "¡Solo queda 1 artículo!",
"product.addToCart": "Añadir al carrito",
"profile.welcome": "¡Bienvenido, {{name}}!",
"profile.memberSince": "Miembro desde: {{date}}",
"profile.loyaltyPoints": "Tienes {{points}} puntos de fidelidad",
"profile.orders_zero": "No tienes pedidos",
"profile.orders_one": "Tienes 1 pedido",
"profile.orders_other": "Tienes {{count}} pedidos",
"profile.editProfile": "Editar perfil",
},
},
};
法文 (utils/locales/fr/translation.js)
export default {
fr: {
translation: {
"product.price": "Prix",
"product.inStock": "En stock: {{count}} articles",
"product.outOfStock": "Rupture de stock",
"product.onlyOneLeft": "Plus qu'un article !",
"product.addToCart": "Ajouter au panier",
"profile.welcome": "Bienvenue, {{name}} !",
"profile.memberSince": "Membre depuis : {{date}}",
"profile.loyaltyPoints": "Vous avez {{points}} points de fidélité",
"profile.orders_zero": "Vous n'avez aucune commande",
"profile.orders_one": "Vous avez 1 commande",
"profile.orders_other": "Vous avez {{count}} commandes",
"profile.editProfile": "Modifier le profil",
},
},
};