Skip to content

概览

本文把你现有代码里**"获取当前用户语言"**这块抽出来,整理成一套可复用的思路与最简实现,覆盖以下来源的检测顺序:

  1. 本地存储(localStorage / @vueuse/coreuseStorage
  2. 浏览器首选语言列表(navigator.languages
  3. 浏览器主语言(navigator.language
  4. 操作系统语言(通过浏览器 UA 能力间接获得,现代浏览器已暴露到 navigator.*language*
  5. 兜底语言(fallback)

并包含语言规范化(大小写、连字符)、地区-语言映射常见方言回退


整体实现思路

  • 唯一数据源:维护 LANGS(支持的 i18n 码与后端码映射)。
  • 规范化:把用户环境提供的原始语言字符串(可能是 en_USen-usZH)统一转成 BCP-47 风格(lang-REGION,如 en-USzh-CN)。
  • 就近匹配:尝试"原样 → 下划线转连字符 → 仅语言部分"三轮匹配。
  • 语言级回退映射:如只拿到 zh,映射到 zh-CNjaja-JP 等。
  • 检测顺序localStorage 优先;否则遍历 navigator.languages;否则 navigator.language;都没有时用兜底 en
  • 落盘与副作用:选出最终语言后,同步更新 localStorage<html lang>,并准备好后端需要的语言码(如 X-App-Lang)。

分步实现

1) 定义支持语言与后端映射(唯一数据源)

ts
// 唯一数据源:UI 展示名 + i18n 标准码 + 后端语言码
export const LANGS = [
  { i18n: "en", backend: "en", autonym: "English" },
  { i18n: "zh-CN", backend: "zhCn", autonym: "简体中文" },
  { i18n: "zh-TW", backend: "zhTw", autonym: "繁體中文" },
  { i18n: "ja-JP", backend: "jp", autonym: "日本語" },
  { i18n: "ko-KR", backend: "kr", autonym: "한국어" },
  // ...按需补充
] as const;

export type LocaleCode = (typeof LANGS)[number]["i18n"];

export const FALLBACK_LANG: LocaleCode = "en";

export const BACKEND_LANG_MAP: Record<string, string> = Object.fromEntries(
  LANGS.map((x) => [x.i18n, x.backend])
);
// 常见浏览器变体回退
(BACKEND_LANG_MAP as any)["en-US"] = BACKEND_LANG_MAP["en"];

2) 语言规范化(大小写、连字符、就近匹配)

ts
// 复杂逻辑:把 raw 语言字符串规范化为受支持的 i18n 码
function normalizeToSupported(raw: string): LocaleCode | undefined {
  if (!raw) return;

  const code = raw.trim().toLowerCase();
  // 候选序列:原样 -> 下划线改连字符 -> 仅语言部分
  const candidates = [code, code.replace("_", "-"), code.split("-")[0]];

  const supported = new Set<LocaleCode>(LANGS.map((x) => x.i18n));
  for (const c of candidates) {
    // 复杂逻辑:标准化大小写(lang-REGION)
    const canonical = c
      .split("-")
      .map((seg, i) => (i === 0 ? seg : seg.toUpperCase()))
      .join("-") as LocaleCode;
    if (supported.has(canonical)) return canonical;
  }

  // 复杂逻辑:语言级兜底映射(仅语言 → 指定地区)
  const languageOnly = code.split("-")[0];
  const languageToExact: Partial<Record<string, LocaleCode>> = {
    en: "en",
    zh: "zh-CN",
    "zh-hk": "zh-TW",
    "zh-mo": "zh-TW",
    "zh-sg": "zh-CN",
    ja: "ja-JP",
    ko: "ko-KR",
  };
  const mapped = (languageToExact[languageOnly] ?? languageToExact[code]) as
    | LocaleCode
    | undefined;
  if (mapped && supported.has(mapped)) return mapped;

  return;
}

3) 读取 localStorage 与浏览器 / 操作系统配置

ts
// 使用 @vueuse/core 时:useStorage;否则可用原生 localStorage
import { useStorage } from "@vueuse/core";

const STORAGE_KEY_LANG = "APP_LANG";

// 本地存储中的当前语言(值为 i18n 码)
export const lang = useStorage<LocaleCode>(STORAGE_KEY_LANG, FALLBACK_LANG);

// 复杂逻辑:获取初始语言(优先本地存储;再浏览器/系统;再回退 EN)
export function getInitialLocale(): LocaleCode {
  // 1) 本地存储优先
  if (lang.value) return lang.value as LocaleCode;

  // 2) 浏览器首选语言列表(现代浏览器会把 OS 语言反映到这里)
  const navList =
    navigator.languages && navigator.languages.length > 0
      ? navigator.languages
      : [navigator.language].filter(Boolean);

  for (const raw of navList) {
    const matched = normalizeToSupported(raw);
    if (matched) return matched;
  }

  // 3) 兜底
  return FALLBACK_LANG;
}

4) 应用副作用(<html lang> 与后端请求头)

