Next.js实现i18n

半兽人 发表于: 2025-07-22   最后更新时间: 2025-07-22 16:46:27  
{{totalSubscript}} 订阅, 62 游览

国际化(internationalization,i18n)在项目中是必备的,接下来是一个标准的国际化(Internationalization)配置,支持多语言切换功能。

核心特性

  • 多语言支持:支持英文(en-US)和中文(zh-Hans)
  • 服务端渲染:支持 SSR 和 SSG 的国际化
  • 自动语言检测:基于浏览器语言偏好和 Cookie 自动检测
  • 类型安全:完整的 TypeScript 类型支持
  • 动态加载:按需加载翻译资源,优化性能

依赖包

核心依赖

包名 版本 作用
i18next ^23.16.4 国际化核心库,提供翻译功能
react-i18next ^15.1.0 React 集成库,提供 React 组件和 Hooks
i18next-resources-to-backend ^1.2.1 动态加载翻译资源的插件

语言检测依赖

包名 版本 作用
negotiator ^0.6.3 HTTP 语言协商,解析 Accept-Language 头
@formatjs/intl-localematcher ^0.5.6 语言匹配算法,选择最佳匹配的语言

类型定义

包名 版本 作用
@types/negotiator ^0.6.3 Negotiator 的 TypeScript 类型定义

package.json

{
  "name": "next-base",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "next": "15.4.2",
    ...
    "react-i18next": "^15.1.0",
    "i18next": "^23.16.4",
    "i18next-resources-to-backend": "^1.2.1",
    "negotiator": "^0.6.3",
    "@formatjs/intl-localematcher": "^0.5.6"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@tailwindcss/postcss": "^4",
    "tailwindcss": "^4",
    "eslint": "^9",
    "eslint-config-next": "15.4.2",
    "@eslint/eslintrc": "^3",
    ...
    "@types/negotiator": "^0.6.3"
  }
}

文件结构

i18n/
├── README.md              # 文档
├── ARCHITECTURE.md        # 架构设计说明
├── index.ts               # 导出配置和类型
├── server.ts              # 服务端国际化工具
├── en-US/                 # 英文翻译文件
│   ├── common.ts          # 通用翻译
│   └── apps.ts            # 应用页面翻译
└── zh-Hans/               # 中文翻译文件
    ├── common.ts          # 通用翻译
    └── apps.ts            # 应用页面翻译

核心文件说明

index.ts - 配置导出

export type Locale = 'en-US' | 'zh-Hans'

export const i18n = {
  defaultLocale: 'en-US' as const,
  locales: ['en-US', 'zh-Hans'] as const,
} as const

功能

  • 定义支持的语言类型
  • 配置默认语言和可用语言列表
  • 导出类型安全的配置对象

server.ts - 服务端工具

// 初始化 i18next 实例
const initI18next = async (lng: Locale, ns: string) => { ... }

// 获取翻译函数
export async function useTranslation(lng: Locale, ns = '', options = {}) { ... }

// 服务端语言检测
export const getLocaleOnServer = async (): Promise<Locale> => { ... }

功能

  • 语言检测:从 Cookie 和 HTTP 头中检测用户语言偏好
  • 翻译初始化:动态加载翻译资源并初始化 i18next
  • 服务端翻译:在服务端组件中使用翻译功能

翻译文件结构

// en-US/common.ts
export default {
  welcome: 'Welcome',
  apps: 'Apps',
  // ...
} as const

// zh-Hans/common.ts  
export default {
  welcome: '欢迎',
  apps: '应用',
  // ...
} as const

使用方法

1. 在服务端组件中使用

import { useTranslation } from '@/i18n/server'

// 在服务端组件中
const MyServerComponent = async () => {
  const { t } = await useTranslation('en-US', 'common')

  return <h1>{t('welcome')}</h1>
}

2. 获取用户语言

import { getLocaleOnServer } from '@/i18n/server'

const Layout = async ({ children }) => {
  const locale = await getLocaleOnServer()

  return (
    <html lang={locale}>
      {children}
    </html>
  )
}

3. 添加新的翻译

  1. 创建翻译文件
// i18n/en-US/home.ts
export default {
  title: 'Home Page',
  description: 'Welcome to our website',
} as const

 // i18n/zh-Hans/home.ts
export default {
  title: '首页',
  description: '欢迎访问我们的网站',
} as const
  1. 使用翻译
    const { t } = await useTranslation('en-US', 'home')
    return <h1>{t('title')}</h1>
    

切换语言页面实现

page.tsx

import { useTranslation } from '@/i18n/server'
import { pathToLocaleMap } from '@/i18n'
import LanguageSwitcher from '@/app/components/LanguageSwitcher'
import { notFound } from 'next/navigation'
import { Metadata } from 'next'

interface AppsPageProps {
    params: {
        locale: string;
    };
}

// 生成动态元数据
export async function generateMetadata({ params }: AppsPageProps): Promise<Metadata> {
    const locale = pathToLocaleMap[params.locale as keyof typeof pathToLocaleMap]
    if (!locale) {
        return {}
    }

    const { t: tApps } = await useTranslation(locale, 'apps')

    const title = tApps('title')
    const description = tApps('description')

    return {
        title,
        description,
        openGraph: {
            title,
            description,
            locale: locale,
        },
        alternates: {
            canonical: `/${params.locale}/apps`,
            languages: {
                'en': '/en/apps',
                'zh': '/zh/apps',
            },
        },
    }
}

