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:
import { useLoaderData } from "react-router";
import { LocaleLink } from "~/components/LocaleLink";
import { msg } from "~/lib/messages";
export async function loader({ context }: LoaderFunctionArgs) {
return {
locale: context.locale,
messages: context.messages,
};
}
export default function Homepage() {
const { locale, messages } = useLoaderData<typeof loader>();
const common = messages.common;
return (
<div>
<h1>{msg(common, "welcome", "Welcome")}</h1>
<LocaleLink to="/products" locale={locale}>
{msg(common, "shop_now", "Shop Now")}
</LocaleLink>
</div>
);
}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>
);
}