Next.js 如何处理国际化(i18n)?有哪些内置支持或推荐方案?
Next.js 内置 i18n 支持
1. Pages Router 的 i18n
// next.config.js
const nextConfig = {
i18n: {
locales: ['en', 'zh', 'ja'],
defaultLocale: 'en',
localeDetection: true,
},
}
module.exports = nextConfig
// pages/index.js
import { useRouter } from 'next/router'
export default function HomePage() {
const router = useRouter()
const { locale, locales, asPath } = router
return (
<div>
<h1>Welcome</h1>
<p>Current locale: {locale}</p>
<div>
{locales.map((l) => (
<a
key={l}
href={asPath}
locale={l}
style={{ fontWeight: l === locale ? 'bold' : 'normal' }}
>
{l}
</a>
))}
</div>
</div>
)
}
2. App Router 的 i18n
// app/[locale]/layout.js
import { notFound } from 'next/navigation'
const locales = ['en', 'zh', 'ja']
export function generateStaticParams() {
return locales.map((locale) => ({ locale }))
}
export default function LocaleLayout({ children, params }) {
const { locale } = params
if (!locales.includes(locale)) {
notFound()
}
return (
<html lang={locale}>
<body>{children}</body>
</html>
)
}
// app/[locale]/page.js
export default function HomePage({ params }) {
const { locale } = params
return (
<div>
<h1>Welcome</h1>
<p>Current locale: {locale}</p>
</div>
)
}
推荐方案
1. next-intl
# 安装
npm install next-intl
// next.config.js
const withNextIntl = require('next-intl/plugin')('./i18n.ts')
module.exports = withNextIntl({
// 其他配置
})
// i18n.ts
import { notFound } from 'next/navigation'
import { getRequestConfig } from 'next-intl/server'
const locales = ['en', 'zh', 'ja']
export default getRequestConfig(async ({ locale }) => {
if (!locales.includes(locale)) notFound()
return {
messages: (await import(`./messages/${locale}.json`)).default,
}
})
// app/[locale]/layout.js
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
export default async function LocaleLayout({ children, params }) {
const { locale } = params
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
// app/[locale]/page.js
import { useTranslations } from 'next-intl'
export default function HomePage() {
const t = useTranslations('HomePage')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
)
}
// messages/en.json
{
"HomePage": {
"title": "Welcome",
"description": "This is the home page"
}
}
// messages/zh.json
{
"HomePage": {
"title": "欢迎",
"description": "这是首页"
}
}
2. react-i18next
# 安装
npm install react-i18next i18next
// lib/i18n.js
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
const resources = {
en: {
translation: {
welcome: 'Welcome',
description: 'This is the home page',
},
},
zh: {
translation: {
welcome: '欢迎',
description: '这是首页',
},
},
}
i18n.use(initReactI18next).init({
resources,
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
})
export default i18n
// app/[locale]/layout.js
'use client'
import { I18nextProvider } from 'react-i18next'
import i18n from '@/lib/i18n'
export default function LocaleLayout({ children, params }) {
const { locale } = params
// 设置语言
i18n.changeLanguage(locale)
return (
<html lang={locale}>
<body>
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
</body>
</html>
)
}
// app/[locale]/page.js
'use client'
import { useTranslation } from 'react-i18next'
export default function HomePage() {
const { t } = useTranslation()
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('description')}</p>
</div>
)
}
实际应用示例
1. 多语言博客
// app/[locale]/blog/[slug]/page.js
import { useTranslations } from 'next-intl'
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const { locale, slug } = params
const t = useTranslations('BlogPost')
// 获取文章内容
const post = await fetch(
`https://api.example.com/posts/${slug}?locale=${locale}`
)
if (!post.ok) {
notFound()
}
const data = await post.json()
return (
<article>
<header>
<h1>{data.title}</h1>
<p>{t('publishedOn', { date: data.publishedAt })}</p>
</header>
<div dangerouslySetInnerHTML={{ __html: data.content }} />
<footer>
<p>
{t('tags')}: {data.tags.join(', ')}
</p>
</footer>
</article>
)
}
// messages/en.json
{
"BlogPost": {
"publishedOn": "Published on {{date}}",
"tags": "Tags"
}
}
// messages/zh.json
{
"BlogPost": {
"publishedOn": "发布于 {{date}}",
"tags": "标签"
}
}
2. 多语言表单
// app/[locale]/contact/page.js
import { useTranslations } from 'next-intl'
export default function ContactPage() {
const t = useTranslations('ContactPage')
return (
<form>
<div>
<label htmlFor="name">{t('name')}</label>
<input
id="name"
name="name"
type="text"
placeholder={t('namePlaceholder')}
required
/>
</div>
<div>
<label htmlFor="email">{t('email')}</label>
<input
id="email"
name="email"
type="email"
placeholder={t('emailPlaceholder')}
required
/>
</div>
<div>
<label htmlFor="message">{t('message')}</label>
<textarea
id="message"
name="message"
placeholder={t('messagePlaceholder')}
required
/>
</div>
<button type="submit">{t('submit')}</button>
</form>
)
}
3. 语言切换器
// app/components/LanguageSwitcher.jsx
'use client'
import { useRouter, usePathname } from 'next/navigation'
import { useLocale } from 'next-intl'
export default function LanguageSwitcher() {
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const languages = [
{ code: 'en', name: 'English' },
{ code: 'zh', name: '中文' },
{ code: 'ja', name: '日本語' },
]
const switchLanguage = (newLocale) => {
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`)
router.push(newPath)
}
return (
<select value={locale} onChange={(e) => switchLanguage(e.target.value)}>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
)
}
高级功能
1. 动态导入翻译
// lib/translations.js
export async function getTranslations(locale) {
try {
const messages = await import(`./messages/${locale}.json`)
return messages.default
} catch (error) {
console.error(`Failed to load translations for ${locale}:`, error)
return {}
}
}
2. 翻译验证
// lib/translation-validator.js
export function validateTranslations(translations) {
const requiredKeys = ['HomePage.title', 'HomePage.description']
const missingKeys = []
for (const key of requiredKeys) {
if (!getNestedValue(translations, key)) {
missingKeys.push(key)
}
}
if (missingKeys.length > 0) {
console.warn('Missing translation keys:', missingKeys)
}
return missingKeys.length === 0
}
function getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj)
}
3. 翻译缓存
// lib/translation-cache.js
const translationCache = new Map()
export async function getCachedTranslations(locale) {
if (translationCache.has(locale)) {
return translationCache.get(locale)
}
const translations = await getTranslations(locale)
translationCache.set(locale, translations)
return translations
}
最佳实践
1. 翻译文件组织
# 翻译文件结构
messages/
├── en/
│ ├── common.json
│ ├── home.json
│ └── blog.json
├── zh/
│ ├── common.json
│ ├── home.json
│ └── blog.json
└── ja/
├── common.json
├── home.json
└── blog.json
2. 类型安全
// types/translations.ts
export interface Translations {
HomePage: {
title: string
description: string
}
BlogPost: {
publishedOn: string
tags: string
}
}
// 使用类型安全的翻译
const t = useTranslations<Translations>('HomePage')
3. 性能优化
// 懒加载翻译
const translations = await import(`./messages/${locale}.json`)
// 预加载翻译
export async function generateStaticParams() {
return locales.map((locale) => ({
locale,
translations: import(`./messages/${locale}.json`),
}))
}
总结
Next.js 国际化方案:
内置支持:
- Pages Router 的 i18n 配置
- App Router 的动态路由
推荐方案:
- next-intl (推荐)
- react-i18next
- 自定义解决方案
最佳实践:
- 合理的文件组织
- 类型安全
- 性能优化
- 翻译验证
注意事项:
- 选择合适的方案
- 考虑性能影响
- 处理缺失翻译
- 测试多语言功能