export default async function AppsPage({ params }: AppsPageProps) {
    // 验证语言参数
    const locale = pathToLocaleMap[params.locale as keyof typeof pathToLocaleMap]
    if (!locale) {
        notFound()
    }

    const { t: tCommon } = await useTranslation(locale, 'common')
    const { t: tApps } = await useTranslation(locale, 'apps')

    // 结构化数据
    const structuredData = {
        "@context": "https://schema.org",
        "@type": "WebPage",
        "name": tApps('title'),
        "description": tApps('description'),
        "inLanguage": locale,
        "url": `https://yourdomain.com/${params.locale}/apps`,
        "alternateName": {
            "en": "Applications",
            "zh": "应用程序"
        }
    }

    return (
        <>
            <script
                type="application/ld+json"
                dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
            />
            <div className="p-8 max-w-4xl mx-auto">
                <div className="flex justify-between items-center mb-6">
                    <div>
                        <h1 className="text-3xl font-bold">{tApps('title')}</h1>
                        <p className="text-gray-600 mt-2">{tApps('description')}</p>
                    </div>
                    <LanguageSwitcher />
                </div>

                <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
                    {/* 统计卡片 */}
                    <div className="bg-white p-6 rounded-lg shadow-md">
                        <h3 className="text-lg font-semibold mb-4">{tApps('stats.total')}</h3>
                        <div className="space-y-2">
                            <div className="flex justify-between">
                                <span>{tApps('stats.active')}:</span>
                                <span className="font-bold text-green-600">12</span>
                            </div>
                            <div className="flex justify-between">
                                <span>{tApps('stats.inactive')}:</span>
                                <span className="font-bold text-gray-600">3</span>
                            </div>
                        </div>
                    </div>

                    {/* 功能特性 */}
                    <div className="bg-white p-6 rounded-lg shadow-md">
                        <h3 className="text-lg font-semibold mb-4">{tApps('features.title')}</h3>
                        <ul className="space-y-2">
                            {tApps('features.list.0') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.0')}
                            </li>}
                            {tApps('features.list.1') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.1')}
                            </li>}
                            {tApps('features.list.2') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.2')}
                            </li>}
                            {tApps('features.list.3') && <li className="flex items-center">
                                <span className="text-green-500 mr-2">✓</span>
                                {tApps('features.list.3')}
                            </li>}
                        </ul>
                    </div>
                </div>

                {/* 操作按钮 */}
                <div className="bg-white p-6 rounded-lg shadow-md">
                    <h3 className="text-lg font-semibold mb-4">操作</h3>
                    <div className="flex flex-wrap gap-3">
                        <button className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
                            {tApps('actions.create')}
                        </button>
                        <button className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors">
                            {tApps('actions.edit')}
                        </button>
                        <button className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
                            {tApps('actions.view')}
                        </button>
                        <button className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
                            {tApps('actions.delete')}
                        </button>
                    </div>
                </div>

                {/* 语言链接 */}
                <div className="mt-6 p-4 bg-blue-50 rounded-lg">
                    <h3 className="text-lg font-medium mb-2">其他语言版本:</h3>
                    <div className="flex gap-4">
                        <a href="/en/apps" className="text-blue-600 hover:underline">🇺🇸 English</a>
                        <a href="/zh/apps" className="text-blue-600 hover:underline">🇨🇳 中文</a>
                    </div>
                </div>

                {/* 调试信息 */}
                <div className="mt-8 p-4 bg-gray-100 rounded-lg">
                    <h3 className="text-lg font-medium mb-2">调试信息:</h3>
                    <p><strong>当前语言:</strong> {locale}</p>
                    <p><strong>URL路径:</strong> /{params.locale}/apps</p>
                    <p><strong>页面标题:</strong> {tApps('title')}</p>
                    <p><strong>欢迎信息:</strong> {tCommon('welcome')}</p>
                    <p><strong>SEO优化:</strong> ✅ 支持搜索引擎索引</p>
                </div>
            </div>
        </>
    )
}

展示

next.js i18n语言切换

语言检测流程

  1. Cookie 检查:首先检查 locale Cookie
  2. HTTP 头解析:解析 Accept-Language
  3. 语言匹配:使用 @formatjs/intl-localematcher 匹配最佳语言
  4. 默认回退:如果没有匹配,使用默认语言

添加新语言

1. 更新配置

// i18n/index.ts
export type Locale = 'en-US' | 'zh-Hans' | 'ja-JP'

export const i18n = {
  defaultLocale: 'en-US' as const,
  locales: ['en-US', 'zh-Hans', 'ja-JP'] as const,
} as const

2. 创建翻译文件

i18n/
└── ja-JP/
    ├── common.ts
    └── home.ts

3. 添加翻译内容

// i18n/ja-JP/common.ts
export default {
  welcome: 'ようこそ',
  apps: 'アプリ',
  // ...
} as const

性能优化

  • 动态加载:翻译文件按需加载,不会增加初始包大小
  • 缓存机制:i18next 会缓存已加载的翻译资源
  • 类型安全:TypeScript 确保翻译键的正确性

调试技巧

1. 检查语言检测

const locale = await getLocaleOnServer()
console.log('Detected locale:', locale)

2. 验证翻译加载

const { t, i18n } = await useTranslation('en-US', 'common')
console.log('Available namespaces:', i18n.reportNamespaces.getUsedNamespaces())

3. 测试翻译

const { t } = await useTranslation('en-US', 'common')
console.log('Translation test:', t('welcome'))

相关文档

贡献指南

  1. 添加新翻译时,确保所有语言都有对应的翻译
  2. 使用 as const 确保类型安全
  3. 翻译键名使用 camelCase 命名
  4. 测试所有支持的语言

这个国际化配置为项目提供了完整的多语言支持,支持服务端渲染和客户端交互。

更新于 2025-07-22

查看NextJS更多相关的文章或提一个关于NextJS的问题,也可以与我们一起分享文章