ts
import { http } from "@/libs";

// 复杂逻辑:设置后端请求头与 <html lang>
export function applyLanguageSideEffects(locale: LocaleCode) {
  const backend = BACKEND_LANG_MAP[locale] ?? locale;

  // Accept-Language 和自定义头
  http.defaults.headers.common["Accept-Language"] = backend;
  http.defaults.headers.common["X-App-Lang"] = backend;

  // HTML 根节点的 lang(利于可访问性与 SEO)
  document.documentElement?.setAttribute("lang", locale);
}

5) 初始化与一次性落盘

ts
// 复杂逻辑:选择语言 → 写入 localStorage → 应用副作用
export function ensureLocaleInitialized() {
  const initial = getInitialLocale();
  // 若本地没有存过语言,把检测到的 initial 存起来
  if (!lang.value) lang.value = initial;
  applyLanguageSideEffects(initial);
  return initial;
}

最简可复用示例(整合版)

适合在任意前端项目中落地,不绑定框架。把此段放到你的 i18n 初始化附近。

ts
// i18n-locale.detector.ts
type LangItem = { i18n: string; backend: string; autonym: string };
export const LANGS = [
  { i18n: "en", backend: "en", autonym: "English" },
  { i18n: "zh-CN", backend: "zhCn", autonym: "简体中文" },
  { i18n: "zh-TW", backend: "zhTw", autonym: "繁體中文" },
  { i18n: "ja-JP", backend: "jp", autonym: "日本語" },
  { i18n: "ko-KR", backend: "kr", autonym: "한국어" },
] as const;

export type LocaleCode = (typeof LANGS)[number]["i18n"];
export const FALLBACK_LANG: LocaleCode = "en";

export const BACKEND_LANG_MAP: Record<string, string> = Object.fromEntries(
  LANGS.map((x) => [x.i18n, x.backend])
);
(BACKEND_LANG_MAP as any)["en-US"] = BACKEND_LANG_MAP["en"];

// ——— 复杂逻辑:规范化 ———
function normalizeToSupported(raw: string): LocaleCode | undefined {
  if (!raw) return;
  const code = raw.trim().toLowerCase();
  const candidates = [code, code.replace("_", "-"), code.split("-")[0]];
  const supported = new Set<LocaleCode>(LANGS.map((x) => x.i18n));
  for (const c of candidates) {
    // 复杂逻辑:lang-REGION 标准化
    const canonical = c
      .split("-")
      .map((seg, i) => (i === 0 ? seg : seg.toUpperCase()))
      .join("-") as LocaleCode;
    if (supported.has(canonical)) return canonical;
  }
  // 复杂逻辑:语言级映射
  const languageOnly = code.split("-")[0];
  const languageToExact: Partial<Record<string, LocaleCode>> = {
    en: "en",
    zh: "zh-CN",
    "zh-hk": "zh-TW",
    "zh-mo": "zh-TW",
    "zh-sg": "zh-CN",
    ja: "ja-JP",
    ko: "ko-KR",
  };
  const mapped = (languageToExact[languageOnly] ?? languageToExact[code]) as
    | LocaleCode
    | undefined;
  if (mapped && supported.has(mapped)) return mapped;
  return;
}

