Remix Integration
Learn how to integrate i18next with Remix applications for internationalization.
Integrate i18next with your Remix application to support multiple languages using Lengrise-managed translations, with both client and server-side capabilities.
Prerequisites
- Remix application (v1.14.0+)
- Node.js (v16.0.0+)
- Package manager (npm, yarn, or pnpm)
- Translations downloaded from Lengrise
Pull Translation Files
First, you must pull your translation files from Lengrise API. These files will contain all the translated text for your application.
- Go to the Installation Guide and follow the steps to download your translations
- Place the downloaded JSON files in your project as shown in the project structure below
- Ensure that each language has its own JSON file named with the language code (e.g.,
en.json
,es.json
)
Integration Setup
After downloading your translation files, install the necessary i18next packages:
npm install i18next react-i18next i18next-fs-backend i18next-browser-languagedetector remix-i18next --save
Project Structure
Configuration Steps
1. Set up server-side i18n configuration
Create a file at app/utils/i18n.server.ts
:
import { RemixI18Next } from "remix-i18next";
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { i18nConfig } from "./i18n.config";
// Setting up i18next backend for server-side
let i18next = new RemixI18Next({
detection: {
// Detect language from query string, cookie or Accept-Language header
supportedLanguages: i18nConfig.supportedLngs,
fallbackLanguage: i18nConfig.fallbackLng,
},
// Set up i18next server-side instance
i18next: {
...i18nConfig,
backend: {
loadPath: resolve("./public/locales/{{lng}}.json"),
},
},
plugins: [Backend],
});
export default i18next;
2. Create a shared i18n configuration
Create a file at app/utils/i18n.config.ts
:
// Shared configuration between client and server
export const i18nConfig = {
// Default language
fallbackLng: "en",
// Supported languages
supportedLngs: ["en", "es"],
// Namespace used in both client and server
defaultNS: "translations",
// Disable suspense mode in react-i18next
react: {
useSuspense: false,
},
// Don't escape translations with HTML
interpolation: {
escapeValue: false,
},
};
3. Set up client-side i18n configuration
Create a file at app/utils/i18n.client.ts
:
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { getInitialNamespaces } from "remix-i18next";
import { i18nConfig } from "./i18n.config";
// Initialize client-side i18next
i18next
// Detect user language
.use(LanguageDetector)
// Load translations using http backend
.use(Backend)
// Pass the i18n instance to react-i18next
.use(initReactI18next)
// Initialize i18next
.init({
...i18nConfig,
// Namespace pre-loaded on the server in entry.server.tsx
ns: getInitialNamespaces(),
// Client-side backend configuration
backend: {
loadPath: "/locales/{{lng}}.json",
},
// Detects language from URL, cookie, and browser
detection: {
order: ["path", "cookie", "navigator"],
lookupFromPathIndex: 0,
caches: ["cookie"],
},
});
export default i18next;
4. Update entry.server.tsx
Modify your app/entry.server.tsx
to handle i18n on the server:
import { PassThrough } from "stream";
import { Response } 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 Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { I18nextProvider, initReactI18next } from "react-i18next";
import i18next from "./utils/i18n.server";
import { i18nConfig } from "./utils/i18n.config";
const ABORT_DELAY = 5000;
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
// Get language from URL or Accept-Language header
const instance = createInstance();
const lng = await i18next.getLocale(request);
const ns = i18next.getRouteNamespaces(remixContext);
// Initialize i18next on the server
await instance
.use(initReactI18next)
.use(Backend)
.init({
...i18nConfig,
lng,
ns,
backend: {
loadPath: resolve("./public/locales/{{lng}}.json"),
},
});
// Handle bot requests differently (for SEO)
const callbackName = isbot(request.headers.get("user-agent"))
? "onAllReady"
: "onShellReady";
return new Promise((resolve, reject) => {
let didError = false;
const { pipe, abort } = renderToPipeableStream(
<I18nextProvider i18n={instance}>
<RemixServer context={remixContext} url={request.url} />
</I18nextProvider>,
{
[callbackName]: () => {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(body, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
})
);
pipe(body);
},
onShellError: (err: unknown) => {
reject(err);
},
onError: (error: unknown) => {
didError = true;
console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
5. Update entry.client.tsx
Modify your app/entry.client.tsx
to handle i18n on the client:
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import i18next from "./utils/i18n.client";
import { I18nextProvider, initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { getInitialNamespaces } from "remix-i18next";
import { i18nConfig } from "./utils/i18n.config";
// Initialize i18next for the browser
async function hydrate() {
await i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(Backend)
.init({
...i18nConfig,
ns: getInitialNamespaces(),
backend: {
loadPath: "/locales/{{lng}}.json",
},
detection: {
order: ["path", "cookie", "navigator"],
},
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<I18nextProvider i18n={i18next}>
<RemixBrowser />
</I18nextProvider>
</StrictMode>
);
});
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
} else {
window.setTimeout(hydrate, 1);
}
6. Update root.tsx
Modify your app/root.tsx
to handle i18n at the root level:
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from "@remix-run/react";
import { json, LoaderFunction } from "@remix-run/node";
import { useTranslation } from "react-i18next";
import { useChangeLanguage } from "remix-i18next";
import i18next from "./utils/i18n.server";
export const loader: LoaderFunction = async ({ request }) => {
// Get the user's locale from the server instance of i18next
const locale = await i18next.getLocale(request);
// Return locale information to the client
return json({ locale });
};
export default function App() {
// Get locale information from the loader
const { locale } = useLoaderData<{ locale: string }>();
const { i18n } = useTranslation();
// This hook will change the i18n instance language to the current locale
useChangeLanguage(locale);
// Add HTML lang attribute based on the current language
return (
<html lang={locale} dir={i18n.dir()}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
7. Create a Language Switcher Component
Create a file at app/components/LanguageSwitcher.tsx
:
import { useTranslation } from "react-i18next";
import { Form } from "@remix-run/react";
export default function LanguageSwitcher() {
const { i18n } = useTranslation();
// We use Remix Form component to handle language changes with a POST action
return (
<div className="language-switcher">
<Form method="post" action="/set-language">
<label htmlFor="language-select">Select Language:</label>
<select
id="language-select"
name="language"
defaultValue={i18n.language}
onChange={(e) => {
const form = e.target.form;
if (form) form.submit();
}}
>
<option value="en">English</option>
<option value="es">Español</option>
</select>
</Form>
</div>
);
}
8. Create a Language Setter Route
Create a file at app/routes/set-language.tsx
:
import { ActionFunction, redirect } from "@remix-run/node";
import { i18nConfig } from "~/utils/i18n.config";
export const action: ActionFunction = async ({ request }) => {
// Get the form data
const formData = await request.formData();
const language = formData.get("language") as string;
// Validate that the language is supported
if (!i18nConfig.supportedLngs.includes(language)) {
return redirect("/");
}
// Create a cookie with the language choice
const cookieHeader = request.headers.get("Cookie") || "";
// Get the URL to redirect to (defaulting to home)
const redirectTo = (formData.get("redirectTo") as string) || "/";
// Create a Response to set the cookie and redirect
return redirect(redirectTo, {
headers: {
"Set-Cookie": `i18next=${language}; Path=/; Max-Age=${
60 * 60 * 24 * 365
}`,
},
});
};
Using Translations
In Loaders
To use translations in your Remix loaders:
import { json, LoaderFunction } from "@remix-run/node";
import i18next from "~/utils/i18n.server";
export const loader: LoaderFunction = async ({ request }) => {
// Get the locale from the request
const locale = await i18next.getLocale(request);
// Get an instance of i18next for the request
const t = await i18next.getFixedT(request);
// Use translations in your loader
const pageTitle = t("page.title");
const welcomeMessage = t("welcome.message", { name: "Remix Developer" });
return json({
pageTitle,
welcomeMessage,
locale,
});
};
In Components
Use the useTranslation
hook from react-i18next in your components:
import { useTranslation } from "react-i18next";
import { useLoaderData } from "@remix-run/react";
import LanguageSwitcher from "~/components/LanguageSwitcher";
export default function Index() {
const { t } = useTranslation();
const { pageTitle, welcomeMessage } = useLoaderData();
return (
<div>
<h1>{pageTitle}</h1>
<p>{welcomeMessage}</p>
<LanguageSwitcher />
<section>
<h2>{t("features.title")}</h2>
<ul>
<li>{t("features.easy")}</li>
<li>{t("features.fast")}</li>
<li>{t("features.flexible")}</li>
</ul>
</section>
</div>
);
}
Translation with Variables
Your translations can include variables:
import { useTranslation } from "react-i18next";
export default function Greeting() {
const { t } = useTranslation();
const username = "John";
return <p>{t("greeting", { name: username })}</p>;
}
Your en.json
file should contain:
{
"greeting": "Hello, {{name}}!"
}
And your es.json
file:
{
"greeting": "¡Hola, {{name}}!"
}
Resources
For more detailed information, check out these resources: