remix-i18next
The easiest way to translate your Remix apps.
Why remix-i18next?
- Easy to set up, easy to use: setup only takes a few steps, and configuration is simple.
- No other requirements:
remix-i18next
simplifies internationalisation for your Remix app without extra dependencies. - Production ready:
remix-i18next
supports passing translations and configuration options into routes from the loader. - Take the control:
remix-i18next
doesn't hide the configuration so you can add any plugin you want or configure as pleased.
Setup
Installation
The first step is to install it in your project with
npm install remix-i18next i18next react-i18next i18next-http-backend i18next-fs-backend i18next-browser-languagedetector
If you're going to use TypeScript it is recommended to install @types/i18next-fs-backend
as well:
npm install --save-dev @types/i18next-fs-backend
Configuration
Then create a i18n.server.ts
file somewhere in your app and add the following code:
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next";
export let i18n = new RemixI18Next({
detection: {
supportedLanguages: ["es", "en"],
fallbackLanguage: "en",
},
i18next: {
backend: { loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json") },
},
backend: Backend,
});
Client-side configuration
Now in your entry.client.tsx
replace the default code with this:
import { RemixBrowser } from "@remix-run/react";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { hydrate } from "react-dom";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next";
i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(Backend)
.init({
supportedLngs: ["es", "en"],
defaultNS: "common",
fallbackLng: "en",
react: { useSuspense: false },
ns: getInitialNamespaces(),
backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" },
detection: {
order: ["htmlTag"],
caches: [],
},
})
.then(() => {
return hydrate(
<I18nextProvider i18n={i18next}>
<RemixBrowser />
</I18nextProvider>,
document
);
});
Server-side configuration
And in your entry.server.tsx
replace the code with this:
import { RemixServer } from "@remix-run/react";
import type { EntryContext } from "@remix-run/server-runtime";
import { createInstance } from "i18next";
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { renderToString } from "react-dom/server";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { i18n } from "./services/i18n.server";
export default async function handleRequest(
request: Request,
statusCode: number,
headers: Headers,
context: EntryContext
) {
let instance = createInstance();
let lng = await i18n.getLocale(request);
let ns = i18n.getRouteNamespaces(context);
await instance
.use(initReactI18next)
.use(Backend)
.init({
supportedLngs: ["es", "en"],
defaultNS: "common",
fallbackLng: "en",
react: { useSuspense: false },
lng,
ns,
backend: {
loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"),
},
});
let markup = renderToString(
<I18nextProvider i18n={instance}>
<RemixServer context={context} url={request.url} />
</I18nextProvider>
);
headers.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: statusCode,
headers: headers,
});
}
Usage
Now, in your root
file create a loader if you don't have one with the following code and also run the useSetupTranslations
hook on the Root component.
import { json, LoaderFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { i18n } from "~/i18n.server.ts";
import { useChangeLanguage } from "remix-i18next";
import { useTranslation } from "react-i18next";
type LoaderData = { locale: string };
export let loader: LoaderFunction = async ({ request }) => {
let locale = await i18n.getLocale(request);
return json<LoaderData>({ locale });
};
export let handle = {
i18n: ["translations", "root"],
};
export default function Root() {
let { locale } = useLoaderData<LoaderData>();
let { i18n } = useTranslation();
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
<head>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Finally, in any route you want to translate you can do this:
import { json, LoaderFunction } from "@remix-run/node";
import { useTranslation } from "react-i18next";
export let handle = {
i18n: "home",
};
export default function Component() {
let { t } = useTranslation("home");
return <h1>{t("title")}</h1>;
}
And that's it, repeat the last step for each route you want to translate, remix-i18next will automatically let i18next what namespaces and language to use and this one will load the correct translation files using your configured backend.
Translating text inside loaders or actions
If you need to get translated texts inside a loader or action function, for example to translate the page title used later in a MetaFunction, you can use the i18n.getFixedT
method to get a t
function.
export let loader: LoaderFunction = async ({ request }) => {
let t = await i18n.getFixedT(request);
let title = t("My page title");
return json({ title });
};
export let meta: MetaFunction = ({ data }) => {
return { title: data.title };
};
The getFixedT
function can be called using a combination of parameters:
getFixedT(request)
: will use the request to get the locale and the defaultNS
set in the config or translation
(the i18next default namespace)getFixedT("es")
: will use the specified es
locale and the defaultNS
set in config, or translation
(the i18next default namespace)getFixedT(request, "common")
will use the request to get the locale and the specified common
namespace to get the translations.getFixedT("es", "common")
will use the specified es
locale and the specified common
namespace to get the translations.getFixedT(request, "common", { keySeparator: false })
will use the request to get the locale and the common
namespace to get the translations, also use the options of the third argument to initialize the i18next instance.getFixedT("es", "common", { keySeparator: false })
will use the specified es
locale and the common
namespace to get the translations, also use the options of the third argument to initialize the i18next instance.
If you always need to set the same i18next options, you can pass them to RemixI18Next when creating the new instance.
export let i18n = new RemixI18Next({
detection: { supportedLanguages: ["es", "en"], fallbackLanguage: "en" },
i18next: {
backend: { loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json") },
},
backend: Backend,
});
This options will be overwritten by the options provided to getFixedT
.