Setup
On this page
Get @better-i18n/remix running in your Remix app in 5 steps.
Install the package #
npm install @better-i18n/remixCreate 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.
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:
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>:
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:
bun add i18next react-i18nextCreate a helper that builds an i18next instance from your CDN translations:
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:
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:
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:
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:
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:
bun add i18next react-i18next remix-i18nextimport { 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:
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:
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
connectSrc: [
"'self'",
"cdn.better-i18n.com", // [!code highlight]
],
});Next Steps #
- Set up locale detection from headers or URLs
- Add locale-prefixed routing with
LocaleLink - For Hydrogen stores, follow the Hydrogen guide
Better I18N