Next.js 如何处理国际化(i18n)?有哪些内置支持或推荐方案?

52 阅读1分钟

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
  • 自定义解决方案

最佳实践

  • 合理的文件组织
  • 类型安全
  • 性能优化
  • 翻译验证

注意事项

  • 选择合适的方案
  • 考虑性能影响
  • 处理缺失翻译
  • 测试多语言功能