自定义 Locale 解析器

通过提供自定义 resolver 文件,定制 locale 的检测与持久化方式。

默认情况下,编译器使用基于 cookie 的 locale 持久化机制。自定义 resolver 允许你实现如 localStorage、URL 参数、数据库查询或子域名检测等替代策略。

工作原理

.lingo/ 目录下创建可选文件:

  • .lingo/locale-resolver.server.ts — 服务器端 locale 检测
  • .lingo/locale-resolver.client.ts — 客户端 locale 检测与持久化

如果这些文件不存在,编译器将使用默认的基于 cookie 的实现。

服务器端 Locale 解析器

创建 .lingo/locale-resolver.server.ts 以实现自定义的服务器端 locale 检测:

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

示例: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;
}

示例:数据库查询

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

示例:子域名检测

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

客户端 Locale 解析器

创建 .lingo/locale-resolver.client.ts 以实现自定义的客户端 locale 检测与持久化:

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

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

示例: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();
}

示例:URL 参数

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

示例:组合策略

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 类型

两个 resolver 都是完全类型化的:

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

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

与 setLocale 的集成

来自 useLingoContext()setLocale() 函数会自动调用你的自定义 persistLocale()

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

function MyComponent() {
  const { setLocale } = useLingoContext();

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

SSR 注意事项

对于 SSR 框架(如 Next.js、Remix 等):

  • 服务端 resolver 在每次请求时运行
  • 客户端 resolver 在浏览器 hydration 后运行
  • 确保服务端和客户端检测结果一致

常见模式: 服务端从 cookie/header 读取,客户端持久化到 cookie/localStorage。

默认实现

如果没有提供自定义 resolver,编译器会使用如下默认实现:

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

常见问题

我需要同时实现服务端和客户端 resolver 吗? 不需要。只需根据自定义需求提供相应部分。缺失的文件会采用默认行为。

SPA 应用可以用自定义 resolver 吗? 可以。SPA 应用只需客户端 resolver。服务端 resolver 仅用于 SSR。

支持 Vite 吗? 支持。客户端 resolver 行为一致。服务端 resolver 仅适用于 Next.js(SSR 场景)。

如何测试自定义 resolver?

  1. 创建 resolver 文件
  2. 实现你的逻辑
  3. 运行开发服务器
  4. 测试自定义持久化下的语言切换

可以访问 Next.js 专用 API 吗? 可以。在 resolver 文件中直接引入 Next.js 工具(headers、cookies 等)。

如果 getServerLocale 返回了无效的 locale 怎么办? 如果返回的 locale 不在 targetLocales 中,编译器会回退到 sourceLocale

按用例分类的示例

基于子域名的路由

服务端:

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

客户端:

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

数据库驱动的用户偏好

服务器:

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

客户端:

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

基于路径的路由(/en/about,/es/about)

服务器:

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

客户端:

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

下一步