How toPersist the User Locale Using Cookies with React Router and i18next

Modern React Router applications with remix-i18next can automatically detect and persist user language preferences using cookies. This is particularly useful for applications where users want their language choice remembered across sessions, like e-commerce sites or content platforms where users might switch between languages frequently.

With remix-i18next's middleware approach, you can configure automatic locale detection that checks cookies first, then falls back to browser Accept-Language headers.

Set Up the i18next Middleware with Cookie Support

First, create the middleware configuration that includes cookie support for locale persistence.

app/middleware/i18next.ts
import { initReactI18next } from "react-i18next"; import { createCookie } from "react-router"; import { createI18nextMiddleware } from "remix-i18next/middleware"; import resources from "~/locales"; // Your translation resources // Cookie to store the user's locale preference export const localeCookie = createCookie("lng", { path: "/", sameSite: "lax", secure: process.env.NODE_ENV === "production", httpOnly: true, maxAge: 365 * 24 * 60 * 60, // 1 year }); export const [i18nextMiddleware, getLocale, getInstance] = createI18nextMiddleware({ detection: { supportedLanguages: ["en", "es", "ja"], // Your supported languages fallbackLanguage: "en", cookie: localeCookie, // Enable cookie-based locale detection }, i18next: { resources, // Your locales configuration }, plugins: [initReactI18next], });

This middleware will automatically check the cookie for a stored locale preference before falling back to other detection methods.

Enable the Middleware in Your App

Configure the middleware in your root route to run on every request.

app/root.tsx
import { data, Links, Meta, Outlet, Scripts, ScrollRestoration, } from "react-router"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import type { Route } from "./+types/root"; import { getLocale, i18nextMiddleware, localeCookie, } from "./middleware/i18next"; export const middleware = [i18nextMiddleware]; export async function loader({ context }: Route.LoaderArgs) { let locale = getLocale(context); return data( { locale }, { headers: { "Set-Cookie": await localeCookie.serialize(locale), }, }, ); } export function Layout({ children }: { children: React.ReactNode }) { let { i18n } = useTranslation(); return ( <html lang={i18n.language} dir={i18n.dir(i18n.language)}> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); } export default function App({ loaderData: { locale } }: Route.ComponentProps) { let { i18n } = useTranslation(); useEffect(() => { if (i18n.language !== locale) { i18n.changeLanguage(locale); } }, [locale, i18n]); return <Outlet />; }

The loader automatically sets the detected locale in a cookie, ensuring the preference persists across sessions.

Create a Language Switcher Component

Build a component that allows users to change their language preference and persist it in the cookie.

app/components/language-switcher.tsx
import { useTranslation } from "react-i18next"; import { Form } from "react-router"; const LANGUAGES = { en: "English", es: "Español", ja: "日本語", }; export function LanguageSwitcher() { let { i18n, t } = useTranslation(); return ( <Form method="post" action="/api/set-locale"> <label htmlFor="locale-select">{t("selectLanguage")}</label> <select id="locale-select" name="locale" value={i18n.language} onChange={(event) => event.target.form?.requestSubmit()} > {Object.entries(LANGUAGES).map(([code, name]) => ( <option key={code} value={code}> {name} </option> ))} </select> </Form> ); }

This component automatically submits the form when users select a different language, providing immediate feedback.

Handle Locale Changes with an Action Route

Create an action route to handle locale changes and update the cookie.

app/routes/api.set-locale.ts
import { redirect } from "react-router"; import { safeRedirect } from "remix-utils/safe-redirect"; import type { Route } from "./+types/api.set-locale"; import { localeCookie } from "~/middleware/i18next"; const SUPPORTED_LOCALES = ["en", "es", "ja"] as const; export async function action({ request }: Route.ActionArgs) { let formData = await request.formData(); let locale = formData.get("locale"); // Validate the locale if (!locale || !SUPPORTED_LOCALES.includes(locale as any)) { locale = "en"; // Fallback to default } let headers = new Headers(); headers.append("Set-Cookie", await localeCookie.serialize(locale)); // Safely redirect back to the referring page or home let redirectTo = safeRedirect(request.headers.get("Referer"), "/"); return redirect(redirectTo, { headers }); }

This action validates the locale choice and updates the cookie before redirecting back to the previous page.

Access Locale in Route Loaders and Actions

Use the locale detected by the middleware in your route loaders and actions.

app/routes/dashboard.ts
import { data } from "react-router"; import type { Route } from "./+types/dashboard"; import { getLocale, getInstance } from "~/middleware/i18next"; export async function loader({ context }: Route.LoaderArgs) { let locale = getLocale(context); let i18next = getInstance(context); // Use locale for date formatting let welcomeMessage = i18next.t("welcome"); let currentDate = new Date().toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", }); return data({ welcomeMessage, currentDate, locale, }); }

The middleware provides both the detected locale and the configured i18next instance for server-side translations.

Final Thoughts

Using remix-i18next middleware with cookie support provides automatic locale persistence without manual cookie handling in every route. The middleware handles the detection logic, checking cookies first before falling back to browser headers, while your routes can focus on using the detected locale for formatting and translations. This approach scales well as your application grows and provides a seamless internationalization experience for users.