文章导读:本文介绍了在 Next.js 14 (基于App Router) 中实现 i18n 国际化多语言功能,并考虑在真实的场景中,一步步优化将功能完善。通过阅读完本文,你将立即掌握如何在 Next.js 中实现 i18n。
前言在互联网世界越来越扁平化的时代,产品的多语言显得越来越重要。幸运的在 Next.js 中通过简单的配置和代码即可快速支持多语言。但是,当我们在互联网上搜索 Next.js 如何支持多语言时,可能会看到各种实现方式、鱼龙混杂和奇技淫巧的方案,于是我们一头雾水,不禁怀疑人生:到底哪里出了问题?
今天,让我们从 0 到 1 在 Next.js 中实现一个多语言,揭开多语言的神秘面纱。
我们查看 Next.js 官方文档中的 i18n 介绍, https://nextjs.org/docs/app/building-your-application/routing/internationalization,比较清晰详细了,本文也将基于此篇文档制作。
开始之前,先看看最终运行效果:https://next-i18n-demo-two.vercel.app/
准备工作首先,我们初始化一个 Next.js app,
1 npx create-next-app@latest
请注意选择 App Router,此处我使用的是 TypeScript。
1 2 3 4 5 6 7 8 ❯ npx create-next-app@latest ✔ What is your project named? … `next-i18n-demo` ✔ Would you like to use TypeScript? … No / `Yes` ✔ Would you like to use ESLint? … No / `Yes` ✔ Would you like to use Tailwind CSS? … No / `Yes ✔ Would you like to use `src/` directory? … No / `Yes` ✔ Would you like to use App Router? (recommended) … No / `Yes` ✔ Would you like to customize the default import alias (@/*)? … `No` / Yes
本地启动,
打开 http://localhost:3000 看到程序运行正常。
国际化介绍在正式开始之前,我们先简单介绍一下国际化,国际化 internationalization ,简称 i18n ,也即在产品中支持多国语言文化和环境风俗,主要包括语言/时间/货币符号等。这篇文章中将只专注于语言部分。
在国际化的具体呈现上,常见的方式是网站默认进入某个语言的官网(通常是英文),并支持选择语言或地区,进行切换网站的不同语言版本。
具体实现方式上,有的网站以语言简称为前缀,如 en.wikipedia.org, zh.wikipedia.org;有的网站以语言简称作为路径后缀,如 aws.amazon.com/cn, aws.amazon.com/jp,也有以国家地区域名为区分的,如以前的 apple.cn, apple.jp。
其中诸如 en, zh, cn, jp ,也即语言编码,在不同版本的语言编码版本中略有不同,具体可参考文章下方参考资料。
在本文案例中,将以 ISO_3166 中的 en 和 zh 编码分别代表英文和中文。
开始配置多语言项目之前的文件结构:
1 2 3 4 5 6 7 8 9 10 11 12 ├── package.json ├── public │ ├── next.svg │ └── vercel.svg ├── src │ └── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── tailwind.config.ts └── tsconfig.json
我们在 app 目录新建一个文件夹 [lang],然后将 app 目录的 laytout.tsx 和 page.tsx 移入 [locales]中,
移动后的文件结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ├── package.json ├── postcss.config.mjs ├── public │ ├── next.svg │ └── vercel.svg ├── src │ └── app │ ├── [lang] │ │ ├── layout.tsx │ │ └── page.tsx │ ├── favicon.ico │ └── globals.css ├── tailwind.config.ts └── tsconfig.json
Tips:
注意同步修改 layout.tsx 中 globals.css 的引用位置。
接下来,我们定义不同语言的 json 资源文件,你可以放入你习惯的文件目录,我这里放入 public/dictionaries,格式如下:
en.json
1 2 3 4 5 6 7 8 9 10 { "page" : { "title" : "Next.js i18n Demo" , "desc" : "How to implement i18n with Next.js (based on App Router)" } , "home" : { "title" : "Hello, Next.js i18n" , "desc" : "This is a demo of Next.js i18n" } }
zh.json
1 2 3 4 5 6 7 8 9 10 { "page" : { "title" : "Next.js i18n 示例" , "desc" : "搞懂 Next.js 实现 i18n 国际化多语言(基于App Router)" } , "home" : { "title" : "你好, Next.js i18n" , "desc" : "这是一个 Next.js i18n 示例" } }
紧接着,我们创建一个文件,用于加载多语言资源文件并获取相应语言文本。
在 app/[lang] 目录添加 dictionaries.js,注意检查文件目录及文件名是正确并匹配的。
1 2 3 4 5 6 7 8 import 'server-only' const dictionaries = { en : () => import ('./dictionaries/en.json' ).then ((module ) => module .default ), zh : () => import ('./dictionaries/zh.json' ).then ((module ) => module .default ), } export const getDictionary = async (locale ) => dictionaries[locale]()
使用多语言我们在 pages.tsx 页面中使用多语言功能。
首先,为函数增加 lang 参数,注意为函数添加 async 关键字,
1 2 3 export default async function Home ({ params: { lang } }: { params: { lang: string } } ) { ... }
添加多语言的调用,
1 const t = await getDictionary (lang);
在页面上使用,为了方便我将 page.tsx 上默认的代码进行清理,只保留文本展示。
1 2 3 4 5 6 <main className="flex min-h-screen flex-col items-center justify-between p-24" > <p className ="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30" > {t.home.title} </p > {t.home .desc } </main>
重启程序或等程序热更新成功,分别打开不同语言的页面 http://localhost:3000/en http://localhost:3000/zh 即可看到效果。
设置默认语言看起来不错,但是细心的朋友会发现打开 http://localhost:3000 会出现 404 error。为了解决这个问题,我们需要在未选择语言时,默认设置一个语言。
为此,我们可以在 src 目录创建一个 middleware.ts ,然后复制文档中的代码。
核心逻辑很简单:
判断 URL 的 pathname 中是否含有某个语言标识,如果有则直接返回,否则在获取合适的语言后,将 URL 重定向为 /${locale}${pathname}
重点在 getLocale 函数中,我们需要指定合适的语言。在此处,我们先简单处理:使用默认的 defaultLocale = "en" 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { NextRequest , NextResponse } from "next/server" ;let locales = ["en" , "zh" ];let defaultLocale = "en" ;function getLocale (request: NextRequest ) { return defaultLocale; } export function middleware (request: NextRequest ) { const { pathname } = request.nextUrl ; const pathnameHasLocale = locales.some ( (locale ) => pathname.startsWith (`/${locale} /` ) || pathname === `/${locale} ` ); if (pathnameHasLocale) return ; const locale = getLocale (request); request.nextUrl .pathname = `/${locale} ${pathname} ` ; return NextResponse .redirect (request.nextUrl ); } export const config = { matcher : [ "/((?!_next).*)" , ], };
程序更新后,我们打开 http://localhost:3000/ 可以看到会自动跳转到设置的默认语言页面。
获取默认语言的优化在上一节获取默认语言时,我们简单处理为 defaultLocale = "en" ,更优雅的方式是:根据用户的系统或者浏览器的语言来设置默认语言 :
我们可以通过获取浏览器 HTTP headers 中的 Accept-Language 字段来达到目的。它的数据格式大致如下:
1 2 3 4 英文时: accept-language: en-US,en;q=0.5 中文时: accept-language: zh-CN,zh-Hans;q=0.9
我们将 middleware 改造如下:
从 HTTP headers 中获取 Accept-Language,如果为空则返回默认语言
解析 Accept-Language 中的语言列表,并根据配置的语言列表,匹配获取对应的语言(如果没有则返回默认语言)
安装依赖 @formatjs/intl-localematcher, negotiator, @types/negotiator,并实现如下逻辑:
1 2 3 4 5 6 7 function getLocale (request: NextRequest ) { const acceptLang = request.headers .get ("Accept-Language" ); if (!acceptLang) return defaultLocale; const headers = { "accept-language" : acceptLang }; const languages = new Negotiator ({ headers }).languages (); return match (languages, locales, defaultLocale); }
通过修改系统的语言,打开 http://localhost:3000 会自动跳转到同系统语言一致的页面,测试成功。
多语言的其它处理 存储用户网页语言更进一步地,我们可以在 Cookie 中存储用户网页中的语言,并在下次访问时使用:
1 2 3 4 5 6 7 if (request.cookies .has (cookieName)) { return request.cookies .get (cookieName)!.value ; } response.cookies .set (cookieName, locale);
网页标题描述等的多语言处理在网页 metadata 中使用多语言时,page.tsx添加如下代码:
1 2 3 4 5 6 7 export async function generateMetadata ({ params: { lang } } : { params: { lang: string } } ) { const t = await getDictionary (lang); return { title : t.page .title , description : t.page .desc , }; }
SSG 的多语言处理在处理静态站点(SSG)中使用多语言时,layout.tsx代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface LangParams { lang : string ; } export async function generateStaticParams ( ) { return [{ lang : "en" }, { lang : "zh" }]; } export default function RootLayout ({ children, params, }: Readonly<{ children: React.ReactNode; params: LangParams; }> ) { return ( <html lang ={params.lang} > <body className ={inter.className} > {children}</body > </html > ); }
切换多语言(语言选择器或链接)可根据实际情况添加语言选择器(下拉框)或不同的链接,从而跳转到对应语言的页面。
例如通过链接实现多语言切换:
1 2 3 4 5 <div className="space-x-2" > <Link href ="/en" > English</Link > <span > |</span > <Link href ="/zh" > Chinese</Link > </div>
尾声通过上述步骤的学习,我们初步熟悉并实践了在 Next.js 中使用多语言。千里之行,始于足下,国际化的工作不止于此,我们当然也还有尚未完善的地方,就留给屏幕前的你吧。
最后附上 middleware.ts 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import Negotiator from "negotiator" ;import { match } from "@formatjs/intl-localematcher" ;import { NextRequest , NextResponse } from "next/server" ;const locales = ["en" , "zh" ];const defaultLocale = "zh" ;const cookieName = "i18nlang" ;function getLocale (request: NextRequest ): string { if (request.cookies .has (cookieName)) return request.cookies .get (cookieName)!.value ; const acceptLang = request.headers .get ("Accept-Language" ); if (!acceptLang) return defaultLocale; const headers = { "accept-language" : acceptLang }; const languages = new Negotiator ({ headers }).languages (); return match (languages, locales, defaultLocale); } export function middleware (request: NextRequest ) { if (request.nextUrl .pathname .startsWith ("/_next" )) return NextResponse .next (); const { pathname } = request.nextUrl ; const pathnameHasLocale = locales.some ( (locale ) => pathname.startsWith (`/${locale} /` ) || pathname === `/${locale} ` ); if (pathnameHasLocale) return ; const locale = getLocale (request); request.nextUrl .pathname = `/${locale} ${pathname} ` ; const response = NextResponse .redirect (request.nextUrl ); response.cookies .set (cookieName, locale); return response; } export const config = { matcher : [ "/((?!_next).*)" , ], };
完整代码可在 https://github.com/xumeng/next-i18n-demo 获取。
最终运行效果:https://next-i18n-demo-two.vercel.app/
参考资料:https://nextjs.org/docs/app/building-your-application/routing/internationalization
https://en.wikipedia.org/wiki/ISO_3166
https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
https://en.wikipedia.org/wiki/IETF_language_tag
https://www.alchemysoftware.com/livedocs/ezscript/Topics/Catalyst/Language.htm