当首选语言不可用时如何处理语言环境回退

在用户偏好不受支持的语言环境时自动选择受支持的语言

简介

并非每个 Web 应用都支持世界上的所有语言。当用户偏好一种您的应用不支持的语言时,您需要一种回退机制,以便用下一个最合适的语言显示内容,而不是显示错误或未翻译的文本。

语言环境回退是指在首选语言环境不可用时选择替代语言环境的过程。JavaScript 的 Intl API 可以自动处理这一过程:它接受多个语言环境选项,并选择第一个受支持的选项。这样可以确保即使没有完全匹配的首选语言环境,您的应用也能始终正确显示格式化内容。

本课程将介绍 JavaScript 中语言环境回退的工作原理,如何高效实现回退机制,以及如何为有特定语言环境支持需求的应用构建自定义回退逻辑。

不受支持的语言环境带来的问题

当您将一个语言环境标识符传递给 Intl API 时,JavaScript 运行时必须支持该语言环境,才能正确格式化内容。如果您请求挪威新挪威语的格式化,但运行时只支持挪威书面语,格式化器就需要一种优雅的处理方式。

如果没有回退机制,应用在遇到不受支持的语言环境时将无法显示内容,或显示未翻译的文本。来自使用较少见语言变体地区的用户会遇到界面异常的问题。

以讲加拿大法语的用户为例。如果您的应用只支持欧洲法语,您希望格式化器使用欧洲法语的约定,而不是完全失败。虽然这并不完美,但总比没有本地化要好。

Intl API 如何自动处理回退

每个 Intl 构造函数都可以接受单个语言环境字符串或一个语言环境字符串数组。当你传递一个数组时,运行时会按顺序评估每个语言环境,并使用它支持的第一个。

const locales = ["fr-CA", "fr-FR", "en-US"];
const formatter = new Intl.DateTimeFormat(locales);

const date = new Date("2025-03-15");
console.log(formatter.format(date));
// Uses fr-CA if available
// Falls back to fr-FR if fr-CA is not available
// Falls back to en-US if neither French variant is available

运行时会从左到右检查数组。如果支持加拿大法语,就使用它。如果不支持,则尝试欧洲法语。如果两种法语变体都不可用,则回退到美式英语。

这种自动回退机制意味着你无需手动检查支持情况,也无需在请求特定语言环境时处理错误。Intl API 保证会选择一个受支持的语言环境,或者回退到系统默认值。

提供多种语言环境选项

实现回退的最简单方法是按优先顺序传递一个语言环境数组。这适用于所有 Intl 构造函数,包括 DateTimeFormatNumberFormatCollator 等。

const locales = ["es-MX", "es-ES", "es", "en"];
const numberFormatter = new Intl.NumberFormat(locales, {
  style: "currency",
  currency: "USD"
});

console.log(numberFormatter.format(1234.56));
// Uses Mexican Spanish if available
// Falls back to European Spanish
// Falls back to generic Spanish
// Falls back to English as final option

这种模式提供了优雅的降级路径。用户如果有可用的首选方言会优先获得,否则会获得更广泛的语言变体,最后是通用的回退语言。

顺序很重要。运行时会选择它支持的第一个语言环境,因此应将最具体、最优先的语言环境放在前面。

理解语言环境匹配的工作原理

当你提供多个语言环境时,JavaScript 运行时会使用一种语言环境匹配算法来选择最佳可用选项。该算法会将你请求的语言环境与运行时支持的语言环境集合进行比较。

如果请求的语言环境与受支持的语言环境完全匹配,运行时会立即使用它。如果没有完全匹配,运行时可能会根据语言和地区代码选择相关的语言环境。

例如,如果你请求 en-AU(澳大利亚英语),但运行时只支持 en-USen-GB,那么它会选择其中一个英语变体,而不是回退到完全不同的语言。

const locales = ["en-AU", "en"];
const formatter = new Intl.DateTimeFormat(locales);

const resolvedLocale = formatter.resolvedOptions().locale;
console.log(resolvedLocale);
// Might show "en-US" or "en-GB" depending on the runtime
// The runtime selected a supported English variant

