国际化 (i18n)
next-intl 配置与多语言支持指南
国际化 (i18n)
Sistine Starter 使用 next-intl 提供完整的国际化支持,让你的应用轻松覆盖全球用户。本指南将介绍如何使用和扩展多语言功能。
next-intl 简介
next-intl 是专为 Next.js 设计的国际化库,具有以下特点:
- 类型安全: 完整的 TypeScript 支持,自动推断翻译键
- App Router 优化: 原生支持 Next.js 14 App Router
- 零运行时开销: 编译时优化,性能出色
- 灵活路由: 支持
[locale]
动态路由 - 服务端优先: 服务端组件原生支持
- Rich Features: 支持复数、日期格式化、数字格式化等
当前支持的语言
Sistine Starter 开箱即用支持以下语言:
语言代码 | 语言名称 | 翻译文件 |
---|---|---|
en | English (英文) | messages/en.json , messages/seo.en.json |
zh | 中文 (简体中文) | messages/zh.json , messages/seo.zh.json |
默认语言: 英文 (en
)
配置架构
1. 核心配置文件
i18n.config.ts
定义支持的语言和默认语言:
// i18n.config.ts
export const locales = ['en', 'zh'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';
export const localePrefix = 'as-needed';
export const localeNames: Record<Locale, string> = {
en: 'English',
zh: '中文'
};
配置说明:
配置项 | 说明 | 示例 |
---|---|---|
locales | 支持的语言列表 | ['en', 'zh'] |
defaultLocale | 默认语言 | 'en' |
localePrefix | 路由前缀策略 | 'as-needed' (默认语言无前缀) |
localeNames | 语言显示名称 | { en: 'English', zh: '中文' } |
localePrefix 策略:
'as-needed'
: 默认语言不显示前缀 (推荐)- 英文:
/about
(无前缀) - 中文:
/zh/about
(有前缀)
- 英文:
'always'
: 所有语言都显示前缀- 英文:
/en/about
- 中文:
/zh/about
- 英文:
'never'
: 都不显示前缀 (不推荐)
lib/i18n.ts
配置消息加载逻辑:
// lib/i18n.ts
import { getRequestConfig } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { locales, type Locale } from '@/i18n.config';
export default getRequestConfig(async ({ locale }) => {
console.log('getRequestConfig called with locale:', locale);
// 如果 locale 未定义,使用默认语言
if (!locale) {
console.log('Locale is undefined, using default locale: en');
const messages = (await import(`../messages/en.json`)).default;
const seoMessages = (await import(`../messages/seo.en.json`)).default;
return {
locale: 'en',
messages: {
...messages,
seo: seoMessages
}
};
}
// 验证 locale 是否有效
if (!locales.includes(locale as Locale)) {
console.log('Locale not found:', locale);
notFound();
}
try {
const messages = (await import(`../messages/${locale}.json`)).default;
const seoMessages = (await import(`../messages/seo.${locale}.json`)).default;
console.log('Messages loaded for locale:', locale);
return {
locale,
messages: {
...messages,
seo: seoMessages
}
};
} catch (error) {
console.error('Error loading messages for locale:', locale, error);
notFound();
}
});
关键逻辑:
- 动态加载翻译文件: 根据 locale 加载对应的 JSON 文件
- 合并消息: 将通用消息和 SEO 消息合并
- 错误处理: locale 不存在时返回 404
2. 中间件配置
middleware.ts
自动处理语言检测和重定向:
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale, localePrefix } from './i18n.config';
export default createMiddleware({
locales,
defaultLocale,
localePrefix
});
export const config = {
matcher: [
'/',
'/((?!api|_next|_vercel|.*\\..*).*)'
]
};
工作原理:
- 拦截所有页面请求 (除了 API、静态资源等)
- 检测用户语言偏好 (通过
Accept-Language
请求头或 URL) - 自动重定向到正确的语言路径
示例:
用户访问: https://yourdomain.com/
浏览器语言: zh-CN
→ 重定向到: https://yourdomain.com/zh/
用户访问: https://yourdomain.com/
浏览器语言: en-US
→ 保持: https://yourdomain.com/ (默认语言无前缀)
3. 根布局配置
app/[locale]/layout.tsx
为每个语言路由提供 Provider:
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { locales } from "@/i18n.config";
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
// 验证 locale
if (!locales.includes(locale as any)) {
notFound();
}
// 设置当前请求的 locale
setRequestLocale(locale);
// 加载翻译消息
const messages = await getMessages({ locale });
return (
<html lang={locale} suppressHydrationWarning>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
翻译文件结构
文件位置
messages/
├── en.json # 英文翻译 (通用消息)
├── zh.json # 中文翻译 (通用消息)
├── seo.en.json # 英文 SEO 消息
└── seo.zh.json # 中文 SEO 消息
消息结构
翻译文件使用 JSON 格式,支持嵌套结构:
{
"common": {
"brand": {
"name": "SISTINE",
"tagline": "Sistine AI & Sistine Labs"
},
"actions": {
"signIn": "Sign in",
"signUp": "Sign Up",
"save": "Save",
"cancel": "Cancel"
}
},
"navigation": {
"main": {
"pricing": "Pricing",
"blog": "Blog",
"contact": "Contact"
}
},
"auth": {
"login": {
"title": "Sign in to your account",
"emailLabel": "Email address",
"passwordLabel": "Password"
}
}
}
命名规范:
- 使用 camelCase 命名键
- 按功能模块分组 (
auth
,navigation
,dashboard
等) - 通用消息放在
common
命名空间
路由格式
URL 结构
基于 localePrefix: 'as-needed'
配置:
页面 | 英文 URL | 中文 URL |
---|---|---|
首页 | / | /zh |
定价 | /pricing | /zh/pricing |
登录 | /login | /zh/login |
仪表板 | /dashboard | /zh/dashboard |
博客文章 | /blog/post-slug | /zh/blog/post-slug |
文件系统路由
app/
└── [locale]/ # 动态路由段
├── (marketing)/
│ ├── page.tsx # 首页 (/ 或 /zh)
│ ├── pricing/
│ │ └── page.tsx # 定价页 (/pricing 或 /zh/pricing)
│ └── blog/
│ └── page.tsx # 博客列表
├── (auth)/
│ ├── login/
│ │ └── page.tsx # 登录页
│ └── signup/
│ └── page.tsx # 注册页
└── (protected)/
└── dashboard/
└── page.tsx # 仪表板
在组件中使用翻译
服务端组件 (Server Component)
使用 useTranslations
(从 next-intl
导入):
import { useTranslations } from 'next-intl';
export default function ServerComponent() {
const t = useTranslations('navigation.main');
return (
<nav>
<a href="/pricing">{t('pricing')}</a>
<a href="/blog">{t('blog')}</a>
<a href="/contact">{t('contact')}</a>
</nav>
);
}
客户端组件 (Client Component)
同样使用 useTranslations
,但需要标记 "use client"
:
"use client";
import { useTranslations } from 'next-intl';
export default function ClientComponent() {
const t = useTranslations('auth.login');
return (
<form>
<h1>{t('title')}</h1>
<label>{t('emailLabel')}</label>
<input type="email" placeholder={t('emailPlaceholder')} />
<label>{t('passwordLabel')}</label>
<input type="password" placeholder={t('passwordPlaceholder')} />
<button>{t('signInButton')}</button>
</form>
);
}
使用多个命名空间
import { useTranslations } from 'next-intl';
export default function MultiNamespaceComponent() {
const tCommon = useTranslations('common.actions');
const tAuth = useTranslations('auth.login');
return (
<div>
<h1>{tAuth('title')}</h1>
<button>{tCommon('signIn')}</button>
<button>{tCommon('cancel')}</button>
</div>
);
}
带变量的翻译
翻译文件:
{
"dashboard": {
"welcome": "Welcome back, {name}!",
"creditsRemaining": "You have {count} credits remaining"
}
}
组件中使用:
const t = useTranslations('dashboard');
<h1>{t('welcome', { name: user.name })}</h1>
<p>{t('creditsRemaining', { count: user.credits })}</p>
Rich Text (支持 HTML)
翻译文件:
{
"terms": {
"agreement": "By signing up, you agree to our <link>Terms of Service</link>"
}
}
组件中使用:
import { useTranslations } from 'next-intl';
const t = useTranslations('terms');
<p>
{t.rich('agreement', {
link: (chunks) => <a href="/terms">{chunks}</a>
})}
</p>
服务端和客户端的区别
服务端组件 (推荐)
优势:
- ✅ 更好的性能 (消息在服务端渲染,不发送到客户端)
- ✅ 更小的 bundle 体积
- ✅ SEO 友好
- ✅ 支持异步数据获取
使用场景:
- 营销页面 (首页、定价、博客)
- 静态内容
- SEO 重要的页面
示例:
// app/[locale]/(marketing)/page.tsx
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('hero');
return (
<section>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</section>
);
}
客户端组件
使用场景:
- 交互式组件 (表单、对话框)
- 需要 React hooks 的组件
- 需要浏览器 API 的组件
示例:
"use client";
import { useTranslations } from 'next-intl';
import { useState } from 'react';
export default function LoginForm() {
const t = useTranslations('auth.login');
const [email, setEmail] = useState('');
return (
<form>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('emailPlaceholder')}
/>
</form>
);
}
添加新语言
步骤 1: 复制翻译文件
复制英文翻译文件作为模板(以法语为例):
# 复制通用消息
cp messages/en.json messages/fr.json
# 复制 SEO 消息
cp messages/seo.en.json messages/seo.fr.json
步骤 2: 翻译消息
打开 messages/fr.json
和 messages/seo.fr.json
,将所有英文文本翻译为目标语言。
示例:
{
"common": {
"brand": {
"name": "SISTINE",
"tagline": "Sistine AI & Sistine Labs"
},
"actions": {
"signIn": "Se connecter",
"signUp": "S'inscrire",
"save": "Enregistrer",
"cancel": "Annuler"
}
}
}
步骤 3: 更新 i18n.config.ts
添加新语言到配置:
// i18n.config.ts
export const locales = ['en', 'zh', 'fr'] as const; // 添加 'fr'
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';
export const localePrefix = 'as-needed';
export const localeNames: Record<Locale, string> = {
en: 'English',
zh: '中文',
fr: 'Français' // 添加法语名称
};
步骤 4: 更新 middleware.ts
middleware.ts 会自动使用 i18n.config.ts
中的配置,无需额外修改。
步骤 5: 测试新语言
启动开发服务器并访问:
http://localhost:3000/fr
应该能看到新语言翻译的页面。
语言切换器组件
创建一个语言切换器让用户手动选择语言:
"use client";
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';
import { locales, localeNames } from '@/i18n.config';
export function LanguageSwitcher() {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const handleChange = (newLocale: string) => {
// 替换当前路径中的 locale
const segments = pathname.split('/');
if (locales.includes(segments[1] as any)) {
segments[1] = newLocale;
} else {
segments.splice(1, 0, newLocale);
}
const newPath = segments.join('/');
router.push(newPath);
};
return (
<select
value={locale}
onChange={(e) => handleChange(e.target.value)}
className="border rounded px-2 py-1"
>
{locales.map((loc) => (
<option key={loc} value={loc}>
{localeNames[loc]}
</option>
))}
</select>
);
}
使用:
import { LanguageSwitcher } from '@/components/language-switcher';
<header>
<nav>
{/* 导航项 */}
</nav>
<LanguageSwitcher />
</header>
最佳实践
1. 翻译键的命名
✅ 推荐:
{
"auth": {
"login": {
"title": "Sign in to your account",
"emailLabel": "Email address",
"submitButton": "Sign in"
}
}
}
❌ 不推荐:
{
"authLoginTitle": "Sign in to your account",
"authLoginEmailLabel": "Email address",
"authLoginSubmitButton": "Sign in"
}
使用嵌套结构更易于管理和理解。
2. 保持翻译文件同步
确保所有语言文件具有相同的键结构:
# 英文文件
messages/en.json
{
"hero": {
"title": "...",
"description": "..."
}
}
# 中文文件 (必须有相同的键)
messages/zh.json
{
"hero": {
"title": "...",
"description": "..."
}
}
如果缺少键,会导致运行时错误。
3. 使用 TypeScript 类型检查
next-intl 提供类型安全的翻译:
import { useTranslations } from 'next-intl';
const t = useTranslations('auth.login');
// ✅ 类型安全 - IDE 会自动补全
t('title');
t('emailLabel');
// ❌ 编译时错误 - 键不存在
t('nonExistentKey'); // TypeScript 会报错
4. 避免在翻译中硬编码 URL
❌ 不推荐:
{
"footer": {
"privacyLink": "https://yourdomain.com/privacy"
}
}
✅ 推荐:
{
"footer": {
"privacy": "Privacy Policy"
}
}
<a href="/privacy">{t('footer.privacy')}</a>
5. 处理复数形式
翻译文件:
{
"credits": {
"remaining": "{count, plural, =0 {No credits remaining} =1 {1 credit remaining} other {# credits remaining}}"
}
}
组件中使用:
const t = useTranslations('credits');
<p>{t('remaining', { count: 0 })}</p> // "No credits remaining"
<p>{t('remaining', { count: 1 })}</p> // "1 credit remaining"
<p>{t('remaining', { count: 5 })}</p> // "5 credits remaining"
6. 日期和数字格式化
import { useFormatter } from 'next-intl';
export function FormattingExample() {
const format = useFormatter();
const date = new Date('2025-10-15');
const number = 1234.56;
return (
<div>
{/* 日期格式化 */}
<p>{format.dateTime(date, { dateStyle: 'full' })}</p>
{/* 英文: Wednesday, October 15, 2025 */}
{/* 中文: 2025年10月15日星期三 */}
{/* 数字格式化 */}
<p>{format.number(number, { style: 'currency', currency: 'USD' })}</p>
{/* 英文: $1,234.56 */}
{/* 中文: US$1,234.56 */}
</div>
);
}
7. SEO 优化
为每个语言版本设置正确的 lang
和 hreflang
:
// app/[locale]/layout.tsx
export default async function LocaleLayout({ params: { locale } }) {
return (
<html lang={locale}>
<head>
<link rel="alternate" hreflang="en" href="https://yourdomain.com/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh" />
<link rel="alternate" hreflang="x-default" href="https://yourdomain.com/" />
</head>
{/* ... */}
</html>
);
}
8. 翻译缺失的后备方案
配置默认命名空间和后备语言:
// lib/i18n.ts
export default getRequestConfig(async ({ locale }) => {
// ...
return {
locale,
messages: {
...messages,
seo: seoMessages
},
// 翻译缺失时的后备语言
defaultTranslationValues: {
fallback: 'Translation missing'
}
};
});
常见问题
如何获取当前语言?
客户端组件:
"use client";
import { useLocale } from 'next-intl';
export function MyComponent() {
const locale = useLocale(); // 'en' 或 'zh'
return <p>Current locale: {locale}</p>;
}
服务端组件:
import { getLocale } from 'next-intl/server';
export default async function MyComponent() {
const locale = await getLocale(); // 'en' 或 'zh'
return <p>Current locale: {locale}</p>;
}
如何在 API 路由中使用翻译?
API 路由不直接支持翻译,但可以手动加载消息:
// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
// 从请求头获取语言
const locale = request.headers.get('accept-language')?.split(',')[0].split('-')[0] || 'en';
// 动态加载消息
const messages = (await import(`@/messages/${locale}.json`)).default;
return NextResponse.json({
message: messages.api.hello
});
}
翻译文件太大怎么办?
将大型翻译文件拆分为多个小文件:
messages/
├── en/
│ ├── common.json
│ ├── auth.json
│ ├── dashboard.json
│ └── admin.json
└── zh/
├── common.json
├── auth.json
├── dashboard.json
└── admin.json
然后在 lib/i18n.ts
中合并:
const commonMessages = (await import(`../messages/${locale}/common.json`)).default;
const authMessages = (await import(`../messages/${locale}/auth.json`)).default;
const dashboardMessages = (await import(`../messages/${locale}/dashboard.json`)).default;
return {
locale,
messages: {
common: commonMessages,
auth: authMessages,
dashboard: dashboardMessages
}
};
如何处理动态内容的翻译?
对于来自数据库的动态内容,需要在数据库中存储多语言版本:
例如 blog_posts
表可以为每种语言各保留一列 (如 title_en
、title_zh
、content_en
、content_zh
)。建表语句请参考 数据库
文档中的多语言示例。
组件中使用:
import { useLocale } from 'next-intl';
export function BlogPost({ post }) {
const locale = useLocale();
const title = locale === 'zh' ? post.title_zh : post.title_en;
const content = locale === 'zh' ? post.content_zh : post.content_en;
return (
<article>
<h1>{title}</h1>
<div>{content}</div>
</article>
);
}
如何测试翻译完整性?
编写测试确保所有语言文件具有相同的键:
// tests/i18n.test.ts
import enMessages from '@/messages/en.json';
import zhMessages from '@/messages/zh.json';
function getKeys(obj: any, prefix = ''): string[] {
return Object.keys(obj).flatMap(key => {
const value = obj[key];
const fullKey = prefix ? `${prefix}.${key}` : key;
return typeof value === 'object' && !Array.isArray(value)
? getKeys(value, fullKey)
: [fullKey];
});
}
test('All translations have the same keys', () => {
const enKeys = getKeys(enMessages).sort();
const zhKeys = getKeys(zhMessages).sort();
expect(enKeys).toEqual(zhKeys);
});
如何在生产环境优化性能?
- 使用静态生成 (SSG):
// app/[locale]/page.tsx
export function generateStaticParams() {
return locales.map(locale => ({ locale }));
}
- 启用 Tree-shaking: 只加载当前语言的消息
- 使用 CDN: 部署到 Vercel 等平台,自动全球加速