引言
国际化(Internationalization
, 简称 i18n
)是一个设计和开发软件的过程, 使我们的系统可以在不同的地区和语言环境中使用, 而不需要进行大规模的改动。对于前端来说这通常涉及到日期、时间、数字、货币和文本的格式化和翻译。
刚好前段时间做的新项目, 需要在 NextJS
项目中增加对国际化的支持, 但是实际项目中我们使用的是社区的一个第三方库 next-intl。所以对于 NextJS
中国际化实现原理其实一直是一知半解的。
本文将借助 NextJs
的自身那一套路由配合中间件来简单实现 i18n
, 而这一切就变得很简单了。因为 NextJs
本身也是配套了一些国际化路由的支持, 也可以帮助我们轻松地实现 i18n
。下面我们将演示在不依赖任何第三方库情况下, 完成 i18n
配置, 通过这一步我们可以了解到在 NextJs
中 i18n
实现原理。
本文项目最终代码: next-play/tree/i18n-pure
一、定义路由
在 app
目录下, 创建一个动态路由 [lang] 而所有页面路由都创建在该路由, 这里我们创建几个页面对应路由和页面关系如下:
- 首页, 路由为
/[lang]
demo
页面, 路由为:/[lang]/demo
detail
页面, 路由为:/[lang]/detail
list
页面, 路由为:/[lang]/list
创建出来的项目, 目录树结果如下:
sh
└── app
├── Provider.tsx
├── [lang]
│ ├── demo
│ │ └── page.tsx
│ ├── detail
│ │ └── page.tsx
│ ├── list
│ │ └── page.tsx
│ └── post
│ └── page.tsx
│ ├── page.tsx
├── ....
截图如下:
运行项目后, 浏览器访问 http://localhost:3001/zh-CN/detail
能够正常的展示画面
下面我们调整首页 /[lang]/page.tsx
内容: 使用 Link
来实现不同页面的跳转
js
import Link from 'next/link';
const Home = () => {
return (
<main className="space-y-10 [&_>*]:block">
<Link href="/en/demo">demo</Link>
<Link href="/zh/detail">detail</Link>
<Link href="/ja/list">list</Link>
<Link href="/ko/post">post</Link>
</main>
);
};
export default Home;
效果如下: 点击链接、能顺利切换, 并且路由前面都是带有语言标识的
二、获取动态路由参数
在上文我们定义了动态路由 [lang]
下面我们介绍下如何在不同情况下, 获取动态路由 [lang]
中参数值
2.1 客户端组件
在客户端组件中, 我们可以通过 useParams
hooks
获取到 URL
中的所有动态路由参数内容, 如下代码所示:
js
// src/app/[lang]/post/page.tsx
'use client';
import { useParams } from 'next/navigation';
const Post = () => {
const { lang } = useParams();
console.log('%c [ lang ]', 'background:pink; color:#bf2c9f;', lang);
return <main>post</main>;
};
export default Post;
最终在 浏览器
控制台将输出如下内容:
2.2 page.jsx
NextJS
默认会将动态路由所有参数作为 Page
组件的 props
进行传递, 也就是说在 Page
组件内我们可以通过 props
获取到 URL
中的所有动态路由参数内容, 这里不限制 Page
组件到底是 服务端组件
还是 客户端组件
, 都是可以获取到我们需要的内容。
- 客户端组件:
js
// src/app/[lang]/post/page.tsx
'use client';
const Post = (props) => {
console.log('%c [ rest ]', 'background:pink; color:#bf2c9f;', props);
return <main>post</main>;
};
export default Post;
在 浏览器
控制台中打印内容如下:
- 服务端组件:
js
// src/app/[lang]/post/page.tsx
const Post = (props) => {
console.log('%c [ rest ]', 'background:pink; color:#bf2c9f;', props);
return <main>post</main>;
};
export default Post;
在 命令行
终端中打印内容如下:
补充:
searchParams
是URL
参数,NextJS
也会帮我们解析好传给Page
组件
2.3 layout.tsx
NextJS
默认会将动态路由所有参数作为 Layout
组件的 props
进行传递, 也就是说在 Layout
组件内我们可以通过 props
获取到 URL
中的所有动态路由参数内容, 这里不限制 Page
组件到底是 服务端组件
还是 客户端组件
, 都是可以获取到我们需要的内容。
- 客户端组件:
js
// src/app/[lang]/layout.tsx
'use client';
export default function RootLayout({
children,
...restProps
}: Readonly<{
children: React.ReactNode;
}>) {
console.log('%c [ restProps ]', 'background:pink; color:#bf2c9f;', restProps);
return <>{children}</>;
}
在 浏览器
控制台中打印内容如下:
- 服务端组件:
js
// src/app/[lang]/layout.tsx
export default function RootLayout({
children,
...restProps
}: Readonly<{
children: React.ReactNode;
}>) {
console.log('%c [ restProps ]', 'background:pink; color:#bf2c9f;', restProps);
return <>{children}</>;
}
在 命令行
终端中打印内容如下:
补充: 不同于
Page
组件这里是没有searchParams
参数的
2.4 generateMetadata
在服务端 page.jsx
或 layout.tsx
组件中, 我们可以导出一个 generateMetadata
方法, 该方法返回一个 Metadata
对象, 通过这种方式我们可以为不同的页面动态的设置 Metadata
值。该方法的第一个参数其实就是 page.jsx
或 layout.tsx
是 Props
值, 所以在该方法内, 我们其实也是可以拿到所有动态路由参数, 然后我们可以通过不同的动态路由值动态设置 Metadata
。
js
// src/app/[lang]/post/page.tsx
import { Metadata } from 'next';
/** 动态设置元数据 */
export const generateMetadata = async (props) => {
console.log('%c [ props ]-5', 'background:pink; color:#bf2c9f;', props);
return {} as Metadata;
};
const Post = () => {
return <main>post</main>;
};
export default Post;
在 命令行
终端中打印内容如下:
注意 📢: Layout
页面中 generateMetadata
是没有 searchParams
字段的
js
// src/app/[lang]/layout.tsx
import { Metadata } from 'next';
/** 动态设置元数据 */
export const generateMetadata = async (props) => {
console.log('%c [ props ]-5', 'background:pink; color:#bf2c9f;', props);
return {} as Metadata;
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <>{children}</>;
}
三、本地化
上面介绍了 NextJS
动态路由
可以帮我们应对不同语言环境, 并将动态路由参数 lang
转发到每个 Layout
和 Page
页面
接下来我们需要考虑的就是如何根据用户所选的语言环境, 呈现对应的语言内容, 当然这并不是 NextJS
独有的功能。
这里我们则需要提供每个语言环境的 字典
包, 该 字典
提供从某个 键
到本地化 字符串
的映射对象。针对不同的语言环境加载不同的的字典, 并将页面内容映射为对应语言环境。
3.1 定义字典
如下代码所示, 我们定义了三种语言的字典包
json
// src/dictionaries/en.json
{
"cart": "Add to Cart"
}
json
// src/dictionaries/zh.json
{
"cart": "加入购物车"
}
json
// src/dictionaries/ja.json
{
"cart": "カートに入れる"
}
3.2 使用
开始前, 我们需要写一个方法, 来获取当前语言环境对于的语言包:
- 这里使用了
import
方法, 目的是为了实现按需加载 - 同时导出了
getDictionary
方法, 该方法接收一个参数(当前语言环境), 并返回对应语言环境的字典
js
// src/app/dictionaries/index.ts
const dictionaries = {
en: () => import('./en.json').then((module) => module.default),
ja: () => import('./ja.json').then((module) => module.default),
zh: () => import('./zh.json').then((module) => module.default),
} as Record<string, () => Promise<Record<string, string>>>;
export const getDictionary = async (locale: string) => dictionaries[locale]();
最后再需要使用字典的地方, 调用 getDictionary
方法, 即可
js
import { getDictionary } from '@/dictionaries';
interface PostProps {
params: {
lang: string;
};
}
const Post = async ({ params: { lang } }: PostProps) => {
const dict = await getDictionary(lang); // en
return <main>{dict.cart}</main>;
};
export default Post;
3.3 测试
最后看下最终的效果吧