resolvedOptions() 方法会返回格式化器实际使用的 locale。这可以让你在回退后验证选择了哪个 locale。

检查支持哪些 locale

supportedLocalesOf() 静态方法用于检查某个 Intl 构造函数对给定列表中的哪些 locale 提供支持。该方法会返回一个仅包含受支持 locale 的数组。

const requestedLocales = ["fr-CA", "fr-FR", "de-DE", "ja-JP"];
const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf(requestedLocales);

console.log(supportedLocales);
// Output depends on runtime support
// Example: ["fr-FR", "de-DE", "ja-JP"]
// Canadian French was not supported, others were

此方法会过滤请求的 locale,显示运行时可以直接使用(无需回退到默认值)的 locale。不受支持的 locale 会从返回的数组中移除。

你可以用此方法在创建格式化器前检查支持情况,或用于展示给用户可用的语言选项。

const availableLocales = ["en-US", "es-MX", "fr-FR", "de-DE", "ja-JP"];
const supported = Intl.NumberFormat.supportedLocalesOf(availableLocales);

console.log("This runtime supports:", supported);
// Shows which of your application locales work in this environment

每个 Intl 构造函数都有自己的 supportedLocalesOf() 方法,因为不同国际化特性对 locale 的支持可能不同。例如,某个运行时可能支持数字格式化的法语,但不支持文本分段的法语。

基于 locale 标识符构建回退链

当你已知应用支持特定 locale 时,可以构建一个逐步降低 specificity 的回退链。该模式从完整的 locale 标识符开始,逐步移除组件,直到找到匹配项为止。

function buildFallbackChain(locale) {
  const chain = [locale];

  const parts = locale.split("-");
  if (parts.length > 1) {
    chain.push(parts[0]);
  }

  chain.push("en");

  return chain;
}

const fallbacks = buildFallbackChain("zh-Hans-CN");
console.log(fallbacks);
// ["zh-Hans-CN", "zh", "en"]

const formatter = new Intl.DateTimeFormat(fallbacks);
// Tries Simplified Chinese for China
// Falls back to generic Chinese
// Falls back to English

此函数通过从 locale 标识符中提取语言代码并添加最终的英语回退,来创建回退链。你可以根据应用支持的 locale,扩展此逻辑以实现更复杂的回退规则。

对于支持多种语言变体的应用程序,您可能希望在回退到英语之前,先回退到相关的方言。

function buildSmartFallbackChain(locale) {
  const chain = [locale];

  if (locale.startsWith("es-")) {
    chain.push("es-MX", "es-ES", "es");
  } else if (locale.startsWith("fr-")) {
    chain.push("fr-FR", "fr-CA", "fr");
  } else if (locale.startsWith("zh-")) {
    chain.push("zh-Hans-CN", "zh-Hant-TW", "zh");
  }

  const parts = locale.split("-");
  if (parts.length > 1 && !chain.includes(parts[0])) {
    chain.push(parts[0]);
  }

  if (!chain.includes("en")) {
    chain.push("en");
  }

  return chain;
}

const fallbacks = buildSmartFallbackChain("es-AR");
console.log(fallbacks);
// ["es-AR", "es-MX", "es-ES", "es", "en"]
// Tries Argentinian Spanish
// Falls back to Mexican Spanish
// Falls back to European Spanish
// Falls back to generic Spanish
// Falls back to English

这种方法确保用户在回退到英语之前,能够看到其语言相关方言的内容。

选择本地化匹配算法

Intl API 支持两种本地化匹配算法:lookup 和 best fit。您可以在创建格式化器时,通过 localeMatcher 选项指定要使用的算法。

lookup 算法遵循 BCP 47 Lookup 规范。它通过系统地比较本地化标识符并选择第一个完全匹配项来执行严格匹配。

const locales = ["de-DE", "en-US"];
const formatter = new Intl.NumberFormat(locales, {
  localeMatcher: "lookup"
});

console.log(formatter.resolvedOptions().locale);
// Uses strict lookup matching rules

best fit 算法允许运行时使用其自身的匹配逻辑选择本地化。即使不是完全匹配,该算法也能智能地决定哪个本地化最能满足用户需求。

