JS 中不同框架都有自己的多语言库,在 Remix 使用多语言,需要安装 remix-i18next 这个库。这个包是基于 i18next 开发的,使用方式可以到官网查看。 Remix-i18next 安装步骤如下:
安装依赖
npm install remix-i18next i18next react-i18next i18next-browser-languagedetector i18next-http-backend i18next-fs-backend
创建语言文件
创建英文和中文的语言文件。
public/locales/en/common.json
{
"greeting": "Hello"
}
public/locales/zh/common.json
{
"greeting": "你好"
}
i18n 配置文件
app/i18n.ts
export default {
// 这是您的应用程序支持的语言列表
supportedLngs: ["en", "zh"],
// 如果用户的语言不在 supportedLngs 中,
// 您希望使用的默认语言
fallbackLng: "zh",
// i18next 的默认命名空间是 "translation",
// 但您可以在这里自定义
defaultNS: "common",
};
app/i18next.server.ts
import Backend from "i18next-fs-backend"; // 导入 i18next 文件系统后端
import { resolve } from "node:path"; // 导入 node 的 path 解析函数
import { RemixI18Next } from "remix-i18next/server"; // 导入 Remix 的 i18next 服务器端模块
import i18n from "~/i18n"; // 导入您的 i18n 配置文件
let i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs, // 支持的语言列表
fallbackLanguage: i18n.fallbackLng, // 默认回退语言
},
// 以下是仅在服务器端翻译消息时使用的 i18next 配置
i18next: {
...i18n,
backend: {
loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"), // 指定翻译文件加载路径
},
},
// 您希望 RemixI18next 在 loaders 和 actions 中通过 `i18n.getFixedT` 使用的 i18next 插件。
// 例如:文件系统后端插件用于从文件系统加载翻译
// 提示:您可以将 `resources` 传递给 `i18next` 配置并在此处避免使用后端
plugins: [Backend],
});
export default i18next; // 导出 i18next 配置
Remix Client
app/entry.client.tsx
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import i18n from "./i18n";
import i18next from "i18next";
import { I18nextProvider, initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { getInitialNamespaces } from "remix-i18next/client";
async function hydrate() {
await i18next
.use(initReactI18next) // 告诉 i18next 使用 react-i18next 插件
.use(LanguageDetector) // 设置客户端语言检测
.use(Backend) // 配置您的后端
.init({
...i18n, // 展开配置
// 此功能检测您的路由在服务端渲染时使用的命名空间
ns: getInitialNamespaces(),
backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" },
detection: {
// 这里仅启用 htmlTag 检测, 我们将仅在服务器端使用 remix-i18next 检测语言
// 通过使用 `<html lang>` 属性,我们可以通知客户端服务器端检测到的语言
order: ["htmlTag"],
// 因为我们只使用 htmlTag,没有理由在浏览器上缓存语言,所以我们禁用它
caches: [],
},
});
startTransition(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<StrictMode>
<RemixBrowser />
</StrictMode>
</I18nextProvider>,
);
});
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
} else {
// Safari 不支持 requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1);
}
Remix Server
app/entry.server.tsx
import { PassThrough } from "stream";
import {
createReadableStreamFromReadable,
type EntryContext,
} from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { createInstance } from "i18next";
import i18next from "./i18next.server";
import { I18nextProvider, initReactI18next } from "react-i18next";
import Backend from "i18next-fs-backend";
import i18n from "./i18n"; // 你的 i18n 配置文件
import { resolve } from "node:path";
const ABORT_DELAY = 5000; // 终止延迟时间
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
let callbackName = isbot(request.headers.get("user-agent"))
? "onAllReady" // 当所有内容准备就绪时
: "onShellReady"; // 当外壳准备就绪时
let instance = createInstance();
let lng = await i18next.getLocale(request); // 获取请求的语言环境
let ns = i18next.getRouteNamespaces(remixContext); // 获取即将渲染的路由的命名空间
await instance
.use(initReactI18next) // 告诉我们的实例使用 react-i18next
.use(Backend) // 设置我们的后端
.init({
...i18n, // 展开配置
lng, // 我们上面检测到的语言环境
ns, // 路由想要使用的命名空间
backend: { loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json") }, // 加载路径
});
return new Promise((resolve, reject) => {
let didError = false;
let { pipe, abort } = renderToPipeableStream(
<I18nextProvider i18n={instance}>
<RemixServer context={remixContext} url={request.url} />
</I18nextProvider>,
{
[callbackName]: () => {
let body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html"); // 设置内容类型
resolve(
new Response(stream, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
}),
);
pipe(body);
},
onShellError(error: unknown) { // 外壳错误处理
reject(error);
},
onError(error: unknown) { // 错误处理
didError = true;
console.error(error); // 打印错误信息
},
},
);
setTimeout(abort, ABORT_DELAY); // 设置终止时间
});
}
修改入口文件
修改 root.tsx 文件,添加 i18n相关配置。
import { useChangeLanguage } from "remix-i18next/react";
import { useTranslation } from "react-i18next";
import i18next from "~/i18next.server";
export async function loader({ request }: LoaderArgs) {
let locale = await i18next.getLocale(request);
return json({ locale });
}
export let handle = {
// In the handle export, we can add a i18n key with namespaces our route
// will need to load. This key can be a single string or an array of strings.
// TIP: In most cases, you should set this to your defaultNS from your i18n config
// or if you did not set one, set it to the i18next default namespace "translation"
i18n: "common",
};
export default function Root() {
// Get the locale from the loader
let { locale } = useLoaderData<typeof loader>();
let { i18n } = useTranslation();
// This hook will change the i18n instance language to the current locale
// detected by the loader, this way, when we do something to change the
// language, this locale will change and i18next will load the correct
// translation files
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
把 t 方法引进来直接使用即可。
let { i18n, t} = useTranslation();
配置生效
更多的用法可以参考官网,例如在语言文件中添加参数。
#配置文件
en: {
translation: {
greeting: "Hello, {{name}}! Welcome to our website."
}
}
# 参数
t 将参数传入
t('greeting', { name: userName });