Routing
Locale-prefixed URLs, LocaleLink, and LocaleSwitcher for Remix apps
Remix uses file-based routing with optional segments. This guide covers locale-prefixed routing patterns for multi-language apps.
Route Structure
Use Remix's ($locale) optional segment to support locale prefixes:
app/routes/
├── ($locale)._index.tsx # / or /tr
├── ($locale).products.$handle.tsx # /products/hat or /tr/products/hat
├── ($locale).collections.$handle.tsx
└── ($locale).about.tsxThe $locale parameter is optional — when absent, the default locale is used:
/products/hat → locale: "en" (default)
/tr/products/hat → locale: "tr"
/fr/products/hat → locale: "fr"LocaleLink Component
Create a locale-aware Link wrapper that automatically prepends the locale prefix for non-default locales:
import { Link, type LinkProps } from "react-router";
interface LocaleLinkProps extends Omit<LinkProps, "to"> {
to: string;
locale: string;
children?: React.ReactNode;
}
export function LocaleLink({ to, locale, ...rest }: LocaleLinkProps) {
const prefix = locale === "en" ? "" : `/${locale}`;
const path = to.startsWith("/") ? `${prefix}${to}` : to;
return <Link to={path} {...rest} />;
}Usage:
<LocaleLink to="/products/hat" locale="tr">
View Product
</LocaleLink>
// Renders: <a href="/tr/products/hat">View Product</a>
<LocaleLink to="/products/hat" locale="en">
View Product
</LocaleLink>
// Renders: <a href="/products/hat">View Product</a>LocaleSwitcher Component
Build a dropdown that switches locales by rewriting the current URL path:
import { useLocation, useNavigate } from "react-router";
import type { LanguageOption } from "@better-i18n/remix";
interface LocaleSwitcherProps {
locale: string;
languages: LanguageOption[];
}
export function LocaleSwitcher({ locale, languages }: LocaleSwitcherProps) {
const location = useLocation();
const navigate = useNavigate();
function handleSelect(newLocale: string) {
const currentPath = location.pathname;
// Strip existing locale prefix (non-default locales have a URL prefix)
const nonDefaultCodes = languages.filter((l) => !l.isDefault).map((l) => l.code);
const regex = new RegExp(`^/(${nonDefaultCodes.join("|")})`);
const pathWithoutLocale = currentPath.replace(regex, "") || "/";
// Add new locale prefix (skip for default locale)
const newPath = newLocale === "en"
? pathWithoutLocale
: `/${newLocale}${pathWithoutLocale}`;
navigate(newPath + location.search);
}
return (
<select
value={locale}
onChange={(e) => handleSelect(e.target.value)}
aria-label="Select language"
>
{languages.map((language) => (
<option key={language.code} value={language.code}>
{language.nativeName || language.code.toUpperCase()}
</option>
))}
</select>
);
}The languages array comes from i18n.getLanguages() — pass it through your root loader so the switcher always reflects available languages from the CDN manifest.
Using in Routes
Access locale and messages from loader context in any route. Use hook-based translations for full ICU formatting support:
import { useLoaderData } from "react-router";
import { useTranslation } from "react-i18next";
import { LocaleLink } from "~/components/LocaleLink";
export async function loader({ context }: LoaderFunctionArgs) {
return {
locale: context.locale,
};
}
export default function Homepage() {
const { locale } = useLoaderData<typeof loader>();
const { t } = useTranslation("common");
return (
<div>
<h1>{t("welcome")}</h1>
<LocaleLink to="/products" locale={locale}>
{t("shop_now")}
</LocaleLink>
</div>
);
}import { useLoaderData } from "react-router";
import { useTranslations } from "@better-i18n/remix/react";
import { LocaleLink } from "~/components/LocaleLink";
export async function loader({ context }: LoaderFunctionArgs) {
return {
locale: context.locale,
};
}
export default function Homepage() {
const { locale } = useLoaderData<typeof loader>();
const t = useTranslations("common");
return (
<div>
<h1>{t("welcome")}</h1>
<LocaleLink to="/products" locale={locale}>
{t("shop_now")}
</LocaleLink>
</div>
);
}meta() functions cannot use React hooks. Use the msg() helper for meta tags — see API Reference.
SEO Considerations
For proper SEO with locale-prefixed routes, add hreflang alternate links in your root:
export async function loader({ request, context }: LoaderFunctionArgs) {
const url = new URL(request.url);
return {
locale: context.locale,
languages: context.languages,
canonicalUrl: url.origin + url.pathname,
};
}
export default function App() {
const { locale, languages, canonicalUrl } = useLoaderData<typeof loader>();
return (
<html lang={locale}>
<head>
<link rel="canonical" href={canonicalUrl} />
{languages.map((lang) => (
<link
key={lang.code}
rel="alternate"
hrefLang={lang.code}
href={canonicalUrl.replace(`/${locale}`, lang.isDefault ? "" : `/${lang.code}`)}
/>
))}
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}