Language Detection: 浏览器、地理位置与用户偏好的多层级策略

为 SaaS 应用实现语言检测——浏览器 Accept-Language、地理位置、用户偏好及基于 URL 的策略,并配有合理的降级链路。

Sam Rivera
Sam Rivera
June 1, 202612 min read
其他语言:Deutsch · English

TL;DR: 为每位访客精准检测语言需要叠加浏览器头信息、地理位置、已保存的用户偏好及 URL 信号。本指南涵盖检测策略、降级链路与持久化方案。更多模式见 tanstackship.com。

引言

向用户展示错误的语言,是流失国际访客最快的方式之一。一位德国用户进入纯英文页面后的跳出率高达 70%。有效的语言检测需要多层级策略配合合理的降级方案。如需了解 i18n 架构模式概述,请先阅读 SaaS 应用 i18n:架构模式对比。

检测层级策略

优先级来源工作方式可靠性
1URL 路径/语言前缀/de/dashboard最高
2用户偏好(已保存)Cookie、localStorage、数据库高
3浏览器 Accept-Languagenavigator.languages中
4Geo IP 地理位置Cloudflare、MaxMind中
5默认语言环境可配置的降级方案最低

基于 URL 的检测(最高优先级)

ts
// TanStack Router: 从 URL 路径获取 locale
import { createRootRoute, createRouter, redirect } from '@tanstack/react-router'

const SUPPORTED_LOCALES = ['en', 'de', 'zh', 'ja', 'ar']
const DEFAULT_LOCALE = 'en'

// 带有 locale 参数的根路由
const rootRoute = createRootRoute({
  beforeLoad: ({ params, location }) => {
    const locale = params.locale
    
    if (!locale || !SUPPORTED_LOCALES.includes(locale)) {
      // 检测并重定向
      const detected = detectUserLocale()
      throw redirect({
        to: `/${detected}${location.pathname}`,
        replace: true,
      })
    }
    
    // 为应用设置 locale
    setLocale(locale)
  },
})

浏览器语言检测(中间件)

ts
// Cloudflare Worker: Accept-Language 解析
function parseAcceptLanguage(header: string | null): string[] {
  if (!header) return [DEFAULT_LOCALE]
  
  return header
    .split(',')
    .map(entry => {
      const [lang, quality = 'q=1'] = entry.trim().split(';')
      const q = parseFloat(quality.replace('q=', '')) || 1
      const locale = lang.split('-')[0] // 'en-US' → 'en'
      return { locale, q }
    })
    .sort((a, b) => b.q - a.q)
    .map(entry => entry.locale)
    .filter(locale => SUPPORTED_LOCALES.includes(locale))
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)
    const pathLocale = url.pathname.split('/')[1]
    
    if (SUPPORTED_LOCALES.includes(pathLocale)) {
      return env.ASSETS.fetch(request)
    }
    
    // 从 Accept-Language 检测
    const acceptLang = request.headers.get('Accept-Language')
    const preferredLocales = parseAcceptLanguage(acceptLang)
    const detectedLocale = preferredLocales[0] || 'en'
    
    // 重定向到检测到的 locale 路径
    return Response.redirect(`${url.origin}/${detectedLocale}${url.pathname}`, 302)
  }
}

用户偏好持久化

tsx
// React: 保存并恢复语言偏好
import { setLocale } from '~/paraglide/runtime.js'

function LanguageSwitcher() {
  const switchLanguage = (locale: string) => {
    // 保存偏好
    localStorage.setItem('preferred-locale', locale)
    document.cookie = `locale=${locale}; path=/; max-age=${60 * 60 * 24 * 365}`
    
    // 切换 locale(Paraglide.js)
    setLocale(locale)
    
    // 无需全量刷新更新 URL
    const url = new URL(window.location.href)
    url.pathname = url.pathname.replace(/^\/[a-z]{2}/, `/${locale}`)
    window.history.pushState({}, '', url)
  }
  
  return (
    <select onChange={(e) => switchLanguage(e.target.value)}>
      <option value="en">English</option>
      <option value="de">Deutsch</option>
      <option value="zh">中文</option>
      <option value="ja">日本語</option>
      <option value="ar">العربية</option>
    </select>
  )
}

检测决策流程

访客到达
      ↓
URL 中是否有 locale? ──是──→ 使用 URL locale
      否
      ↓
是否有已保存的偏好? ──是──→ 使用已保存的 locale
      否
      ↓
检查 Accept-Language
      ↓
locale 是否受支持? ──是──→ 使用浏览器 locale
      否
      ↓
Geo IP 检测
      ↓
使用默认值(en) ──────→ 醒目展示语言切换器

结语

分层构建检测策略:URL 路径优先,已保存偏好其次,浏览器头信息第三,地理位置第四。无论检测多精准,都要始终展示醒目的语言切换器——自动检测不应成为切换语言的唯一方式。将用户选择缓存在 Cookie 和 localStorage 中,以实现跨会话持久化。

如需了解语言检测的 SEO 方面,请参阅 hreflang 标签终极指南。如需构建特定语言的落地页,构建语言特定落地页策略 涵盖了相关方法。如需跨语言运行实验,请查看 多语言 A/B 测试指南。