NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软
引言
国际化(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
post
页面, 路由为:/[lang]/post
创建出来的项目, 目录树结构如下:
└── 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
来实现不同页面的跳转
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
中的所有动态路由参数内容, 如下代码所示:
// 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
组件到底是 服务端组件
还是 客户端组件
, 都是可以获取到我们需要的内容。
- 客户端组件:
// 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;
在 浏览器
控制台中打印内容如下:
- 服务端组件:
// 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
中的所有动态路由参数内容, 这里不限制 Layout
组件到底是 服务端组件
还是 客户端组件
, 都是可以获取到我们需要的内容。
- 客户端组件:
// 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}</>;
}
在 浏览器
控制台中打印内容如下:
- 服务端组件:
// 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
。
// 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
字段的
// 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 定义字典
如下代码所示, 我们定义了 三种语言
(en
、zh
、js
)的字典包
// src/dictionaries/en.json
{
"cart": "Add to Cart"
}
// src/dictionaries/zh.json
{
"cart": "加入购物车"
}
// src/dictionaries/ja.json
{
"cart": "カートに入れる"
}
3.2 使用
开始前, 我们需要写一个方法, 来获取当前语言环境对于的语言包:
- 这里使用了
import
方法, 目的是为了实现按需加载 - 同时导出了
getDictionary
方法, 该方法接收一个参数(当前语言环境), 并返回对应语言环境的字典
// 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
方法, 即可
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 测试
最后看下最终的效果吧
四、默认语言环境设置
现在我们直接访问 /
路由时是会报 404
错误, 如下所示:
这里是因为我们第一段是动态路由, 也是必须有滴!! 但是这里我希望第一段路由可以不填, 当没有填写第一段动态路由时也能正常访问页面, 当没有填写第一段路由(语言环境)时, 可以使用默认的语言环境!
4.1 设置固定的默认语言环境
首先
- 我们需要列出所有的语言列表, 用于判断当前路由是否包含了语言环境
- 我们需要定一个默认的语言环境
- 实现原理: 在中间件中根据路由进行检测, 如果路由没有带有语言环境则进行
重写
或则重定向
(为路由带上默认的语言环境)
- 创建中间件(创建
src/middleware.ts
文件), 并添加如下内容:
import { NextRequest, NextResponse } from 'next/server';
const NEXT_PUBLIC_LOCALES = ['en', 'zh', 'ja', 'ko']; // 语言列表
const DEFAULT_LOCALE = 'zh'; // 默认语言
const middleware = async (request: NextRequest) => {
const { pathname } = request.nextUrl;
if (NEXT_PUBLIC_LOCALES.every((v) => !pathname.startsWith(`/${v}`))) {
// 重写
return NextResponse.rewrite(new URL(`/${DEFAULT_LOCALE}${pathname}`, request.url));
// 如需要重定向, 可使用 NextResponse.redirect
}
};
export const config = {
// 中间件匹配规则
matcher: ['/', '/(en-US|zh-CN|zh|ko-KR|ja-JP)/:path*', '/((?!_next|_vercel|.*\\..*).*)'],
};
// 导出中间件
export default middleware;
- 最后效果如下: 当我们访问路由前面不带有语言环境时, 会展示语言环境内容
4.2 根据用户浏览器环境来选择
上文我们写死默认的语言环境, 下面我们希望能够根据
用户浏览器
的所设置的语言
来决定是否使用默认的语言
- 如果用户浏览器设置的语言
在
我们支持的语言列表, 则采用用户设置的语言
- 如果用户浏览器设置的语言
不
在我们支持的语言列表, 则使用我们设定的默认语言
- 如何获取用户浏览器的语言环境? 其实在中间件中我们能够拿到每个请求的
request
中就包含了我们所需要的信息, 打印下request
来看下里面的内容:
const middleware = async (request: NextRequest) => {
console.log(request);
// ....
}
如下图, 可以看到这里是有个 accept-language
请求头, 该字段则表示用户客户端所能接受的语言环境
- 开搞: 下面我们只需要拿到请求头
accept-language
和我们的语言列表进行一个匹配, 如果能够匹配上则直接默认使用该语言环境, 如果不行则使用我们定义的固定的默认语言环境
import { NextRequest, NextResponse } from 'next/server';
const NEXT_PUBLIC_LOCALES = ['en', 'zh', 'ja', 'ko'];
+ const DEFAULT_LOCALE = 'en';
const middleware = async (request: NextRequest) => {
+ console.log(request)
const { pathname } = request.nextUrl;
if (NEXT_PUBLIC_LOCALES.every((v) => !pathname.startsWith(`/${v}`))) {
+ const acceptLanguage = request.headers.get('accept-language');
+ const defaultLocale = NEXT_PUBLIC_LOCALES.find((v) => acceptLanguage?.includes(v)) || DEFAULT_LOCALE;
// 重定向
+ return NextResponse.rewrite(new URL(`/${defaultLocale}${pathname}`, request.url));
}
};
export const config = {
matcher: ['/', '/(en-US|zh-CN|zh|ko-KR|ja-JP)/:path*', '/((?!_next|_vercel|.*\\..*).*)'],
};
export default middleware;
最后效果如下, 虽然我代码里写死的固定语言是英语(en
), 但是因为我浏览器默认语言设置的是中文(zh
), 并且中文语言环境是在我项目的语言列表中的, 所以这里默认就会取中文(zh
)语言环境
五、总结
其实本文只是简单演示了下, 在 NextJS
中 i18n
实现的简单原理, 当然在实际项目中我们可能需要更多的基础功能, 比如: 记录用户当前选择的语言环境、在跳转时希望自动添加上当前语言环境对应的路由前缀、获取路由时希望自动过滤掉前面的语言环境等等, 而这些功能在 React
中实现起来其实并没有什么难的或者特别之处, 无非就是在切换路由时记录用户的选择, 并存到全局状态或者上下文中, 在后续进行相关操作时根据当前语言环境进行处理即可!
六、参考
转载自:https://juejin.cn/post/7380694342744735782