Next.js 国际化完全指南:next-intl 最佳实践
Next.js 国际化完全指南:next-intl 最佳实践
去年接手一个需要支持多语言的 Next.js 项目,看到配置文件里各种 i18n 的设置,说实话当时有点懵。翻了半天文档才发现,App Router 和以前的 Pages Router 在国际化这块完全是两个思路。折腾了一周,终于把 next-intl 配好了,顺便也踩了不少坑。
今天就来聊聊 Next.js 的国际化方案,特别是在 App Router 下怎么用 next-intl 优雅地实现多语言。
为什么选择 next-intl?
可能你会问,Next.js 不是自带国际化功能吗?确实,在 Pages Router 时代,Next.js 有内置的 i18n 路由支持。但到了 App Router,这个功能被移除了。
官方的建议是:使用第三方库。而 next-intl 就是最受欢迎的选择之一。
next-intl 的优势:
- App Router 原生支持 - 专门为 App Router 设计,用起来特别顺手
- 类型安全 - 配合 TypeScript 能做到翻译文本的类型检查
- 灵活的路由方案 - 支持子路径、域名、Cookie 等多种语言切换方式
- 强大的功能 - 支持复数、日期格式化、数字格式化、富文本等
- 性能优秀 - Server Component 友好,支持静态渲染
说实话,相比其他方案,next-intl 的文档写得也挺清楚,上手没那么痛苦。
基础配置:从零开始
1. 安装依赖
首先当然是安装 next-intl:
npm install next-intl
# 或
pnpm add next-intl
# 或
yarn add next-intl
2. 创建翻译文件
在项目根目录创建 messages 文件夹(也可以叫 locales 或其他名字),然后按语言创建 JSON 文件:
messages/
├── en.json
├── zh.json
└── ja.json
messages/zh.json:
{
"HomePage": {
"title": "欢迎来到我的网站",
"description": "这是一个支持多语言的 Next.js 应用"
},
"Navigation": {
"home": "首页",
"about": "关于",
"contact": "联系我们"
}
}
messages/en.json:
{
"HomePage": {
"title": "Welcome to My Website",
"description": "This is a multilingual Next.js application"
},
"Navigation": {
"home": "Home",
"about": "About",
"contact": "Contact Us"
}
}
这种嵌套结构不是必须的,但我发现按页面或组件分组管理起来会方便很多。
3. 配置 i18n.ts
创建 i18n.ts(或 i18n/config.ts)来配置支持的语言:
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default
}));
这个配置告诉 next-intl 去哪里找翻译文件。locale 参数会自动从 URL 中提取。
4. 创建中间件
在项目根目录创建 middleware.ts,这是处理多语言路由的关键:
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// 支持的语言列表
locales: ['en', 'zh', 'ja'],
// 默认语言
defaultLocale: 'zh',
// 是否在 URL 中始终显示默认语言
localePrefix: 'as-needed'
});
export const config = {
// 匹配所有路径,除了 api、_next/static、_next/image、favicon.ico
matcher: ['/', '/(zh|en|ja)/:path*', '/((?!api|_next|_next/static|_next/image|favicon.ico).*)']
};
关于 localePrefix 的选项:
'always'- 所有语言都显示前缀,包括默认语言 (/zh/about,/en/about)'as-needed'- 默认语言不显示前缀 (/about,/en/about)'never'- 所有语言都不显示前缀(需要其他方式识别语言,比如域名)
我一般用 'as-needed',因为对中文用户友好,URL 看起来也更简洁。
5. 调整 app 目录结构
这是最关键的一步。你需要把所有路由放到 [locale] 动态路由下:
之前的结构:
app/
├── page.tsx
├── about/
│ └── page.tsx
└── layout.tsx
调整后:
app/
├── [locale]/
│ ├── page.tsx
│ ├── about/
│ │ └── page.tsx
│ └── layout.tsx
└── layout.tsx (可选,用于全局配置)
6. 配置根 Layout
在 app/[locale]/layout.tsx 中配置语言:
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
const locales = ['en', 'zh', 'ja'];
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
// 验证语言是否支持
if (!locales.includes(locale)) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
注意这里的 generateStaticParams,如果你用静态生成,这个函数会告诉 Next.js 需要为哪些语言生成页面。
在组件中使用翻译
配置完成后,就可以在组件中使用翻译了。
Server Component 中使用
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
useTranslations 的参数是翻译文件中的命名空间(就是最外层的 key)。如果不传参数,可以用 t('HomePage.title') 这样的完整路径。
Client Component 中使用
在 Client Component 中使用方式完全一样:
'use client';
import { useTranslations } from 'next-intl';
export default function Navigation() {
const t = useTranslations('Navigation');
return (
<nav>
<a href="/">{t('home')}</a>
<a href="/about">{t('about')}</a>
<a href="/contact">{t('contact')}</a>
</nav>
);
}
这就是 next-intl 的优雅之处 - Server 和 Client Component 用法一致。
多语言路由处理
获取当前语言
import { useLocale } from 'next-intl';
export default function LanguageSwitcher() {
const locale = useLocale();
return <div>Current language: {locale}</div>;
}
创建语言切换器
这是每个国际化网站都需要的功能:
'use client';
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';
export default function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const switchLanguage = (newLocale: string) => {
// 替换路径中的语言部分
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
router.push(newPath);
};
return (
<select value={locale} onChange={(e) => switchLanguage(e.target.value)}>
<option value="zh">中文</option>
<option value="en">English</option>
<option value="ja">日本語</option>
</select>
);
}
不过这个方案有个小问题:如果用户在默认语言(比如中文)页面,URL 是 /about,切换到英文后应该是 /en/about。上面的代码需要改进一下:
const switchLanguage = (newLocale: string) => {
// 移除当前语言前缀
let path = pathname;
if (pathname.startsWith(`/${locale}`)) {
path = pathname.substring(locale.length + 1);
}
// 添加新语言前缀(除非是默认语言且配置为 as-needed)
const newPath = newLocale === 'zh' ? path : `/${newLocale}${path}`;
router.push(newPath);
};
使用 Link 组件
next-intl 提供了一个增强版的 Link 组件,会自动处理语言前缀:
import { Link } from '@/navigation'; // 需要先配置
<Link href="/about">
{t('about')}
</Link>
配置 navigation.ts:
import { createSharedPathnamesNavigation } from 'next-intl/navigation';
export const locales = ['en', 'zh', 'ja'] as const;
export const localePrefix = 'as-needed';
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales, localePrefix });
这样导出的 Link、useRouter 等组件都会自动处理语言路径。
高级功能
1. 带参数的翻译
翻译内容经常需要插入变量,next-intl 支持这样写:
messages/zh.json:
{
"welcome": "欢迎回来,{username}!",
"items": "你有 {count} 个新消息"
}
使用:
const t = useTranslations();
<p>{t('welcome', { username: 'John' })}</p>
<p>{t('items', { count: 5 })}</p>
2. 复数处理
不同语言的复数规则不一样,next-intl 提供了 t.rich 来处理:
messages/en.json:
{
"messages": {
"one": "You have {count} message",
"other": "You have {count} messages"
}
}
使用:
t('messages', { count: 1 }) // "You have 1 message"
t('messages', { count: 5 }) // "You have 5 messages"
中文没有复数概念,可以这样写:
messages/zh.json:
{
"messages": "你有 {count} 条消息"
}
3. 日期和数字格式化
next-intl 提供了专门的格式化函数:
import { useFormatter } from 'next-intl';
export default function DateExample() {
const format = useFormatter();
const now = new Date();
return (
<div>
<p>{format.dateTime(now, { dateStyle: 'full' })}</p>
{/* 中文:2025年12月25日星期三 */}
{/* 英文:Wednesday, December 25, 2025 */}
<p>{format.number(1234567.89, { style: 'currency', currency: 'CNY' })}</p>
{/* 中文:¥1,234,567.89 */}
{/* 英文:CN¥1,234,567.89 */}
</div>
);
}
4. 富文本翻译
有时候翻译内容需要包含 HTML 标签或 React 组件:
messages/zh.json:
{
"richText": "我同意<terms>服务条款</terms>和<privacy>隐私政策</privacy>"
}
使用:
import { useTranslations } from 'next-intl';
export default function Agreement() {
const t = useTranslations();
return (
<p>
{t.rich('richText', {
terms: (chunks) => <a href="/terms">{chunks}</a>,
privacy: (chunks) => <a href="/privacy">{chunks}</a>
})}
</p>
);
}
翻译文件管理最佳实践
随着项目变大,翻译文件会越来越难管理。这里分享几个实用技巧:
1. 按功能模块拆分
不要把所有翻译都塞在一个大 JSON 里,按页面或功能拆分:
messages/
├── zh/
│ ├── common.json # 通用翻译(按钮、错误提示等)
│ ├── home.json # 首页
│ ├── about.json # 关于页
│ └── auth.json # 认证相关
├── en/
│ ├── common.json
│ ├── home.json
│ ├── about.json
│ └── auth.json
然后在 i18n.ts 中合并:
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => {
const common = (await import(`./messages/${locale}/common.json`)).default;
const home = (await import(`./messages/${locale}/home.json`)).default;
const about = (await import(`./messages/${locale}/about.json`)).default;
const auth = (await import(`./messages/${locale}/auth.json`)).default;
return {
messages: {
common,
home,
about,
auth
}
};
});
2. 使用 TypeScript 类型检查
这是我觉得特别有用的功能。定义翻译文件的类型,防止拼写错误:
types/i18n.ts:
import zh from '@/messages/zh.json';
type Messages = typeof zh;
declare global {
interface IntlMessages extends Messages {}
}
配置 TypeScript(tsconfig.json):
{
"compilerOptions": {
"types": ["./types/i18n"]
}
}
这样在使用 t('xxx') 时,如果 key 不存在,TypeScript 会报错。非常实用!
3. 提取共用翻译
一些通用文本(比如”确定”、“取消”、“保存”)会在很多地方用到,建议单独管理:
messages/zh/common.json:
{
"actions": {
"save": "保存",
"cancel": "取消",
"delete": "删除",
"confirm": "确认",
"edit": "编辑"
},
"status": {
"success": "操作成功",
"error": "操作失败",
"loading": "加载中..."
}
}
使用:
const t = useTranslations('common.actions');
<button>{t('save')}</button>
4. 使用翻译管理工具
项目大了之后,手动维护 JSON 文件会很痛苦。可以考虑这些工具:
- Tolgee - 开源的翻译管理平台,支持实时编辑
- Localazy - 自动化翻译工作流
- i18n Ally (VSCode 插件) - 在编辑器中直接管理翻译
我现在主要用 i18n Ally,在写代码时能直接看到翻译内容,改起来也方便。
5. 缺失翻译的处理
开发时经常遇到某个语言的翻译还没写完的情况。可以配置回退逻辑:
// i18n.ts
export default getRequestConfig(async ({ locale }) => {
const messages = (await import(`./messages/${locale}.json`)).default;
const fallback = locale !== 'zh'
? (await import(`./messages/zh.json`)).default
: {};
return {
messages: {
...fallback,
...messages
}
};
});
这样如果英文翻译缺失,会自动回退到中文。
性能优化
1. 代码分割
如果翻译文件很大,可以按需加载:
// 只在需要时加载
export default function AdminPage() {
const t = useTranslations('admin'); // 只加载 admin 命名空间
// ...
}
2. 静态生成
对于不常变的页面,使用静态生成可以大幅提升性能:
// app/[locale]/about/page.tsx
export const dynamic = 'force-static';
export function generateStaticParams() {
return [
{ locale: 'zh' },
{ locale: 'en' },
{ locale: 'ja' }
];
}
3. 翻译预加载
对于首屏内容,可以预加载翻译文件:
import { getTranslations } from 'next-intl/server';
// 在服务端预加载
export default async function Home() {
const t = await getTranslations('HomePage');
return <h1>{t('title')}</h1>;
}
常见问题和解决方案
1. 动态路由的语言切换
动态路由(比如 /blog/[slug])切换语言时需要保持 slug:
const switchLanguage = (newLocale: string) => {
const segments = pathname.split('/').filter(Boolean);
// 移除旧的语言前缀
if (['zh', 'en', 'ja'].includes(segments[0])) {
segments.shift();
}
// 添加新的语言前缀(如果需要)
if (newLocale !== 'zh' || localePrefix === 'always') {
segments.unshift(newLocale);
}
router.push('/' + segments.join('/'));
};
2. SEO 优化
多语言网站的 SEO 需要特别注意:
// app/[locale]/layout.tsx
export async function generateMetadata({ params: { locale } }) {
const t = await getTranslations({ locale, namespace: 'metadata' });
return {
title: t('title'),
description: t('description'),
alternates: {
canonical: `https://example.com/${locale}`,
languages: {
'zh-CN': 'https://example.com/zh',
'en-US': 'https://example.com/en',
'ja-JP': 'https://example.com/ja'
}
}
};
}
3. 语言检测
首次访问时自动检测用户语言:
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
const intlMiddleware = createMiddleware({
locales: ['en', 'zh', 'ja'],
defaultLocale: 'zh',
localeDetection: true // 开启自动检测
});
export default function middleware(request: NextRequest) {
return intlMiddleware(request);
}
next-intl 会根据 Accept-Language 请求头自动选择语言。
4. 保持语言偏好
用户选择语言后,最好记住这个选择:
// 使用 Cookie 保存
const switchLanguage = (newLocale: string) => {
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`;
router.push(newPath);
};
next-intl 的中间件会自动读取这个 Cookie。
实战案例:完整的国际化项目
最后,我把之前做过的一个小项目的关键代码整理了一下,供参考。
项目结构:
├── app/
│ ├── [locale]/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── blog/
│ │ └── [slug]/
│ │ └── page.tsx
├── components/
│ ├── LanguageSwitcher.tsx
│ └── Navigation.tsx
├── messages/
│ ├── zh/
│ │ ├── common.json
│ │ └── blog.json
│ ├── en/
│ │ ├── common.json
│ │ └── blog.json
├── i18n.ts
├── middleware.ts
└── navigation.ts
navigation.ts(路由配置):
import { createSharedPathnamesNavigation } from 'next-intl/navigation';
export const locales = ['zh', 'en'] as const;
export const localePrefix = 'as-needed';
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales, localePrefix });
components/Navigation.tsx:
'use client';
import { Link } from '@/navigation';
import { useTranslations } from 'next-intl';
import LanguageSwitcher from './LanguageSwitcher';
export default function Navigation() {
const t = useTranslations('common.navigation');
return (
<nav className="flex items-center justify-between p-4">
<div className="flex gap-4">
<Link href="/">{t('home')}</Link>
<Link href="/blog">{t('blog')}</Link>
<Link href="/about">{t('about')}</Link>
</div>
<LanguageSwitcher />
</nav>
);
}
这个项目上线后,切换语言非常流畅,没遇到什么问题。
总结
Next.js 的国际化在 App Router 时代确实有点复杂,但掌握了 next-intl 之后,其实并不难:
- 核心配置:中间件 + i18n.ts +
[locale]目录 - 翻译使用:
useTranslationsHook 在 Server 和 Client Component 中都能用 - 路由处理:使用 next-intl 提供的 Link 和 Router
- 文件管理:按模块拆分 + TypeScript 类型检查
- 性能优化:静态生成 + 按需加载
说实话,最开始接触这套方案时觉得挺绕的,特别是中间件和动态路由那块。但多写几次就习惯了,现在做国际化项目基本都是这个套路。
如果你正在做或者准备做多语言项目,强烈建议试试 next-intl。虽然有学习成本,但长远来看值得投入时间。
参考资源
希望这篇文章能帮到正在折腾 Next.js 国际化的你!
Next.js国际化完整配置流程
从安装next-intl到配置多语言路由、翻译文件管理的完整步骤
⏱️ 预计耗时: 2 小时
- 1
步骤1: 安装next-intl和基础配置
安装:
```bash
npm install next-intl
```
创建翻译文件:
```
messages/
zh.json
en.json
```
配置middleware.ts:
```ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
export const config = {
matcher: ['/', '/(zh|en)/:path*']
}
```
配置app/[locale]/layout.tsx:
```tsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
export default async function LocaleLayout({
children,
params: { locale }
}) {
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
```
关键点:
• 使用[locale]动态路由
• 在layout中提供翻译
• 配置middleware处理语言切换 - 2
步骤2: 配置多语言路由方案
方案1:子路径(推荐)
• URL格式:/zh/about、/en/about
• 配置简单
• SEO友好
方案2:域名
• URL格式:zh.example.com、en.example.com
• 需要配置多个域名
• 更专业
方案3:Cookie
• 通过Cookie切换语言
• URL不包含语言前缀
• 适合单语言用户
配置子路径:
```ts
// i18n/routing.ts
export const routing = {
locales: ['zh', 'en'],
defaultLocale: 'zh'
}
```
使用:
```tsx
import { useTranslations } from 'next-intl'
export function Page() {
const t = useTranslations('common')
return <h1>{t('title')}</h1>
}
```
关键点:选择适合项目的路由方案,大多数项目用子路径就够了 - 3
步骤3: 管理翻译文件
创建翻译文件:
```json
// messages/zh.json
{
"common": {
"title": "欢迎",
"description": "这是一个多语言网站"
},
"nav": {
"home": "首页",
"about": "关于"
}
}
```
使用翻译:
```tsx
import { useTranslations } from 'next-intl'
export function Page() {
const t = useTranslations('common')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
)
}
```
类型安全:
```ts
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default
}))
```
关键点:
• 使用嵌套结构组织翻译
• 配合TypeScript实现类型安全
• 使用i18n Ally VSCode插件提升开发体验
常见问题
为什么App Router需要next-intl?
Pages Router:
• 有内置的i18n路由支持
• 在next.config.js中配置i18n字段
• 自动处理语言切换
App Router:
• 移除了内置i18n功能
• 需要使用第三方库
• next-intl是最受欢迎的选择
next-intl优势:
• App Router原生支持
• 类型安全
• 灵活的路由方案
• 强大的功能(复数、日期格式化等)
• 性能优秀
建议:如果使用App Router,优先选择next-intl。
next-intl有几种路由方案?
方案1:子路径(推荐)
• URL格式:/zh/about、/en/about
• 配置简单
• SEO友好
• 适合大多数项目
方案2:域名
• URL格式:zh.example.com、en.example.com
• 需要配置多个域名
• 更专业
• 适合大型项目
方案3:Cookie
• 通过Cookie切换语言
• URL不包含语言前缀
• 适合单语言用户
• 配置复杂
选择建议:
• 大多数项目 → 子路径
• 大型项目 → 域名
• 特殊需求 → Cookie
关键点:选择适合项目的路由方案,大多数项目用子路径就够了。
如何配置next-intl?
```bash
npm install next-intl
```
创建翻译文件:
```
messages/
zh.json
en.json
```
配置middleware.ts:
```ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
export const config = {
matcher: ['/', '/(zh|en)/:path*']
}
```
配置app/[locale]/layout.tsx:
```tsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
export default async function LocaleLayout({
children,
params: { locale }
}) {
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
```
关键点:
• 使用[locale]动态路由
• 在layout中提供翻译
• 配置middleware处理语言切换
如何管理翻译文件?
```json
// messages/zh.json
{
"common": {
"title": "欢迎",
"description": "这是一个多语言网站"
},
"nav": {
"home": "首页",
"about": "关于"
}
}
```
使用翻译:
```tsx
import { useTranslations } from 'next-intl'
export function Page() {
const t = useTranslations('common')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
)
}
```
类型安全:
```ts
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default
}))
```
关键点:
• 使用嵌套结构组织翻译
• 配合TypeScript实现类型安全
• 使用i18n Ally VSCode插件提升开发体验
建议:按功能模块组织翻译文件,避免单个文件过大。
next-intl支持哪些功能?
• 翻译文本(t函数)
• 复数处理
• 日期格式化
• 数字格式化
• 富文本支持
使用示例:
```tsx
import { useTranslations, useFormatter } from 'next-intl'
export function Page() {
const t = useTranslations('common')
const format = useFormatter()
return (
<div>
<h1>{t('title')}</h1>
<p>{format.dateTime(new Date(), { dateStyle: 'long' })}</p>
<p>{format.number(1234.56, { style: 'currency', currency: 'USD' })}</p>
</div>
)
}
```
优势:
• 功能强大
• 类型安全
• 性能优秀
• Server Component友好
建议:充分利用next-intl的功能,提升用户体验。
如何实现语言切换?
```tsx
import { Link } from '@/i18n/navigation'
<Link href="/about" locale="en">
English
</Link>
<Link href="/about" locale="zh">
中文
</Link>
```
使用useRouter:
```tsx
'use client'
import { useRouter, usePathname } from '@/i18n/navigation'
export function LanguageSwitcher() {
const router = useRouter()
const pathname = usePathname()
const switchLanguage = (locale: string) => {
router.replace(pathname, { locale })
}
return (
<button onClick={() => switchLanguage('en')}>
English
</button>
)
}
```
关键点:
• 使用next-intl提供的Link和useRouter
• 保持当前路径,只切换语言
• 用户体验好
建议:在导航栏或页脚添加语言切换按钮。
9 分钟阅读 · 发布于: 2025年12月25日 · 修改于: 2026年1月22日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南

评论
使用 GitHub 账号登录后即可评论