remix-i18next
翻譯您的 React Router 框架模式應用程式最簡單的方式。
[!IMPORTANT] 如果您仍在使用 Remix v2,請繼續使用 remix-i18next v6,因為 v7 僅與 React Router v7 相容。
為什麼選擇 remix-i18next?
- 易於設定,易於使用:設定僅需幾個步驟,且組態簡單。
- 沒有其他要求:
remix-i18next
簡化了您的 React Router 應用程式的國際化,而無需額外的依賴項。 - 生產就緒:
remix-i18next
支援從 loader 將翻譯和組態選項傳遞到路由中。 - 掌握控制權:
remix-i18next
不會隱藏組態,因此您可以新增任何想要的插件或隨意組態。
設定
[!TIP] 如果您正在使用 Remix with Vite,請查看 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.dev.org.tw/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
檔案中,如果您沒有 loader,請使用以下程式碼建立一個。
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>
);
}
最後,在任何您想要翻譯的路由中,您可以按照 i18next 文件使用 t()
函式,並使用預設命名空間的翻譯。
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 知道要使用哪個命名空間和語言,並且這個會使用您組態的後端載入正確的翻譯檔案。
翻譯 loader 或 action 內的文字
如果您需要在 loader 或 action 函式中取得翻譯的文字,例如翻譯稍後在 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 路徑名稱中尋找語言環境
如果您想將使用者語言環境保留在路徑名稱中,您有兩種可能的選項。
第一個選項是將 loader/action 參數中的參數傳遞給 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
。