// ——— 复杂逻辑:检测顺序 ———
export function detectLocale(
  getItem = (k: string) => localStorage.getItem(k)
): LocaleCode {
  const saved = getItem("APP_LANG") as LocaleCode | null;
  if (saved) return saved;

  const navList =
    navigator.languages && navigator.languages.length > 0
      ? navigator.languages
      : [navigator.language].filter(Boolean);

  for (const raw of navList) {
    const matched = normalizeToSupported(raw);
    if (matched) return matched;
  }
  return FALLBACK_LANG;
}

// ——— 复杂逻辑:副作用(请求头与 <html lang>)———
export function commitLocale(
  locale: LocaleCode,
  setHeader?: (k: string, v: string) => void
) {
  const backend = BACKEND_LANG_MAP[locale] ?? locale;
  // 可选:对接你的 http 客户端
  setHeader?.("Accept-Language", backend);
  setHeader?.("X-App-Lang", backend);
  document.documentElement?.setAttribute("lang", locale);
}

使用方式(以 Vue 项目为例):

ts
// main.ts
import { detectLocale, commitLocale } from "./i18n-locale.detector";
import i18n from "./i18n";

const initial = detectLocale();
localStorage.setItem("APP_LANG", initial);
// 复杂逻辑:应用副作用(若你用 axios,可在这里设置默认头)
commitLocale(initial, (k, v) =>
  (window as any).http?.defaults?.headers?.common
    ? ((window as any).http.defaults.headers.common[k] = v)
    : null
);
// 然后把 initial 传给你的 i18n 实例
i18n.global.locale = initial;

检测顺序流程图

开始检测

1. 读取 localStorage['APP_LANG']
    ├─ 有值 → 返回
    └─ 无值 → 继续

2. 遍历 navigator.languages[]
    ├─ 找到匹配 → 返回
    └─ 无匹配 → 继续

3. 检查 navigator.language
    ├─ 找到匹配 → 返回
    └─ 无匹配 → 继续

4. 返回兜底语言 (en)

规范化与映射示例

输入原始值规范化步骤最终结果说明
en_US下划线转连字符 → en-USen映射到通用英语
zh-cn大小写标准化 → zh-CNzh-CN简体中文
zh语言级映射 → zh-CNzh-CN默认简体中文
zh-HK语言级映射 → zh-TWzh-TW香港繁体映射到台湾繁体
ja语言级映射 → ja-JPja-JP日语
unknown无匹配en兜底到英语
ZH小写化 → zhzh-CNzh-CN大写输入也能正确处理
en-us大小写标准化 → en-USen统一到 en
zh-sg地区映射 → zh-CNzh-CN新加坡使用简体中文
zh-MO地区映射 → zh-TWzh-TW澳门使用繁体中文

兼容性与注意事项

  • navigator.languages 在现代浏览器广泛支持;老环境可回退到 navigator.language
  • 操作系统语言通常已映射到浏览器语言 API;不建议再读 UA 或非标准字段。
  • 建议把规范化与映射表集中维护在一处,便于审计与扩展。
  • 若后端有自定义语言码(如 zhCn),务必维护好 BACKEND_LANG_MAP 并在请求头里统一使用。
  • SSR 场景:请从请求头 Accept-Language 解析并在服务端先行选择语言,再把结果注水到客户端,避免首屏闪烁。

常见问答

❓ Q1:只有 zh 时选 zh-CN 还是 zh-TW

A:取业务主流市场作为默认(本文示例用 zh-CN)。如有面向地区,可用 GeoIP/域名/部署区域决定初始映射。

❓ Q2:enen-US 怎么处理?

A:统一以 en 作为语言集合里的最小单元;必要时对 en-US 做后端码回退到 en

❓ Q3:如何支持更多地区变体?

A:在 languageToExact 映射表中添加更多规则,例如:

