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.

  1. Go to the Installation Guide and follow the steps to download your translations
  2. Place the downloaded JSON files in your project as shown in the project structure below
  3. 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

package.jsonadd
remix.config.jsadd
appadd
root.tsxadd
entry.client.tsxadd
entry.server.tsxadd
routesadd
_index.tsxadd
utilsadd
i18n.server.tsadd
i18n.client.tsadd
componentsadd
LanguageSwitcher.tsxadd
publicadd
localesadd
en.jsonadd
es.jsonadd

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: