remix-i18next
翻译 Remix 应用最简单的方式。
为什么选择 remix-i18next?
- 易于设置,易于使用:设置只需几个步骤,配置也很简单。
- 无需其他依赖:
remix-i18next
简化了 Remix 应用的国际化,无需额外的依赖项。 - 生产就绪:
remix-i18next
支持从加载器中传递翻译和配置选项到路由。 - 掌控全局:
remix-i18next
不会隐藏配置,因此您可以添加任何所需的插件或根据需要进行配置。
设置
[!TIP] 如果你使用的是带有 Vite 的 Remix,请查看 https://github.com/sergiodxa/remix-vite-i18next 获取示例应用程序,如果遇到问题,请将你的设置与示例进行比较。
安装
第一步是在你的项目中使用以下命令进行安装:
npm install remix-i18next i18next react-i18next i18next-browser-languagedetector
你需要配置一个 i18next 后端和语言检测器,在这种情况下,你也可以安装它们,在接下来的设置指南中,我们将使用 http 和 fs 后端。
npm install i18next-http-backend i18next-fs-backend
配置
首先,让我们创建一些翻译文件
public/locales/en/common.json
:
{
"greeting": "Hello"
}
public/locales/es/common.json
:
{
"greeting": "Hola"
}
接下来,设置你的 i18next 配置。
这两个文件可以放在你的应用文件夹中的任何位置。
对于此示例,我们将创建 app/i18n.ts
export default {
// This is the list of languages your application supports
supportedLngs: ["en", "es"],
// This is the language you want to use in case
// if the user language is not in the supportedLngs
fallbackLng: "en",
// The default namespace of i18next is "translation", but you can customize it here
defaultNS: "common",
};
然后创建一个名为 i18next.server.ts
的文件,其中包含以下代码
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next/server";
import i18n from "~/i18n"; // your i18n configuration file
let i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
},
// This is the configuration for i18next used
// when translating messages server-side only
i18next: {
...i18n,
backend: {
loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"),
},
},
// The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
// E.g. The Backend plugin for loading translations from the file system
// Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
plugins: [Backend],
});
export default i18next;
客户端配置
现在在你的 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) // Tell i18next to use the react-i18next plugin
.use(LanguageDetector) // Setup a client-side language detector
.use(Backend) // Setup your backend
.init({
...i18n, // spread the configuration
// This function detects the namespaces your routes rendered while SSR use
ns: getInitialNamespaces(),
backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" },
detection: {
// Here only enable htmlTag detection, we'll detect the language only
// server-side with remix-i18next, by using the `<html lang>` attribute
// we can communicate to the client the language detected server-side
order: ["htmlTag"],
// Because we only use htmlTag, there's no reason to cache the language
// on the browser, so we disable it
caches: [],
},
});
startTransition(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<StrictMode>
<RemixBrowser />
</StrictMode>
</I18nextProvider>
);
});
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.cn/requestidlecallback
window.setTimeout(hydrate, 1);
}
服务器端配置
并在你的 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"; // your i18n configuration file
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) // Tell our instance to use react-i18next
.use(Backend) // Setup our backend
.init({
...i18n, // spread the configuration
lng, // The locale we detected above
ns, // The namespaces the routes about to render wants to use
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);
});
}
使用
现在,在你的 app/root.tsx
或 app/root.jsx
文件中,如果还没有加载器,请创建以下代码的加载器。
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()
函数,根据 i18next 文档 并使用默认命名空间中的翻译。
import { useTranslation } from "react-i18next";
export default function Component() {
let { t } = useTranslation();
return <h1>{t("greeting")}</h1>;
}
如果你希望拆分翻译文件,你可以创建新的翻译文件,例如
public/locales/en/home.json
{
"title": "remix-i18n is awesome"
}
public/locales/es/home.json
{
"title": "remix-i18n es increíble"
}
并在你的路由中使用它们
import { useTranslation } from "react-i18next";
// This tells remix to load the "home" namespace
export let handle = { i18n: "home" };
export default function Component() {
let { t } = useTranslation("home");
return <h1>{t("title")}</h1>;
}
就是这样,对每个要翻译的路由重复最后一步,remix-i18next 会自动让 i18next 知道要使用哪些命名空间和语言,并且它会使用你配置的后端加载正确的翻译文件。
在加载器或操作中翻译文本
如果你需要在加载器或操作函数中获取翻译后的文本,例如翻译稍后在 MetaFunction 中使用的页面标题,你可以使用 i18n.getFixedT
方法获取 t
函数。
export async function loader({ request }: LoaderArgs) {
let t = await i18n.getFixedT(request);
let title = t("My page title");
return json({ title });
}
export let meta: MetaFunction = ({ data }) => {
return { title: data.title };
};
getFixedT
函数可以使用参数组合进行调用
getFixedT(request)
:将使用请求获取语言环境和配置中设置的defaultNS
或translation
(i18next 默认命名空间)。getFixedT("es")
:将使用指定的es
语言环境和配置中设置的defaultNS
或translation
(i18next 默认命名空间)。getFixedT(request, "common")
将使用请求获取语言环境和指定的common
命名空间以获取翻译。getFixedT("es", "common")
将使用指定的es
语言环境和指定的common
命名空间以获取翻译。getFixedT(request, "common", { keySeparator: false })
将使用请求获取语言环境和common
命名空间以获取翻译,并使用第三个参数中的选项初始化 i18next 实例。getFixedT("es", "common", { keySeparator: false })
将使用指定的es
语言环境和common
命名空间以获取翻译,并使用第三个参数中的选项初始化 i18next 实例。
如果始终需要设置相同的 i18next 选项,则可以在创建新实例时将其传递给 RemixI18Next。
export let i18n = new RemixI18Next({
detection: { supportedLanguages: ["es", "en"], fallbackLanguage: "en" },
// The config here will be used for getFixedT
i18next: {
backend: { loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json") },
},
// This backend will be used by getFixedT
backend: Backend,
});
这些选项将被传递给 getFixedT
的选项覆盖。
keyPrefix
选项与 getFixedT
使用 getFixedT
函数现在支持 keyPrefix
选项,允许你在翻译键前添加前缀。当你想对翻译进行命名空间而无需每次都指定完整的键路径时,这尤其有用。
以下是使用方法
export async function loader({ request }: LoaderArgs) {
// Assuming "greetings" namespace and "welcome" keyPrefix
let t = await i18n.getFixedT(request, "greetings", { keyPrefix: "welcome" });
let message = t("user"); // This will look for the "welcome.user" key in your "greetings" namespace
return json({ message });
}
此功能简化了使用嵌套深度较深的翻译键的工作,并增强了翻译文件的组织。
从请求 URL 路径名中查找语言环境
如果要将用户语言环境保留在路径名中,则有两种选择。
第一种选择是从加载器/操作参数将参数传递给 getFixedT
。这样,你将停止使用 remix-i18next 的语言检测功能。
第二种选择是将 findLocale
函数传递给 RemixI18Next 中的检测选项。
export let i18n = new RemixI18Next({
detection: {
supportedLanguages: ["es", "en"],
fallbackLanguage: "en",
async findLocale(request) {
let locale = request.url.pathname.split("/").at(1);
return locale;
},
},
});
findLocale
返回的语言环境将针对支持的语言环境列表进行验证,如果无效,则将使用回退语言环境。
从数据库查询语言环境
如果你的应用程序在数据库中存储用户语言环境,则可以使用 findLocale
函数查询数据库并返回语言环境。
export let i18n = new RemixI18Next({
detection: {
supportedLanguages: ["es", "en"],
fallbackLanguage: "en",
async findLocale(request) {
let user = await db.getUser(request);
return user.locale;
},
},
});
请注意,每次调用 getLocale
和 getFixedT
都会调用 findLocale
,因此务必使其尽可能快。
如果同时需要语言环境和 t
函数,则可以调用 getLocale
,并将结果传递给 getFixedT
。