---
title: Routing
description: 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.tsx
```

The `$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:

```tsx title="app/components/LocaleLink.tsx"
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:

```tsx
<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:

```tsx title="app/components/LocaleSwitcher.tsx"
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>
  );
}
```

<Callout type="info">
The `languages` array comes from `i18n.getLanguages()` — pass it through your root loader so the switcher always reflects available languages from the CDN manifest.
</Callout>

## Using in Routes

Access `locale` and `messages` from loader context in any route. Use hook-based translations for full ICU formatting support:

<Tabs items={['i18next', 'use-intl']}>
<Tab value="i18next">

```tsx title="app/routes/($locale)._index.tsx"
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"); // [!code highlight]

  return (
    <div>
      <h1>{t("welcome")}</h1>
      <LocaleLink to="/products" locale={locale}>
        {t("shop_now")}
      </LocaleLink>
    </div>
  );
}
```

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

```tsx title="app/routes/($locale)._index.tsx"
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"); // [!code highlight]

  return (
    <div>
      <h1>{t("welcome")}</h1>
      <LocaleLink to="/products" locale={locale}>
        {t("shop_now")}
      </LocaleLink>
    </div>
  );
}
```

</Tab>
</Tabs>

<Callout type="warn">
**`meta()` functions cannot use React hooks.** Use the `msg()` helper for meta tags — see [API Reference](/frameworks/remix/api-reference#msg).
</Callout>

## SEO Considerations

For proper SEO with locale-prefixed routes, add `hreflang` alternate links in your root:

<Callout type="warn">
**If you use `shouldRevalidate`**, make sure locale changes always trigger revalidation. Returning `false` for locale-changing navigations will keep UI translations stuck in the old language:

```tsx
export function shouldRevalidate({ currentParams, nextParams }) {
  if (currentParams.locale !== nextParams.locale) return true;
  return false; // skip revalidation for same-locale navigations
}
```
</Callout>

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