const locales = ["de-DE", "en-US"];
const formatter = new Intl.NumberFormat(locales, {
  localeMatcher: "best fit"
});

console.log(formatter.resolvedOptions().locale);
// Uses runtime's best fit algorithm
// Might select a related locale more intelligently

默认算法为 best fit。大多数应用程序应使用默认设置,因为它能在不同 JavaScript 运行环境下提供更好的效果。仅当您需要可预测、严格的匹配行为时才使用 lookup。

利用浏览器语言偏好实现自动回退

navigator.languages 属性返回一个按优先级排序的用户首选语言数组。您可以将该数组直接传递给 Intl 构造函数,以根据浏览器设置实现自动回退。

const formatter = new Intl.DateTimeFormat(navigator.languages);

const date = new Date("2025-03-15");
console.log(formatter.format(date));
// Automatically uses user's preferred supported language

这种方法让浏览器自动处理所有回退逻辑。如果用户的首选语言不被支持,Intl API 会自动尝试其第二、第三偏好,依次类推。

当你希望尊重用户的所有语言偏好,而不需要手动构建回退链时,这种模式非常有效。

console.log(navigator.languages);
// ["fr-CA", "fr", "en-US", "en"]

const numberFormatter = new Intl.NumberFormat(navigator.languages, {
  style: "currency",
  currency: "USD"
});

console.log(numberFormatter.format(1234.56));
// Tries Canadian French first
// Falls back through the user's complete preference list

Intl API 会依次评估数组中的每种语言,并选择第一个支持的语言,因此这是处理多样化用户偏好的强大方案。

结合应用支持与用户偏好

对于仅支持有限本地化的应用,可以在创建格式化器前,先将用户偏好过滤为你支持的语言。这可以确保你只会尝试应用能够处理的语言环境。

const supportedLocales = ["en-US", "es-MX", "fr-FR", "de-DE"];

function findBestLocale(userPreferences, appSupported) {
  const preferences = userPreferences.map(pref => {
    const parts = pref.split("-");
    return [pref, parts[0]];
  }).flat();

  for (const pref of preferences) {
    if (appSupported.includes(pref)) {
      return pref;
    }
  }

  return appSupported[0];
}

const userLocale = findBestLocale(navigator.languages, supportedLocales);
const formatter = new Intl.DateTimeFormat(userLocale);

console.log(formatter.resolvedOptions().locale);
// Selected locale matches both user preference and app support

此函数会在用户偏好和支持的语言环境之间查找第一个匹配项,并尝试去除区域码以实现更广泛的匹配。

如果需要更复杂的匹配,可以检查用户偏好中哪些语言环境在当前运行时的 Intl API 中受支持。

const supportedLocales = ["en-US", "es-MX", "fr-FR", "de-DE"];

function findBestSupportedLocale(userPreferences, appSupported) {
  const runtimeSupported = Intl.DateTimeFormat.supportedLocalesOf(appSupported);

  for (const pref of userPreferences) {
    if (runtimeSupported.includes(pref)) {
      return pref;
    }

    const lang = pref.split("-")[0];
    const match = runtimeSupported.find(s => s.startsWith(lang));
    if (match) {
      return match;
    }
  }

  return runtimeSupported[0] || "en";
}

const userLocale = findBestSupportedLocale(navigator.languages, supportedLocales);
const formatter = new Intl.DateTimeFormat(userLocale);

这种方法可以确保所选语言环境既被你的应用支持,也被 JavaScript 运行时支持。

处理没有匹配语言环境的情况

如果没有任何请求的语言环境被支持,Intl API 会回退到系统默认语言环境。该默认值因环境而异,由操作系统、浏览器或 Node.js 配置决定。

const unsupportedLocales = ["non-existent-locale"];
const formatter = new Intl.DateTimeFormat(unsupportedLocales);

console.log(formatter.resolvedOptions().locale);
// Shows the system default locale
// Might be "en-US" or another locale depending on the system

系统默认值可以确保格式化器始终可用,即使传入完全无效或不受支持的语言环境列表,你的应用也不会因语言环境问题抛出错误。