ts
const languageToExact: Partial<Record<string, LocaleCode>> = {
  en: "en",
  "en-gb": "en-GB", // 英国英语
  "en-au": "en-AU", // 澳大利亚英语
  zh: "zh-CN",
  "zh-hk": "zh-TW",
  "zh-mo": "zh-TW",
  "zh-sg": "zh-CN",
  ja: "ja-JP",
  ko: "ko-KR",
  "pt-br": "pt-BR", // 巴西葡萄牙语
  "pt-pt": "pt-PT", // 葡萄牙葡萄牙语
};

❓ Q4:为什么要同步更新 <html lang> 属性?

A

  1. 可访问性:屏幕阅读器会根据 lang 属性选择正确的发音。
  2. SEO:搜索引擎使用 lang 属性来确定页面语言。
  3. 浏览器行为:某些浏览器功能(如翻译提示)依赖此属性。

❓ Q5:如何在 SSR 项目中实现?

A:SSR 场景建议:

ts
// 服务端
import { detectLocale } from "./i18n-locale.detector";

app.get("*", (req, res) => {
  // 从请求头获取语言
  const acceptLanguage = req.headers["accept-language"];
  const locale = detectLocaleFromHeader(acceptLanguage);

  // 渲染时注入到 HTML
  const html = renderToString(app, { locale });
  res.send(html);
});

// 客户端
// 从服务端注水的数据中读取语言,避免客户端重新检测导致闪烁
const locale = window.__INITIAL_STATE__.locale;

与其他国际化方案对比

方案特点本文方案仅用浏览器语言仅用 localStorage后端返回语言
记住用户选择
首次访问自动检测
支持地区方言映射⚠️⚠️
无需后端接口
规范化处理(大小写、连字符)⚠️

完整使用示例(Vue 3 + Vite + vue-i18n)

ts
// src/locales/detector.ts
export {
  detectLocale,
  commitLocale,
  LANGS,
  LocaleCode,
} from "./i18n-locale.detector";

// src/main.ts
import { createApp } from "vue";
import { createI18n } from "vue-i18n";
import App from "./App.vue";
import { detectLocale, commitLocale } from "./locales/detector";
import axios from "axios";

// 1. 检测语言
const initialLocale = detectLocale();

// 2. 创建 i18n 实例
const i18n = createI18n({
  legacy: false,
  locale: initialLocale,
  fallbackLocale: "en",
  messages: {
    // 你的语言包
  },
});

// 3. 应用副作用
commitLocale(initialLocale, (k, v) => {
  axios.defaults.headers.common[k] = v;
});

// 4. 创建并挂载应用
const app = createApp(App);
app.use(i18n);
app.mount("#app");

最佳实践总结

推荐做法

  1. 统一数据源:所有语言配置集中在 LANGS 数组中管理
  2. 优先级明确:localStorage > navigator.languages > navigator.language > fallback
  3. 规范化处理:统一转换为 BCP-47 格式(lang-REGION
  4. 地区映射:为常见地区(如香港、新加坡)提供合理的默认映射
  5. 副作用同步:同时更新 localStorage、HTML lang、axios 请求头
  6. TypeScript 类型:使用 as const 和类型推导确保类型安全

🚫 不推荐的做法

  1. 硬编码语言判断:避免 if (lang === 'zh-CN') 这种散落在各处的判断
  2. 忽略大小写:不处理 ZHzhZh 等变体
  3. 不做规范化:直接使用原始的 navigator.language
  4. 忘记更新副作用:只更新 i18n locale,不更新 HTML lang 和请求头

结语

把"获取用户语言"的逻辑抽象为检测 → 规范化 → 映射 → 兜底 → 副作用五步法,可以在任意框架落地复用。你可以直接把本文的最简可复用示例拎走,用在现有 i18n 初始化流程中即可。

这套方案的核心优势:

  1. 多来源检测:覆盖 localStorage、浏览器、操作系统
  2. 智能规范化:处理各种大小写和连字符变体
  3. 地区映射:为常见地区提供合理的语言默认值
  4. 副作用同步:确保 i18n、HTML、请求头保持一致
  5. 框架无关:可用于 Vue、React、Angular 等任意前端项目

现在你可以在项目中实现一个健壮的多语言检测系统了!🌍

本站总访问