---
title: Setup
description: Step-by-step installation and configuration for @better-i18n/remix
---

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

<Steps>

<Step>
### Install the package

<Tabs items={['npm', 'bun', 'pnpm', 'yarn']}>
  <Tab value="npm">
  ```bash
  npm install @better-i18n/remix
  ```
  </Tab>
  <Tab value="bun">
  ```bash
  bun add @better-i18n/remix
  ```
  </Tab>
  <Tab value="pnpm">
  ```bash
  pnpm add @better-i18n/remix
  ```
  </Tab>
  <Tab value="yarn">
  ```bash
  yarn add @better-i18n/remix
  ```
  </Tab>
</Tabs>
</Step>

<Step>
### 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.

```ts title="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]
});
```

<Callout type="warn">
**Important:** Always instantiate at module scope (top-level `export const`). Creating a new instance per request defeats caching and causes unnecessary CDN calls.
</Callout>
</Step>

<Step>
### Load translations in your server entry

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

```ts title="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);
  },
};
```

<Callout type="info">
For Shopify Hydrogen, see the dedicated [Hydrogen guide](/frameworks/remix/hydrogen) for the full `server.ts` setup with `createStorefrontClient`.
</Callout>
</Step>

<Step>
### Pass data from root loader

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

```tsx title="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>
  );
}
```
</Step>

<Step>
### Add a translation provider

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

<Tabs items={['i18next (recommended)', 'use-intl']}>
<Tab value="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:

```ts title="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`:

```tsx title="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:

```tsx title="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>;
}
```

</Tab>
<Tab value="use-intl">

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

```tsx title="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:

```tsx title="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>;
}
```

</Tab>
</Tabs>

<Callout type="info">
`meta()` functions cannot use React hooks. Use the [`msg()` helper](/frameworks/remix/api-reference#msg) for meta tags and other non-hook contexts.
</Callout>
</Step>

</Steps>

## Middleware-Based i18next (remix-i18next)

For full i18n support with ICU formatting, plurals, and interpolation, use `@better-i18n/remix/i18next` with [remix-i18next](https://github.com/sergiodxa/remix-i18next):

```bash
bun add i18next react-i18next remix-i18next
```

```ts title="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:

```tsx title="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
}
```

<Callout type="info">
`buildI18nextConfig()` fetches translations from the CDN and converts them to i18next resource format. It also returns `languages` (with `nativeName` and `isDefault` fields) for building language switchers.
</Callout>

## Content Security Policy

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

<Callout type="warn">
The SDK fetches translations from `cdn.better-i18n.com` at runtime. Without this CSP entry, requests will be blocked.
</Callout>

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

## Next Steps

- Set up [locale detection](/frameworks/remix/locale-detection) from headers or URLs
- Add [locale-prefixed routing](/frameworks/remix/routing) with `LocaleLink`
- For Hydrogen stores, follow the [Hydrogen guide](/frameworks/remix/hydrogen)
