nextjs13/14(App Router)国际化解决方案

nextjs13/14采用了新的AppRouter模式,这使原来基于next-i18next的国际化方案不再适用,官方给出的国际化样例又难以应用。当你决定看我这个文章的时候,想必你一定是遇到了多种多样的困难,废话不多说我们马上开始正题。

准备工作

官方推荐解决方案 如果你看到我的这个文章,想必是你已经看过这个解决方案了。

为什么不使用上面的方案:

  • 其中间件拦截了路由的配置影响了public下资源的加载
  • 使用了自定义的钩子方案,需要对服务器组件和客户端组件进行区分

参考i18nexus的教程,我给出了如下方案。

设置步骤

1. 安装必要依赖

bash 复制代码
pnpm add i18next react-i18next i18next-resources-to-backend next-i18n-router

2. 建立i18n配置文件

在你代码的组件结构中建立i18n文件夹,我的i18n文件夹在app文件夹中:

md 复制代码
-- app
    -- i18n
        -- locales
            -- zh
                -- common.json
                -- common2.json
            -- en
                -- common.json
                -- common2.json
        -- i18nConfig.ts
        -- index.ts
        -- type.ts

内部代码如下:

json 复制代码
// common.json
{"about":"关于","title":"国际化示例"}
ts 复制代码
// i18nConfig.ts
export function getNamespaces() {
  return ["common", "common2"];
}

export function getOptions(locale: string, namespaces?: string[]) {
  const ns = namespaces !== undefined ? namespaces : getNamespaces();
  return {
    lng: locale,
    fallbackLng: i18nConfig.defaultLocale,
    supportedLngs: i18nConfig.locales,
    defaultNS: ns[0],
    fallbackNS: ns[0],
    ns,
    preload: typeof window === "undefined" ? i18nConfig.locales : [],
  };
}

// 这个属性必须要用默认导出
const i18nConfig = {
  defaultLocale: "zh",
  locales: ["zh", "en", "fr"],
};
export default i18nConfig;
ts 复制代码
// index.ts
import { getOptions } from "@/i18n/i18nConfig";
import { InitOptions, createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { ITran } from "./type";

export default async function initTranslations(
  locale: string,
  namespaces?: string[],
) {
  const i18nInstance = createInstance();

  await i18nInstance
    .use(initReactI18next)
    .use(
      resourcesToBackend(
        (language: string, namespace: string) =>
          import(`app/i18n/locales/${language}/${namespace}.json`),
      ),
    )
    .init(getOptions(locale, namespaces));

  return i18nInstance as { t: ITran; options: InitOptions }; // 默认没有代码补全,我们自己手动提供类型
}
ts 复制代码
// type.ts
import common from "./locales/zh/common.json";
interface resource {
  common: typeof common;
  common2: typeof common2;
}
type ResourceKey = keyof resource;
export type LangKeys<T> = T extends ResourceKey ? keyof resource[T] : never;
export type ITran = (key: LangKeys<ResourceKey>) => string;

3. 提供客户端组件的上下文

tsx 复制代码
// TranslationsProvider.tsx
"use client";

import initTranslations from "@/i18n";
import { PropsWithChildren, useEffect, useState } from "react";
import { I18nextProvider } from "react-i18next";

let i18n: any;

export default function TranslationsProvider({
  children,
  locale,
  namespaces,
}: PropsWithChildren<{
  locale: string;
  namespaces: string[];
}>) {
  const [instance, setInstance] = useState(i18n);

  useEffect(() => {
    const init = async () => {
      if (!i18n) {
        const newInstance = await initTranslations(locale, namespaces);
        i18n = newInstance;
        setInstance(newInstance);
      } else {
        if (i18n.language !== locale) {
          i18n.changeLanguage(locale);
        }
      }
    };

    init();
  }, [locale, namespaces]);

  if (!instance) {
    return null;
  }

  return (
    <I18nextProvider i18n={instance} defaultNS={namespaces[0]}>
      {children}
    </I18nextProvider>
  );
}

4. 移动app中文件

默认情况下我们app目录中会有layout和page文件如下面结构所示

md 复制代码
--app
    --layout.ts
    --page.ts

移动为如下结构:

md 复制代码
--app
    --[locale]
        --layout.ts
        --page.ts

代码设置如下

ts 复制代码
// layout.ts
import { dir } from "i18next";
import i18nConfig from "../i18n/i18nConfig";

...
// 静态生成路由
export function generateStaticParams() {
  return i18nConfig.locales.map((locale) => ({ locale }));
}

export default function RootLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  return (
    <html lang={locale} dir={dir(locale)} data-theme='winter'>
      <body>{children}</body>
    </html>
  );
}
ts 复制代码
// page.ts
import TranslationsProvider from "@/common/TranslationsProvider";
import Header from "@/component/Header";
import initTranslations from "@/i18n";
export default async function Home({
  params: { locale },
}: {
  params: { locale: string };
}) {
  // 服务器组件上下文,如果有其它服务器组件可以再次调用该方法
  const { t, options } = await initTranslations(locale); 
  return (
    <TranslationsProvider namespaces={options.ns as string[]} locale={locale}>
      <main className=' relative flex max-h-screen min-h-screen flex-row'>
        <Header />
      </main>
    </TranslationsProvider>
  );
}