为了确保有特定的回退选项,而不是依赖系统默认值,建议在语言环境数组末尾始终包含像 enen-US 这样广泛支持的语言环境。

const locales = ["xyz-INVALID", "en"];
const formatter = new Intl.DateTimeFormat(locales);

console.log(formatter.resolvedOptions().locale);
// Will use "en" since the first locale is invalid
// Guaranteed fallback to English instead of system default

这种模式使您的应用在不同环境下的行为更加可预测。

生产环境应用的实用模式

在构建生产环境应用时,应结合多种回退策略,以确保在多样化用户群体中实现健壮的本地化处理。

一种常见模式是创建带有完整回退链的格式化器,链路包括用户偏好、应用支持的语言环境,以及一个保证的最终回退选项。

class LocaleManager {
  constructor(supportedLocales) {
    this.supportedLocales = supportedLocales;
    this.defaultLocale = "en-US";
  }

  buildLocaleChain(userPreference) {
    const chain = [];

    if (userPreference) {
      chain.push(userPreference);

      const lang = userPreference.split("-")[0];
      if (lang !== userPreference) {
        chain.push(lang);
      }
    }

    chain.push(...navigator.languages);
    chain.push(...this.supportedLocales);
    chain.push(this.defaultLocale);

    const unique = [...new Set(chain)];
    return unique;
  }

  createDateFormatter(userPreference, options = {}) {
    const locales = this.buildLocaleChain(userPreference);
    return new Intl.DateTimeFormat(locales, options);
  }

  createNumberFormatter(userPreference, options = {}) {
    const locales = this.buildLocaleChain(userPreference);
    return new Intl.NumberFormat(locales, options);
  }
}

const manager = new LocaleManager(["en-US", "es-MX", "fr-FR", "de-DE"]);
const dateFormatter = manager.createDateFormatter("pt-BR");

console.log(dateFormatter.resolvedOptions().locale);
// Uses comprehensive fallback chain
// Tries Portuguese for Brazil
// Falls back to Portuguese
// Falls back through navigator.languages
// Falls back through supported locales
// Guaranteed to use en-US if nothing else matches

此类封装了语言环境回退逻辑,确保应用内行为一致。

对于需要在运行时响应用户语言环境变更的应用,可将语言环境管理器与事件监听器结合使用。

class LocaleAwareFormatter {
  constructor(supportedLocales) {
    this.supportedLocales = supportedLocales;
    this.updateFormatters();

    window.addEventListener("languagechange", () => {
      this.updateFormatters();
    });
  }

  updateFormatters() {
    const locales = [...navigator.languages, ...this.supportedLocales, "en"];

    this.dateFormatter = new Intl.DateTimeFormat(locales);
    this.numberFormatter = new Intl.NumberFormat(locales);
  }

  formatDate(date) {
    return this.dateFormatter.format(date);
  }

  formatNumber(number) {
    return this.numberFormatter.format(number);
  }
}

const formatter = new LocaleAwareFormatter(["en-US", "es-MX", "fr-FR"]);
console.log(formatter.formatDate(new Date()));
// Automatically updates when user changes language preferences

这种模式创建的格式化器可与浏览器语言变更保持同步,确保应用始终根据当前偏好显示内容。

总结

语言环境回退可确保即使用户偏好未被明确支持,应用也能正确显示格式化内容。Intl API 通过接受语言环境数组并选择第一个受支持的选项,自动处理回退。

关键概念:

  • 向 Intl 构造函数传递语言环境数组以实现自动回退
  • 运行时会从数组中选择第一个受支持的语言环境
  • 使用 supportedLocalesOf() 检查可用的语言环境
  • 构建从具体到通用的回退链
  • localeMatcher 选项控制匹配算法
  • 直接传递 navigator.languages 以自动处理用户偏好
  • 始终包含如英语等广泛支持的最终回退选项
  • 当没有匹配的语言环境时,运行时会回退到系统默认设置

大多数应用建议使用带有语言环境数组的自动回退。若需对回退顺序有特定控制,可实现自定义回退逻辑。