Locale Detection
本页内容
@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 #
- Parse the
Accept-Languageheader into a priority-sorted list - Match against available locales from the CDN manifest
- Fallback to
defaultLocaleif 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:
| Priority | Strategy | Example |
|---|---|---|
| 1 | Exact match | "tr-TR" matches "tr-TR" |
| 2 | Base language | "tr-TR" matches "tr" (strips region) |
| 3 | Region 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;
}
Better I18N