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

@better-i18n/remix provides built-in utilities for detecting the user's preferred locale. You can combine multiple strategies depending on your needs.

URL Path Detection #

The most common pattern for Remix apps is detecting the locale from the URL path prefix:

Code
/products/hat       → "en" (default, no prefix)
/tr/products/hat    → "tr"
/fr/products/hat    → "fr"
app/lib/i18n.ts
export function getLocaleFromURL(
  request: Request,
  languages: { code: string; isDefault: boolean }[],
): string | null {
  const url = new URL(request.url);
  const firstSegment = url.pathname.split("/")[1]?.toLowerCase();

  // Match against available locales from CDN manifest
  const match = languages.find(
    (l) => l.code === firstSegment && !l.isDefault,
  );
  return match?.code ?? null; // Use default
}

Accept-Language Header Detection #

The SDK exports parseAcceptLanguage() and matchLocale() for detecting locale from the browser's Accept-Language header. The built-in detectLocale() method combines both:

TypeScript
import { i18n } from "~/i18n.server";

// Automatic detection from Accept-Language header
const locale = await i18n.detectLocale(request);
// Parses "tr-TR,tr;q=0.9,en-US;q=0.8" → "tr"

How It Works #

  1. Parse the Accept-Language header into a priority-sorted list
  2. Match against available locales from the CDN manifest
  3. Fallback to defaultLocale if no match found

Using Utilities Directly #

You can also use the parsing and matching utilities independently:

TypeScript
import { parseAcceptLanguage, matchLocale } from "@better-i18n/remix";

// Parse header into priority-sorted language list
const languages = parseAcceptLanguage("tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7");
// → ["tr-TR", "tr", "en-US", "en"]

// Match against your available locales (from CDN manifest)
const availableLocales = await i18n.getLocales();
const locale = matchLocale(languages, availableLocales);
// → "tr"

Matching Strategy #

matchLocale() tries three strategies in order:

PriorityStrategyExample
1Exact match"tr-TR" matches "tr-TR"
2Base language"tr-TR" matches "tr" (strips region)
3Region expansion"tr" matches "tr-TR" (first variant)

Returns null if no match is found.

Combined Strategy #

For production apps, combine URL detection with Accept-Language fallback:

server.ts
import { i18n } from "~/i18n.server";

export default {
  async fetch(request: Request): Promise<Response> {
    // 1. Fetch language list first (needed for URL detection)
    const languages = await i18n.getLanguages();

    // 2. Try URL path first (explicit user choice)
    const urlLocale = getLocaleFromURL(request, languages);

    // 3. Fall back to Accept-Language header
    const locale = urlLocale || await i18n.detectLocale(request);

    const messages = await i18n.getMessages(locale);

    // ... pass to loader context
  },
};

Custom Detection #

You can implement any detection strategy by extracting a locale string and passing it to getMessages():

TypeScript
// Cookie-based detection
function getLocaleFromCookie(request: Request): string | null {
  const cookie = request.headers.get("cookie");
  const match = cookie?.match(/locale=(\w+)/);
  return match?.[1] ?? null;
}

// Subdomain-based detection
function getLocaleFromSubdomain(
  request: Request,
  languages: { code: string }[],
): string | null {
  const hostname = new URL(request.url).hostname;
  const subdomain = hostname.split(".")[0];
  return languages.some((l) => l.code === subdomain) ? subdomain : null;
}