react-intl 与 next-i18next 选型实战
核心问题:React 生态里有几个主流 i18n 框架,什么场景选哪个?Next.js App Router 怎么配置多语言?
真实场景
你在构建一个 Next.js 电商站,需要支持中、英、日三种语言。在搜索后发现了 react-intl、next-i18next、next-intl 几个选项,不知道该选哪个,以及怎么在 App Router 里配置。
React i18n 框架全景对比
| 特性 | react-intl | next-i18next | next-intl |
|---|---|---|---|
| 标准 | ICU 消息格式 | i18next 格式 | ICU 消息格式 |
| Next.js Pages Router | ✅ | ✅ 专为此设计 | ✅ |
| Next.js App Router | ✅ 手动配置 | ⚠️ 支持有限 | ✅ 原生支持 |
| 复数/选择支持 | ✅ 内置 ICU | ✅ i18next 插件 | ✅ 内置 ICU |
| SSR 支持 | ✅ | ✅ | ✅ |
| Bundle 大小 | 中等 | 较大 | 较小 |
| TypeScript | ✅ | ✅ | ✅ 类型安全 |
| 翻译文件格式 | JSON | JSON | JSON |
| 社区生态 | 成熟 | 最广 | 快速增长 |
选型建议: - Next.js App Router(新项目)→ next-intl - Next.js Pages Router(存量项目)→ next-i18next - 纯 React(非 Next.js)→ react-intl 或 react-i18next
方案 A:next-intl(App Router 推荐)
安装
npm install next-intl
目录结构
├── messages/
│ ├── zh-CN.json
│ ├── en-US.json
│ └── ja-JP.json
├── src/
│ ├── i18n/
│ │ ├── routing.ts # locale 路由配置
│ │ └── request.ts # 服务端 locale 获取
│ ├── middleware.ts # locale 中间件
│ └── app/
│ └── [locale]/ # locale 动态路由
│ ├── layout.tsx
│ └── page.tsx
└── next.config.ts
配置步骤
Step 1:路由配置
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['zh-CN', 'en-US', 'ja-JP'],
defaultLocale: 'zh-CN',
// 路径前缀策略
// 'always':所有 locale 都加前缀 /zh-CN/about
// 'as-needed':默认 locale 不加前缀
localePrefix: 'always',
});
Step 2:中间件
// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
// 只对非静态文件和 API 路由执行中间件
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Step 3:服务端 locale 获取
// src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// 确保 locale 有效
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});
Step 4:next.config.ts
// next.config.ts
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
export default withNextIntl({
// 其他 Next.js 配置
});
Step 5:App Router 布局
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
if (!routing.locales.includes(locale as any)) {
notFound();
}
// 获取服务端消息(避免重复加载)
const messages = await getMessages();
return (
<html lang={locale} dir={locale.startsWith('ar') ? 'rtl' : 'ltr'}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Step 6:翻译文件
// messages/zh-CN.json
{
"common": {
"loading": "加载中...",
"error": "出错了,请重试",
"confirm": "确认",
"cancel": "取消"
},
"nav": {
"home": "首页",
"products": "商品",
"cart": "购物车 ({count, plural, =0 {空} one {# 件} other {# 件}})"
},
"product": {
"addToCart": "加入购物车",
"outOfStock": "暂时缺货",
"price": "{price, number, ::currency/CNY}"
}
}
// messages/en-US.json
{
"common": {
"loading": "Loading...",
"error": "Something went wrong, please try again",
"confirm": "Confirm",
"cancel": "Cancel"
},
"nav": {
"home": "Home",
"products": "Products",
"cart": "Cart ({count, plural, =0 {empty} one {# item} other {# items}})"
},
"product": {
"addToCart": "Add to Cart",
"outOfStock": "Out of Stock",
"price": "{price, number, ::currency/USD}"
}
}
Step 7:在组件中使用
// 服务端组件
import { useTranslations } from 'next-intl';
export default function ProductCard({ product }) {
const t = useTranslations('product');
return (
<div>
<h2>{product.name}</h2>
<p>{t('price', { price: product.price })}</p>
<button disabled={!product.inStock}>
{product.inStock ? t('addToCart') : t('outOfStock')}
</button>
</div>
);
}
// 客户端组件
'use client';
import { useTranslations } from 'next-intl';
export function CartButton({ count }: { count: number }) {
const t = useTranslations('nav');
return <button>{t('cart', { count })}</button>;
}
Step 8:语言切换组件
// src/components/LocaleSwitcher.tsx
'use client';
import { useRouter, usePathname } from 'next/navigation';
import { useLocale } from 'next-intl';
const localeNames: Record<string, string> = {
'zh-CN': '中文',
'en-US': 'English',
'ja-JP': '日本語',
};
export function LocaleSwitcher() {
const router = useRouter();
const pathname = usePathname();
const currentLocale = useLocale();
function handleChange(newLocale: string) {
// 替换 URL 中的 locale 部分
const segments = pathname.split('/');
segments[1] = newLocale;
router.push(segments.join('/'));
}
return (
<select value={currentLocale} onChange={(e) => handleChange(e.target.value)}>
{Object.entries(localeNames).map(([code, name]) => (
<option key={code} value={code}>{name}</option>
))}
</select>
);
}
方案 B:next-i18next(Pages Router)
安装
npm install next-i18next react-i18next i18next
配置
// next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'en-US', 'ja-JP'],
},
// 翻译文件路径
localePath: './public/locales',
// 重载翻译文件时间间隔(开发模式)
reloadOnPrerender: process.env.NODE_ENV === 'development',
};
// next.config.js
const { i18n } = require('./next-i18next.config');
module.exports = { i18n };
翻译文件结构
public/locales/
├── zh-CN/
│ ├── common.json # 通用文本
│ ├── product.json # 商品相关
│ └── checkout.json # 结算相关
├── en-US/
│ ├── common.json
│ ├── product.json
│ └── checkout.json
└── ja-JP/
├── common.json
├── product.json
└── checkout.json
// public/locales/zh-CN/common.json
{
"loading": "加载中...",
"error": "出错了,请重试",
"nav": {
"home": "首页",
"cart": "购物车({{count}} 件)"
}
}
在页面中使用
// pages/products/[id].tsx
import { GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
export const getStaticProps: GetStaticProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(locale ?? 'zh-CN', ['common', 'product'])),
},
};
};
export default function ProductPage({ product }) {
// 使用 namespace
const { t: tCommon } = useTranslation('common');
const { t: tProduct } = useTranslation('product');
return (
<div>
<nav>{tCommon('nav.home')}</nav>
<h1>{tProduct('title')}</h1>
<p>{tCommon('nav.cart', { count: 3 })}</p>
</div>
);
}
方案 C:纯 React(react-intl)
npm install react-intl
// App.tsx
import { IntlProvider, FormattedMessage, FormattedNumber } from 'react-intl';
const messages = {
'zh-CN': {
'product.price': '价格:{price, number, ::currency/CNY}',
'cart.items': '{count, plural, =0 {购物车是空的} one {# 件商品} other {# 件商品}}',
},
'en-US': {
'product.price': 'Price: {price, number, ::currency/USD}',
'cart.items': '{count, plural, =0 {Cart is empty} one {# item} other {# items}}',
},
};
function App() {
const [locale, setLocale] = useState('zh-CN');
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<ProductList />
</IntlProvider>
);
}
// 组件内使用
function CartButton({ count, price }) {
return (
<div>
<FormattedMessage id="cart.items" values={{ count }} />
<FormattedMessage id="product.price" values={{ price }} />
</div>
);
}
TypeScript 类型安全(next-intl)
next-intl 支持翻译 key 的 TypeScript 类型推断,防止拼错 key:
// src/types/global.d.ts
import messages from '../../messages/zh-CN.json';
// 声明翻译消息类型
declare global {
interface IntlMessages extends Messages {}
}
type Messages = typeof messages;
// 使用时有类型提示和错误检查
const t = useTranslations('product');
t('addToCart'); // ✅
t('addToCartXXX'); // ❌ TypeScript 报错:不存在该 key
常见问题
Q:SSR 时翻译会闪烁吗?
A:使用 next-intl 时,服务端渲染直接输出已翻译的 HTML,不会闪烁。客户端水合(hydration)时使用相同的翻译数据,保持一致。
Q:如何处理动态翻译内容(来自 CMS)?
// 静态翻译文件 + 动态 CMS 内容分离
const t = useTranslations('product'); // 静态 UI 文本
// 动态内容直接从 API 获取对应语言版本
const { locale } = useLocale();
const { data: product } = useSWR(`/api/products/${id}?locale=${locale}`);
Q:t() 函数里可以用 HTML 吗?
// next-intl 的富文本消息
// messages/zh-CN.json
{
"terms": "点击即表示您同意我们的<link>服务条款</link>"
}
// 组件中
t.rich('terms', {
link: (chunks) => <a href="/terms">{chunks}</a>,
});
下一节:Next.js 的两种方案讲完了,下面看 Vue / Nuxt 生态如何处理多语言。