Custom Locale Resolvers

Customize how locales are detected and persisted by providing custom resolver files.

By default, the compiler uses cookie-based locale persistence. Custom resolvers let you implement alternative strategies like localStorage, URL parameters, database lookup, or subdomain detection.

How It Works

Create optional files in .lingo/ directory:

  • .lingo/locale-resolver.server.ts — Server-side locale detection
  • .lingo/locale-resolver.client.ts — Client-side locale detection and persistence

If these files don't exist, the compiler uses default cookie-based implementation.

Server Locale Resolver

Create .lingo/locale-resolver.server.ts for custom server-side locale detection:

// .lingo/locale-resolver.server.ts
export async function getServerLocale(): Promise<string> {
  // Your custom logic
  return "en";
}

Example: Accept-Language Header (Next.js)

import { headers } from "next/headers";

export async function getServerLocale(): Promise<string> {
  const headersList = await headers();
  const acceptLanguage = headersList.get("accept-language");

  // Parse accept-language: "en-US,en;q=0.9,es;q=0.8"
  const locale = acceptLanguage
    ?.split(",")[0]
    ?.split("-")[0]
    ?.trim() || "en";

  return locale;
}

Example: Database Lookup

import { cookies } from "next/headers";
import { db } from "@/lib/db";

export async function getServerLocale(): Promise<string> {
  const cookieStore = await cookies();
  const sessionToken = cookieStore.get("session")?.value;

  if (!sessionToken) return "en";

  // Query user preferences from database
  const user = await db.user.findUnique({
    where: { sessionToken },
    select: { preferredLocale: true },
  });

  return user?.preferredLocale || "en";
}

Example: Subdomain Detection

import { headers } from "next/headers";

export async function getServerLocale(): Promise<string> {
  const headersList = await headers();
  const host = headersList.get("host") || "";

  // Extract subdomain: es.example.com → es
  const subdomain = host.split(".")[0];

  // Map subdomain to locale
  const localeMap: Record<string, string> = {
    es: "es",
    de: "de",
    fr: "fr",
  };

  return localeMap[subdomain] || "en";
}

Client Locale Resolver

Create .lingo/locale-resolver.client.ts for custom client-side locale detection and persistence:

// .lingo/locale-resolver.client.ts
export function getClientLocale(): string {
  // Detect locale
  return "en";
}

export function persistLocale(locale: string): void {
  // Save locale preference
}

Example: localStorage

export function getClientLocale(): string {
  if (typeof window === "undefined") return "en";

  // Check localStorage
  const stored = localStorage.getItem("user-locale");
  if (stored) return stored;

  // Fall back to browser language
  return navigator.language.split("-")[0] || "en";
}

export function persistLocale(locale: string): void {
  if (typeof window === "undefined") return;

  localStorage.setItem("user-locale", locale);

  // Optionally reload page to apply new locale
  window.location.reload();
}

Example: URL Parameters

export function getClientLocale(): string {
  if (typeof window === "undefined") return "en";

  // Check URL parameter: ?lang=es
  const params = new URLSearchParams(window.location.search);
  const urlLocale = params.get("lang");

  if (urlLocale) return urlLocale;

  // Fall back to localStorage
  return localStorage.getItem("locale") || "en";
}

export function persistLocale(locale: string): void {
  if (typeof window === "undefined") return;

  // Update URL parameter
  const url = new URL(window.location.href);
  url.searchParams.set("lang", locale);
  window.history.replaceState({}, "", url.toString());

  // Also save to localStorage
  localStorage.setItem("locale", locale);

  // Reload to apply new locale
  window.location.reload();
}

Example: Combined Strategy

export function getClientLocale(): string {
  if (typeof window === "undefined") return "en";

  // Priority 1: URL parameter
  const params = new URLSearchParams(window.location.search);
  const urlLocale = params.get("lang");
  if (urlLocale) return urlLocale;

  // Priority 2: localStorage
  const stored = localStorage.getItem("locale");
  if (stored) return stored;

  // Priority 3: Browser language
  const browserLocale = navigator.language.split("-")[0];
  const supportedLocales = ["en", "es", "de", "fr"];
  if (supportedLocales.includes(browserLocale)) {
    return browserLocale;
  }

  // Priority 4: Default
  return "en";
}

export function persistLocale(locale: string): void {
  if (typeof window === "undefined") return;

  // Save to localStorage
  localStorage.setItem("locale", locale);

  // Update URL
  const url = new URL(window.location.href);
  url.searchParams.set("lang", locale);
  window.history.replaceState({}, "", url.toString());

  // Reload page
  window.location.reload();
}

TypeScript Types

Both resolvers are fully typed:

// Server resolver
export async function getServerLocale(): Promise<string>;

// Client resolver
export function getClientLocale(): string;
export function persistLocale(locale: string): void;

Integration with setLocale

The setLocale() function from @lingo.dev/compiler/react automatically calls your custom persistLocale():

import { setLocale } from "@lingo.dev/compiler/react";

// Calls your persistLocale() under the hood
setLocale("es");

SSR Considerations

For SSR frameworks (Next.js, Remix, etc.):

  • Server resolver runs on every request
  • Client resolver runs in browser after hydration
  • Ensure consistency between server and client detection

Common pattern: Server reads from cookie/header, client persists to cookie/localStorage.

Default Implementation

If no custom resolvers are provided, the compiler uses this default:

// Default server resolver
export async function getServerLocale(): Promise<string> {
  const cookies = await import("next/headers").then((m) => m.cookies());
  return cookies().get("locale")?.value || "en";
}

// Default client resolver
export function getClientLocale(): string {
  return document.cookie
    .split("; ")
    .find((row) => row.startsWith("locale="))
    ?.split("=")[1] || "en";
}

export function persistLocale(locale: string): void {
  document.cookie = `locale=${locale}; path=/; max-age=31536000`;
  window.location.reload();
}

Common Questions

Do I need both server and client resolvers? No. Provide only what you need to customize. Missing files use default behavior.

Can I use custom resolvers with SPA apps? Yes. Only the client resolver is relevant for SPA apps. Server resolver is for SSR.

Does this work with Vite? Yes. Client resolver works identically. Server resolver is Next.js-specific (for SSR).

How do I test custom resolvers?

  1. Create resolver files
  2. Implement your logic
  3. Run dev server
  4. Test locale switching with your custom persistence

Can I access Next.js-specific APIs? Yes. Import Next.js utilities (headers, cookies, etc.) directly in your resolver files.

What if getServerLocale returns an invalid locale? The compiler falls back to sourceLocale if the returned locale isn't in targetLocales.

Examples by Use Case

Subdomain-Based Routing

Server:

const host = (await headers()).get("host") || "";
const locale = host.split(".")[0]; // es.example.com → es
return supportedLocales.includes(locale) ? locale : "en";

Client:

const locale = window.location.hostname.split(".")[0];
return supportedLocales.includes(locale) ? locale : "en";

Database-Backed User Preferences

Server:

const session = await getSession();
const user = await db.user.findUnique({
  where: { id: session.userId },
});
return user.locale || "en";

Client:

// After user changes locale in UI
await fetch("/api/user/locale", {
  method: "POST",
  body: JSON.stringify({ locale }),
});
window.location.reload();

Path-Based Routing (/en/about, /es/about)

Server:

const pathname = (await headers()).get("x-pathname") || "/";
const locale = pathname.split("/")[1];
return supportedLocales.includes(locale) ? locale : "en";

Client:

const locale = window.location.pathname.split("/")[1];
return supportedLocales.includes(locale) ? locale : "en";

Next Steps