Skip to content
better-i18n.com
本页内容

Get @better-i18n/remix running in your Remix app in 5 steps.

Install the package #

npm install @better-i18n/remix

Create the i18n singleton #

Create app/i18n.server.ts at module scope. This ensures a single TtlCache instance is shared across all requests, avoiding redundant CDN fetches.

app/i18n.server.ts
import { createRemixI18n } from "@better-i18n/remix";

export const i18n = createRemixI18n({
  projectId: "my-company/web-app", // Your project identifier // [!code highlight]
  defaultLocale: "en", // [!code highlight]
});

Load translations in your server entry #

In your server.ts (or entry.server.ts), load messages and locales before handling requests:

server.ts
import { i18n } from "~/i18n.server"; // [!code highlight]

export default {
  async fetch(request: Request): Promise<Response> {
    // Detect locale from URL or Accept-Language header
    const languages = await i18n.getLanguages(); // [!code highlight]
    const locale = getLocaleFromURL(request, languages) || "en"; // [!code highlight]

    // Load translations and available locales in parallel
    const [messages, locales] = await Promise.all([ // [!code highlight]
      i18n.getMessages(locale), // [!code highlight]
      i18n.getLocales(), // [!code highlight]
    ]); // [!code highlight]

    const handleRequest = createRequestHandler({
      build: remixBuild,
      getLoadContext() {
        return { locale, messages, locales }; // [!code highlight]
      },
    });

    return handleRequest(request);
  },
};

Pass data from root loader #

In root.tsx, return locale, messages, and locales from the loader and set <html lang>:

app/root.tsx
import {
  Links, Meta, Outlet, Scripts,
  ScrollRestoration, useLoaderData,
} from "react-router";

export async function loader({ context }: LoaderFunctionArgs) {
  return {
    locale: context.locale, // [!code highlight]
    messages: context.messages, // [!code highlight]
    locales: context.locales, // [!code highlight]
  };
}

export default function App() {
  const { locale } = useLoaderData<typeof loader>(); // [!code highlight]

  return (
    <html lang={locale} dir="ltr"> // [!code highlight]
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Add a translation provider #

Wrap your app with a translation provider to use hook-based translations with ICU formatting, plurals, and interpolation support.

i18next (recommended)

Install i18next and react-i18next:

Bash
bun add i18next react-i18next

Create a helper that builds an i18next instance from your CDN translations:

app/lib/i18n-client.ts
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import type { Messages } from "@better-i18n/remix";

export function createI18nextInstance(locale: string, messages: Messages) {
  const instance = i18next.createInstance();
  instance.use(initReactI18next).init({
    lng: locale,
    resources: { [locale]: messages }, // [!code highlight]
    initImmediate: false, // sync init — required for SSR // [!code highlight]
    lowerCaseLng: true,
    defaultNS: "common",
    fallbackNS: "common",
    interpolation: { escapeValue: false },
  });
  return instance;
}

Wrap <Outlet /> in your root.tsx:

app/root.tsx
import { useMemo } from "react";
import { I18nextProvider } from "react-i18next"; // [!code highlight]
import { createI18nextInstance } from "~/lib/i18n-client"; // [!code highlight]

export default function App() {
  const { locale, messages } = useLoaderData<typeof loader>();

  const i18nInstance = useMemo( // [!code highlight]
    () => createI18nextInstance(locale, messages), // [!code highlight]
    [locale, messages], // [!code highlight]
  ); // [!code highlight]

  return (
    <html lang={locale} dir="ltr">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <I18nextProvider i18n={i18nInstance}> {/* [!code highlight] */}
          <Outlet />
        </I18nextProvider> {/* [!code highlight] */}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Use useTranslation() in any route:

app/routes/($locale)._index.tsx
import { useTranslation } from "react-i18next"; // [!code highlight]

export default function Home() {
  const { t } = useTranslation("common"); // [!code highlight]
  return <h1>{t("welcome")}</h1>;
}

use-intl

The @better-i18n/remix/react entrypoint provides a use-intl-based provider with built-in hooks:

app/root.tsx
import { RemixI18nProvider } from "@better-i18n/remix/react"; // [!code highlight]

export default function App() {
  const { locale, messages, languages } = useLoaderData<typeof loader>();

  return (
    <html lang={locale} dir="ltr">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <RemixI18nProvider locale={locale} messages={messages} languages={languages}> {/* [!code highlight] */}
          <Outlet />
        </RemixI18nProvider> {/* [!code highlight] */}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Use useTranslations() in any route:

app/routes/($locale)._index.tsx
import { useTranslations } from "@better-i18n/remix/react"; // [!code highlight]

export default function Home() {
  const t = useTranslations("common"); // [!code highlight]
  return <h1>{t("welcome")}</h1>;
}

Middleware-Based i18next (remix-i18next) #

For full i18n support with ICU formatting, plurals, and interpolation, use @better-i18n/remix/i18next with remix-i18next:

Bash
bun add i18next react-i18next remix-i18next
app/middleware/i18next.ts
import { createRemixI18n } from "@better-i18n/remix";
import { buildI18nextConfig } from "@better-i18n/remix/i18next";
import { createI18nextMiddleware } from "remix-i18next/middleware";

const i18n = createRemixI18n({ projectId: "my-company/web-app", defaultLocale: "en" });
const config = await buildI18nextConfig({ i18n });

export const [i18nextMiddleware, getLocale, getInstance] =
  createI18nextMiddleware({
    detection: {
      supportedLanguages: config.supportedLanguages,
      fallbackLanguage: config.fallbackLanguage,
    },
    i18next: {
      resources: config.resources,
      ...config.i18nextOptions,
    },
  });

Then use standard react-i18next hooks in your components:

app/routes/($locale)._index.tsx
import { useTranslation } from "react-i18next";

export default function Homepage() {
  const { t } = useTranslation("home");
  return <h1>{t("welcome")}</h1>; // ICU, plurals, interpolation all work
}

Content Security Policy #

If your app uses CSP, add cdn.better-i18n.com to your connect-src directive:

entry.server.tsx
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
  connectSrc: [
    "'self'",
    "cdn.better-i18n.com", // [!code highlight]
  ],
});

Next Steps #