NextJS 国际化 - 原生实现

引言

国际化(Internationalization, 简称 i18n)是一个设计和开发软件的过程, 使我们的系统可以在不同的地区和语言环境中使用, 而不需要进行大规模的改动。对于前端来说这通常涉及到日期、时间、数字、货币和文本的格式化和翻译。

刚好前段时间做的新项目, 需要在 NextJS 项目中增加对国际化的支持, 但是实际项目中我们使用的是社区的一个第三方库 next-intl。所以对于 NextJS 中国际化实现原理其实一直是一知半解的。

本文将借助 NextJs 的自身那一套路由配合中间件来简单实现 i18n, 而这一切就变得很简单了。因为 NextJs 本身也是配套了一些国际化路由的支持, 也可以帮助我们轻松地实现 i18n。下面我们将演示在不依赖任何第三方库情况下, 完成 i18n 配置, 通过这一步我们可以了解到在 NextJsi18n 实现原理。

本文项目最终代码: 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 组件到底是 服务端组件 还是 客户端组件, 都是可以获取到我们需要的内容。

  1. 客户端组件:
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;

浏览器 控制台中打印内容如下:

  1. 服务端组件:
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;

命令行 终端中打印内容如下:

补充: searchParamsURL 参数, NextJS 也会帮我们解析好传给 Page 组件

2.3 layout.tsx

NextJS 默认会将动态路由所有参数作为 Layout 组件的 props 进行传递, 也就是说在 Layout 组件内我们可以通过 props 获取到 URL 中的所有动态路由参数内容, 这里不限制 Page 组件到底是 服务端组件 还是 客户端组件, 都是可以获取到我们需要的内容。

  1. 客户端组件:
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}</>;
}

浏览器 控制台中打印内容如下:

  1. 服务端组件:
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.jsxlayout.tsx 组件中, 我们可以导出一个 generateMetadata 方法, 该方法返回一个 Metadata 对象, 通过这种方式我们可以为不同的页面动态的设置 Metadata 值。该方法的第一个参数其实就是 page.jsxlayout.tsxProps 值, 所以在该方法内, 我们其实也是可以拿到所有动态路由参数, 然后我们可以通过不同的动态路由值动态设置 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 转发到每个 LayoutPage 页面

接下来我们需要考虑的就是如何根据用户所选的语言环境, 呈现对应的语言内容, 当然这并不是 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 测试

最后看下最终的效果吧

四、参考

参考资料: routing/internationalization

相关推荐
周三有雨6 分钟前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript
木古古1819 分钟前
使用chrome 访问虚拟机Apache2 的默认页面,出现了ERR_ADDRESS_UNREACHABLE这个鸟问题
前端·chrome·apache
爱米的前端小笔记28 分钟前
前端八股自学笔记分享—页面布局(二)
前端·笔记·学习·面试·求职招聘
loey_ln1 小时前
webpack配置和打包性能优化
前端·webpack·性能优化
建群新人小猿1 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
爱上语文1 小时前
HTML和CSS 表单、表格练习
前端·css·html
djk88881 小时前
Layui Table 行号
前端·javascript·layui
今天啥也没干1 小时前
使用 Sparkle 实现 macOS 应用自定义更新弹窗
前端·javascript·swift
痴憨道人1 小时前
openharmony sdk描述
javascript