有更新!
1. 更新了middleware.ts 中文件匹配的写法
2. 更新了ITran类型定义
3. 更新了initTranslations方法
4. 更新了TranslationsProvider方法
5. 更新了useChangeLanguage方法
6. 更新了在非react组件中使用翻译的方法
nextjs13/14
采用了新的AppRouter
模式,这使原来基于next-i18next
的国际化方案不再适用,官方给出的国际化样例又难以应用。当你决定看我这个文章的时候,想必你一定是遇到了多种多样的困难,废话不多说我们马上开始正题。
准备工作
官方推荐解决方案 如果你看到我的这个文章,想必是你已经看过这个解决方案了。
为什么不使用上面的方案:
- 其中间件拦截了路由的配置影响了public下资源的加载
- 使用了自定义的钩子方案,需要对服务器组件和客户端组件进行区分
参考i18nexus的教程,我给出了如下方案。
设置步骤
1. 安装必要依赖
pnpm add i18next react-i18next i18next-resources-to-backend next-i18n-router
2. 建立i18n配置文件
在你代码的组件结构中建立i18n文件夹,我的i18n文件夹在app文件夹中:
-- app
-- i18n
-- locales
-- zh
-- common.json
-- common2.json
-- en
-- common.json
-- common2.json
-- i18nConfig.ts
-- index.ts
-- type.ts
内部代码如下:
// common.json
{"about":"关于","title":"国际化示例"}
// i18nConfig.ts
import { Resource } from "i18next";
export function getNamespaces() {
return ["common", "common2"];
}
export function getOptions(
locale: string,
namespaces?: string[],
resources?: Resource,
) {
const ns = namespaces !== undefined ? namespaces : getNamespaces();
return {
lng: locale,
resources,
fallbackLng: i18nConfig.defaultLocale,
supportedLngs: i18nConfig.locales,
defaultNS: ns[0],
fallbackNS: ns[0],
ns,
preload: resources ? [] : i18nConfig.locales,
};
}
// 这个属性必须要用默认导出
const i18nConfig = {
defaultLocale: "zh",
locales: ["zh", "en", "fr"],
};
export default i18nConfig;
// 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[],
instance?: i18n,
resources?: Resource,
) {
const i18nInstance = instance || createInstance();
i18nInstance.use(initReactI18next);
if (!resources) {
i18nInstance.use(
resourcesToBackend((language: string, namespace: string) => {
return import(`./locales/${language}/${namespace}.json`);
}),
);
}
await i18nInstance.init(getOptions(locale, namespaces, resources));
const t: ITran = (key, namespace, occupied) => {
return (i18nInstance.t as any)(key, { ns: namespace ?? "common", ...occupied });
};
return {
i18n: i18nInstance,
resources: i18nInstance.services.resourceStore.data,
t,
};
}
// type.ts
import common from "./locales/zh/common.json";
interface resource {
common: typeof common;
common2: typeof common2;
}
// 定义一个辅助类型,用于获取 resource 中特定键的类型
type ResourceKeyType<T extends keyof resource> = keyof resource[T];
/**
* 定义函数类型
* key 为国际化中翻译的key值,该值集合由namespace确定
* namespace 命名空间,即国际化json文件的名字
* occupied 是占位值的替换
* 如:有一个字段写为"after_time_redirect_to": "{{time}}秒后,即将跳转至"
* occupied 写作{time: '3'}即可在翻译时进行替换
*/
export type ITran = <T extends keyof resource = "common">(
key: ResourceKeyType<T>,
namespace?: T,
occupied?: Record<string, string>,
) => string;
3. 提供客户端组件的上下文
// TranslationsProvider.tsx
"use client";
import initTranslations from "@/i18n";
import { tranInstanceManager } from '@/hook'
import { Resource, createInstance } from "i18next";
import { PropsWithChildren } from "react";
import { I18nextProvider } from "react-i18next";
export default function TranslationsProvider({
children,
locale,
namespaces,
resources,
}: PropsWithChildren<{
locale: string;
namespaces: string[];
resources: Resource;
}>) {
const i18n = createInstance();
tranInstanceManager.instance = i18n;
initTranslations(locale, namespaces, i18n, resources);
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}
4. 移动app中文件
默认情况下我们app目录中会有layout和page文件如下面结构所示
--app
--layout.ts
--page.ts
移动为如下结构:
--app
--[locale]
--layout.ts
--page.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>
);
}
// 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 ns = getNamespaces();
const { resources } = await initTranslations(locale, ns);
return (
<TranslationsProvider
namespaces={ns}
locale={locale}
resources={resources}
>
<main className=' relative flex max-h-screen min-h-screen flex-row'>
<Header />
</main>
</TranslationsProvider>
);
}
5. middleware.ts
在root目录新建middleware.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给客户端组件调用
// hooks.ts
// 其实客户端可以直接调用useTranslation()获取t,但没有类型提示,所以我们封装了一层
export function useClientTranslation() {
const { t: tt, i18n } = useTranslation();
const t: ITran = (key, namespace, occupied) => {
return (tt as any)(key, { ns: namespace ?? "common", ...occupied });
};
return { t, i18n } as { t: ITran; i18n: i18n };
}
// 改变语言的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=/`;
if (currentLocale === i18nConfig.defaultLocale) {
router.push("/" + newLocale + currentPathname);
} else {
router.push(
currentPathname.replace(`/${currentLocale}`, `/${newLocale}`),
);
}
router.refresh();
};
return { currentLocale, handleChange };
}
// 在非组件中引用
class TranInstanceManager {
private i18nInstance!: i18n;
get instance() {
return this.i18nInstance;
}
set instance(value: i18n) {
this.i18nInstance = value;
}
}
export const tranInstanceManager = new TranInstanceManager();
export function getTranslationWithoutReact() {
const i18n = tranInstanceManager.instance;
return i18n.t as unknown as ITran;
}
结尾
至此,你已经拥有了一整套server/client组件国际化的解决方案,你是一个国际化专家了!