Setup
Step-by-step installation and configuration for @better-i18n/remix
Get @better-i18n/remix running in your Remix app in 5 steps.
Install the package
npm install @better-i18n/remixbun add @better-i18n/remixpnpm add @better-i18n/remixyarn add @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({
project: "my-company/web-app", // Your project identifier
defaultLocale: "en",
});Important: Always instantiate at module scope (top-level export const). Creating a new instance per request defeats caching and causes unnecessary CDN calls.
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";
export default {
async fetch(request: Request): Promise<Response> {
// Detect locale from URL or Accept-Language header
const locale = getLocaleFromURL(request) || "en";
// Load translations and available locales in parallel
const [messages, locales] = await Promise.all([
i18n.getMessages(locale),
i18n.getLocales(),
]);
const handleRequest = createRequestHandler({
build: remixBuild,
getLoadContext() {
return { locale, messages, locales };
},
});
return handleRequest(request);
},
};For Shopify Hydrogen, see the dedicated Hydrogen guide for the full server.ts setup with createStorefrontClient.
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,
messages: context.messages,
locales: context.locales,
};
}
export default function App() {
const { locale } = useLoaderData<typeof loader>();
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>
);
}Create a message helper
Since CDN messages are typed as Record<string, unknown>, create a type-safe helper to extract strings:
/**
* Type-safe message accessor.
* Safely extracts a string value from a namespace, with fallback.
*/
export function msg(
ns: Record<string, unknown> | undefined,
key: string,
fallback: string,
): string {
const val = ns?.[key];
return typeof val === "string" ? val : fallback;
}Usage in components:
import { useLoaderData } from "react-router";
import { msg } from "~/lib/messages";
export default function Home() {
const { messages } = useLoaderData<typeof loader>();
const common = messages.common; // namespace
return (
<h1>{msg(common, "welcome", "Welcome")}</h1>
);
}Content Security Policy
If your app uses CSP, add cdn.better-i18n.com to your connect-src directive:
The SDK fetches translations from cdn.better-i18n.com at runtime. Without this CSP entry, requests will be blocked.
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
connectSrc: [
"'self'",
"cdn.better-i18n.com",
],
});Next Steps
- Set up locale detection from headers or URLs
- Add locale-prefixed routing with
LocaleLink - For Hydrogen stores, follow the Hydrogen guide