5. middleware.ts

在root目录新建middleware.ts文件,并在其中写入如下代码:

ts 复制代码
// moddleware.ts
import { i18nRouter } from "next-i18n-router";
import { NextRequest } from "next/server";
import i18nConfig from "./app/i18n/i18nConfig";

export function middleware(request: NextRequest) {
  return i18nRouter(request, i18nConfig);
}

// applies this middleware only to files in the app directory
export const config = {
  matcher: "/((?!api|static|.*..*|_next).*)",
};

有了上面中间件后,我们在访问域名时,会根据设置的语言转发到对应语言路由下,例

  • 浏览器语言是zh,我们当前设置的也是zh,这个时候请求localhost:3000可以直接访问,域名此时为localhost:3000, 等同于localhost:3000/zh
  • 浏览器语言是zh,我们当前设置是en,这个时候请求localhost:3000,域名会转发到localhost:3000/en

6. 提供hooks给客户端组件调用

ts 复制代码
// hooks.ts
// 其实客户端可以直接调用useTranslation()获取t,但没有类型提示,所以我们封装了一层
export function useClientTranslation() {
  const { t, i18n } = useTranslation();
  return { t, i18n } as { t: ITran; i18n: i18n };
}

// 可以同样的包装一层hook给服务组件使用
export function async useServiceTranslation() {
  return await initTranslations(locale);
}

// 改变语言的hook
export function useChangeLanguage() {
  const { i18n } = useTranslation();
  const currentLocale = i18n.language;
  const router = useRouter();
  const currentPathname = usePathname();

  const handleChange = (newLocale: string) => {
    // set cookie for next-i18n-router
    const days = 30;
    const date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    const expires = "; expires=" + date.toUTCString();
    document.cookie = `NEXT_LOCALE=${newLocale};expires=${expires};path=/`;

    router.push(currentPathname.replace(`/${currentLocale}`, `/${newLocale}`));

    router.refresh();
  };
  return { currentLocale, handleChange };
}
// 在非组件中引用
export function getTranslationWithoutReact() {
  const { t } = getI18n();
  return t as ITran;
}

结尾

至此,你已经拥有了一整套server/client组件国际化的解决方案,你是一个国际化专家了!

相关推荐
然我1 小时前
React 事件机制:从代码到原理,彻底搞懂合成事件的核心逻辑
前端·react.js·面试
光影少年1 小时前
react16-react19都更新哪些内容?
前端·react.js
小陈phd2 小时前
langchain从入门到精通(四十一)——基于ReACT架构的Agent智能体设计与实现
react.js·架构·langchain
16年上任的CTO2 小时前
一文讲清楚React中的key值作用与原理
前端·javascript·react.js·react key
幼年程序猿3 小时前
基于antd组件库,手写动态渲染表单项组件
react.js
FogLetter6 小时前
从原生JS事件到React事件机制:深入理解前端事件处理
前端·javascript·react.js
胡gh8 小时前
React组件实用,每个组件各司其职,成为信息管理大师
前端·react.js
sophie旭8 小时前
《深入浅出react》总结之 10.2 渲染阶段流程探秘-completeWork
前端·react.js·源码
默默地离开8 小时前
React自定义 Hooks 不用死磕,轻松拿捏也能站上技术金字塔尖!
前端·react.js·前端框架
然我8 小时前
把 useState 用明白:从基础到进阶,这些细节决定你的 React 代码质量
前端·javascript·react.js