likes
comments
collection
share

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

作者站长头像
站长
· 阅读数 61

引言

国际化(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
  • post 页面, 路由为: /[lang]/post

创建出来的项目, 目录树结构如下:

└── app
    ├── Provider.tsx
    ├── [lang]
    │   ├── demo
    │   │   └── page.tsx
    │   ├── detail
    │   │   └── page.tsx
    │   ├── list
    │   │   └── page.tsx
    │   └── post
    │       └── page.tsx
    │   ├── page.tsx
    ├── ....

截图如下:

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

运行项目后, 浏览器访问 http://localhost:3001/zh-CN/detail 能够正常的展示页面

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

下面我们调整首页 /[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;

效果如下: 点击链接、能顺利调整页面, 并且路由前面都是带有 语言环境

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

二、获取动态路由参数

在上文我们定义了动态路由 [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;

最终在 浏览器 控制台将输出如下内容:

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

2.2 page.jsx

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

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

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

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

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

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

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

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

2.3 layout.tsx

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

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

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

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

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

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

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

补充: 不同于 Page 组件这里是没有 searchParams 参数的

2.4 generateMetadata

在服务端 page.jsxlayout.tsx 组件中, 我们可以导出一个 generateMetadata 方法, 该方法返回一个 Metadata 对象, 通过这种方式我们可以为不同的页面动态的设置 Metadata 值。该方法的第一个参数其实就是 page.jsxlayout.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;

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

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

注意 📢: 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 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

三、本地化

上面介绍了 NextJS 动态路由 可以帮我们应对不同语言环境, 并将动态路由参数 lang 转发到每个 LayoutPage 页面

接下来我们需要考虑的就是如何根据用户所选的语言环境, 呈现对应的语言内容, 当然这并不是 NextJS 独有的功能。

这里我们则需要提供每个语言环境的 字典 包, 该 字典 提供从某个 到本地化 字符串 的映射对象。针对不同的语言环境加载不同的的字典, 并将页面内容映射为对应语言环境。

3.1 定义字典

如下代码所示, 我们定义了 三种语言(enzhjs)的字典包

// 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 测试

最后看下最终的效果吧

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

四、默认语言环境设置

现在我们直接访问 / 路由时是会报 404 错误, 如下所示:

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

这里是因为我们第一段是动态路由, 也是必须有滴!! 但是这里我希望第一段路由可以不填, 当没有填写第一段动态路由时也能正常访问页面, 当没有填写第一段路由(语言环境)时, 可以使用默认的语言环境!

4.1 设置固定的默认语言环境

首先

  • 我们需要列出所有的语言列表, 用于判断当前路由是否包含了语言环境
  • 我们需要定一个默认的语言环境
  • 实现原理: 在中间件中根据路由进行检测, 如果路由没有带有语言环境则进行 重写 或则 重定向(为路由带上默认的语言环境)
  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;
  1. 最后效果如下: 当我们访问路由前面不带有语言环境时, 会展示语言环境内容

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

4.2 根据用户浏览器环境来选择

上文我们写死默认的语言环境, 下面我们希望能够根据 用户浏览器所设置的语言 来决定是否使用 默认的语言

  • 如果用户浏览器设置的语言 我们支持的语言列表, 则采用 用户设置的语言
  • 如果用户浏览器设置的语言 在我们支持的语言列表, 则使用 我们设定的默认语言
  1. 如何获取用户浏览器的语言环境? 其实在中间件中我们能够拿到每个请求的 request 中就包含了我们所需要的信息, 打印下 request 来看下里面的内容:
const middleware = async (request: NextRequest) => {
  console.log(request);
  // ....
}

如下图, 可以看到这里是有个 accept-language 请求头, 该字段则表示用户客户端所能接受的语言环境

NextJS 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

  1. 开搞: 下面我们只需要拿到请求头 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 国际化 - 原生实现国际化(Internationalization, 简称 i18n)是一个设计和开发软

五、总结

其实本文只是简单演示了下, 在 NextJSi18n 实现的简单原理, 当然在实际项目中我们可能需要更多的基础功能, 比如: 记录用户当前选择的语言环境、在跳转时希望自动添加上当前语言环境对应的路由前缀、获取路由时希望自动过滤掉前面的语言环境等等, 而这些功能在 React 中实现起来其实并没有什么难的或者特别之处, 无非就是在切换路由时记录用户的选择, 并存到全局状态或者上下文中, 在后续进行相关操作时根据当前语言环境进行处理即可!

六、参考

转载自:https://juejin.cn/post/7380694342744735782
评论
请登录