Next.js 15 + next-intl 4 国际化教程:路由与多语言 SEO
用 Next.js 15 App Router 和 next-intl 4 配置中英文路由、消息加载、语言切换、canonical、hreflang 与 sitemap,附版本说明和排错表。
10 min read · 1837 words
如果你正在使用 Next.js App Router,并希望网站支持中文、英文等多种语言,这篇教程会带你用 next-intl 完成一套可上线的国际化方案。
最终我们会得到 /zh、/en、/zh/blog 和 /en/blog 这样的独立语言 URL,同时处理翻译、语言切换、metadata、hreflang 与 sitemap。示例以 Next.js 15、React 19 和 next-intl 4 为基础。
这篇文章解决“怎么搭建”的问题。如果你更想知道常见错误和架构取舍,可以继续阅读《Next.js 国际化踩坑记录:我为什么最终选择 next-intl》。
本文固定使用 Next.js 15 的 middleware.ts。Next.js 16 将这个文件约定改名为 proxy.ts;升级时请按官方 Proxy 文档调整,不要混用版本示例。代码使用 src 目录,未展示的普通页面需要自行创建。
开始前:先确定 URL 策略
本文采用语言前缀路由:
/zh/about
/en/about
/zh/blog/post-slug
/en/blog/post-slug
它比只在 React state、Cookie 或查询参数中切换文案更适合公开内容:链接能保留语言,搜索引擎也能分别抓取每个版本。
1. 安装 next-intl
npm install next-intl
在项目根目录创建语言文件:
messages/
├── en.json
└── zh.json
{
"home": {
"title": "你好,世界",
"description": "欢迎来到我的网站"
}
}
翻译键最好按页面或业务域拆分,如 home、about、blog,不要把所有文案堆在同一层。
2. 创建路由与请求配置
创建 src/i18n/routing.ts:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['zh', 'en'],
defaultLocale: 'zh',
localePrefix: 'always'
});
再创建 src/i18n/request.ts,它负责按当前路由加载对应消息:
import {hasLocale} from 'next-intl';
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
3. 让 next-intl 插件接入 Next.js
在 next.config.mjs 中包装原有配置:
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
export default withNextIntl({});
这一步经常被漏掉。路径要与你的 request.ts 实际位置一致。
4. 配置 Middleware
如果项目使用 src 目录,把文件放在 src/middleware.ts;否则放在项目根目录。二者选一个即可。
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};
这个 matcher 会处理页面路由,同时跳过 API、Next.js 内部资源和带扩展名的静态文件。若项目有特殊公开路径,再按实际情况补充。
5. 建立 [locale] 路由结构
src/app/
└── [locale]/
├── layout.tsx
├── page.tsx
├── about/page.tsx
└── blog/page.tsx
在语言布局中校验 locale,并向客户端组件提供消息:
import {hasLocale, NextIntlClientProvider} from 'next-intl';
import {getMessages, setRequestLocale} from 'next-intl/server';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';
export default async function LocaleLayout({children, params}) {
const {locale} = await params;
if (!hasLocale(routing.locales, locale)) notFound();
setRequestLocale(locale);
const messages = await getMessages();
return (
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
);
}
根 app/layout.tsx 才负责输出 <html> 和 <body>;不要在嵌套的 [locale]/layout.tsx 中重复它们。
如需静态生成语言路由:
export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}));
}
6. 在服务端与客户端读取翻译
Server Component 优先使用 getTranslations:
import {getTranslations} from 'next-intl/server';
export default async function HomePage() {
const t = await getTranslations('home');
return <h1>{t('title')}</h1>;
}
需要交互的 Client Component 使用 useTranslations:
'use client';
import {useTranslations} from 'next-intl';
export default function Hero() {
const t = useTranslations('home');
return <h1>{t('title')}</h1>;
}
不要为了翻译把整个页面都改成 Client Component。尽量让内容在服务端输出,能减少客户端 JavaScript,也更利于首屏与抓取。
7. 创建类型安全的导航与语言切换
在 src/i18n/navigation.ts 中创建 next-intl 导航封装:
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
语言切换器可以保留当前 pathname:
'use client';
import {usePathname, useRouter} from '@/i18n/navigation';
import {useLocale} from 'next-intl';
export default function LanguageSwitcher() {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
return (
<button onClick={() => router.replace(pathname, {
locale: locale === 'zh' ? 'en' : 'zh'
})}>
{locale === 'zh' ? 'English' : '中文'}
</button>
);
}
普通的 next/link 没有 App Router 语言切换所需的 locale 行为,因此不要照搬 Pages Router 时代的写法。
8. 配置多语言 Metadata、canonical 与 hreflang
每个语言页面应有与正文一致的 title 和 description,并声明自己及其他语言版本:
export async function generateMetadata({params}) {
const {locale} = await params;
const title = locale === 'zh' ? '关于我' : 'About me';
return {
title,
alternates: {
canonical: `https://example.com/${locale}/about`,
languages: {
'zh-CN': 'https://example.com/zh/about',
en: 'https://example.com/en/about'
}
}
};
}
注意三点:canonical 指向当前语言页自身;每种语言都列出所有对应版本;只有真正互为翻译的页面才互相声明。
9. 用 App Router 生成多语言 Sitemap
不必额外安装包。创建 src/app/sitemap.ts:
export default function sitemap() {
return [{
url: 'https://example.com/zh/about',
lastModified: new Date('2026-06-25'), // Replace with the page's real update date
alternates: {
languages: {
'zh-CN': 'https://example.com/zh/about',
en: 'https://example.com/en/about'
}
}
}];
}
博客文章应逐篇生成 URL,并使用真实发布日期或更新时间。不要在每次构建时给所有旧文章伪造相同的“今天更新”。
10. 上线前检查清单
/zh与/en能直接访问和刷新- 不支持的 locale 返回 404,而不是静默回退成另一种语言
- 切换语言后仍停留在对应页面
- 页面可见正文、
lang、title 和 description 语言一致 - canonical 指向当前语言 URL
- 两种语言互相输出 hreflang
- sitemap 包含所有可索引语言页面
- robots.txt 没有误拦截语言目录
- 翻译缺失时不会在页面直接显示 key
常见问题
next-intl 和 Next.js 自带国际化有什么区别?
App Router 提供路由能力,但完整应用还需要消息加载、格式化、Server/Client Component API 和导航封装。next-intl 把这些环节组合在一起。
是否应该自动根据 IP 跳转语言?
可以把浏览器语言作为首次访问建议,但应允许用户切换,并始终保留可直接访问的独立语言 URL。不要让同一个 URL 因 IP 返回不可预测的不同正文。
中文和英文文章必须同时发布吗?
不必,但只有真正对应的页面才应互设 hreflang。尚未翻译的文章可以只保留现有语言版本。
总结
一套稳定的 Next.js 国际化方案,核心不是“把文字换掉”,而是让路由、内容、导航和搜索信号保持一致。先设计 /[locale] URL,再接入 next-intl,最后补齐 metadata、hreflang 与 sitemap,后续增加语言时会轻松很多。
常见报错与检查位置
| 问题 | 检查位置 |
|---|---|
| 找不到 next-intl 配置 | next.config.mjs 是否包装插件,request 文件路径是否一致 |
| 切换语言后仍显示旧文案 | 当前 URL 的 locale、消息加载路径、客户端 Provider |
| 动态路由参数读取报错 | Next.js 15 示例是否先 await params |
| 文章出现重复的 html/body | 项目是否同时在根布局和嵌套布局输出文档标签 |
/en/about 返回 404 | 示例结构中的 about/page.tsx 是否实际创建 |
示例中的语言切换器保留 pathname,不包含查询参数或 hash。若页面依赖筛选参数或锚点,需要在切换时显式保留这些状态。
官方文档
实现后可以用国际化踩坑排查表逐项验证,再提交 sitemap。
常见问题
- Next.js App Router 做国际化为什么推荐 next-intl?
- next-intl 同时支持 App Router、Server Components、Client Components 和基于语言前缀的路由,适合需要多语言内容与 SEO 的 Next.js 项目。
- 多语言页面一定要使用 /zh 和 /en 这样的独立 URL 吗?
- 如果希望不同语言分别被搜索引擎发现和收录,独立且可抓取的语言 URL 通常是更清晰、可靠的方案。
- Next.js 多语言网站还需要配置 hreflang 吗?
- 需要。hreflang 用于声明同一内容的不同语言版本,并应与 canonical、站点地图中的语言替代链接保持一致。