Skip to content
better-i18n.com
En esta página

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:

Code
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:

Code
/products/hat         → locale: "en" (default)
/tr/products/hat      → locale: "tr"
/fr/products/hat      → locale: "fr"

Create a locale-aware Link wrapper that automatically prepends the locale prefix for non-default locales:

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:

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>
  );
}

Using in Routes #

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

i18next

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>
  );
}

use-intl

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>
  );
}

SEO Considerations #

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

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>
  );
}