最近开发的项目涉及到了 react, 因为之前没用过 next.js, 发现文档比较乱,所以也是花了点时间,这里做个记录。
前提依赖:App 文件夹路由
{"next": "14.2.22","react-i18next": "^15.5.1","i18next": "^25.0.2","i18next-browser-languagedetector": "^8.1.0","i18next-resources-to-backend": "^1.2.1",
}
因为用的是 app 的路由模式,所以是不需要 next-i18next 的
1. 首先需要知道,next.js 主要做 ssr 的,一定要以这个为前提去考虑,所以说我们很容易想到两个事情
-1. 在服务器端渲染的页面,不能只有一个 i18n 实例,也就是说 “ssr页面” 访问的的 i18n 实例不能共用一个,因为嘉定 A 正在浏览 zh 网站,B 也浏览网站且切语言到 en,A 本地刷新也会变成 en, 因为他们共用一个 i18n 实例,也就是同一个语言环境(当然也可能你发现没这个问题,那应该是因为你在 middleware 中间件中做了一些事情,同时保证 “路径的语言”> “实例的语言”环境,所以所有人在刷新页面的时候都在一直切服务端的 i18n 实例。但这样一来如果出问题也很难排查)
-2. 在客户端渲染的页面,不应该从页面的部分切 locale,因为页面切了 locale, 服务器上的i18n 实例并不会改变。正确的做法应该是切路由(路径),让 middleware(中间件)去处理这个事,因为 next 不是单页面应用,而是从服务器拿页面的。
基于这两点,我直接贴代码和目录结构
next 项目初始都是从 layout 进来的,换句话说。就是从 服务器页面 => 客户端页面,也就是在服务器端其实已经知道了要渲染哪个页面的数据,那么语言就应该是静态的(只有一份,中文或者英文,因为你已经知道了要加载哪个语言)
首先创建 i18n 文件夹
里边包含这几个文件
locales 里放的是翻译文件
/** i18n.ts */
import { createInstance, type Resource } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { languages, defaultLang, namespaces, defaultNamespace } from "./setting";// runs on server
export async function loadI18nRecources(locale: string) {const instance = createInstance();await instance.use(initReactI18next).use(resourcesToBackend((language: string, namespace: string) =>import(`./locales/${language}/${namespace}`).then((result) => result.default))).init({lng: locale,ns: namespaces,preload: namespaces,interpolation: { escapeValue: false },});return instance.services.resourceStore?.data;
}// runs on client
export function createI18nInstance(locale: string, resources: Resource) {const instance = createInstance();instance.use(initReactI18next).init({lng: locale,fallbackLng: defaultLang,resources,supportedLngs: languages,ns: namespaces,defaultNS: defaultNamespace,fallbackNS: defaultNamespace,react: { useSuspense: false },preload: namespaces,interpolation: { escapeValue: false },});return instance;
}
/** * i18n-provider.tsx这个组件是用在 client 的,在页面使用它的时候,创建全局实例
*/"use client";import { I18nextProvider } from "react-i18next";
import { createI18nInstance } from "./i18n";export function I18nProvider({children,resources,locale,
}: ComponentProps<{ children: React.ReactNode; resources: any; locale: string }>) {const i18n = createI18nInstance(locale, resources);return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}
我定义以下几个变量,用在全局引用
/** setting.ts */
export const defaultNamespace = "common";
export const namespaces = [defaultNamespace,"404","home",
];
export const defaultLang = "en";
export const languages = [defaultLang, "ko", "ja"];
export const I18N_COOKIE_KEY = "i18n-locale";
export const HEADER_I18n_NAME = "x-i18next-locale";
namespaces 里放的是 locales 文件夹下的 文件名,在 i18n.ts 会通过 import(`./locales/${language}/${namespace}`).then((result) => result.default) 动态加载翻译数据
使用:
// src/app/[lang]/layout.tsx
import Header from "@/components/header";
import Footer from "@/components/footer";
import { languages } from "@/i18n/setting";
import { dir } from "i18next";
import { I18nProvider } from "@/i18n/i18n-provider";
import { loadI18nRecources } from "@/i18n/i18n";export function generateStaticParams() {return languages.map((lang) => ({ lang }));
}export default async function RootLayout({children,params: { lang },
}: Readonly<{children: React.ReactNode;params: { lang: string };
}>) {const resources = await loadI18nRecources(lang);return (<html lang={lang} dir={dir(lang)}><head></head><body><I18nProvider locale={lang} resources={resources}><Header />{children}<Footer /></I18nProvider></body></html>);
}
这就是 next App 路由模式做 i18n 正规做法,所有的方式都是按照官方文档严格执行的,大家可以放心使用
在页面中使用就很简单,比如 404 页面
const { t } = useTranslation("404");
t('someKey')
--- 这里我补充一个 404 not found 页面做 i18n 的方式,具体的理由和原因,大家可以参考我代码里的 Reference(网友总是比官方强)
创建文件
/** src/app/[lang]/[...not-found]/page.tsx */"use client";
/*** Reference: https://github.com/vercel/next.js/discussions/50518*/
import { useTranslation } from "react-i18next";function NotFoundView() {const { t } = useTranslation("404");return (<div><div>404</div><div>{t("pageNotFound")}</div></div>);
}export default function NotFoundDummy() {return <NotFoundView />;
}