获取可用日历系统列表

了解您的 JavaScript 环境支持哪些日历系统

简介

在为全球用户构建应用程序时,用户通常需要以其偏好的日历系统查看日期。虽然您可能熟悉大多数西方国家使用的公历(格里高利历),但许多文化实际上采用完全不同的日历系统,如伊斯兰(回历)、希伯来历或佛历等。

为了让用户选择其偏好的日历系统,您需要了解 JavaScript 环境支持哪些日历系统。与其维护一个容易过时或包含不受支持值的硬编码列表,不如利用 JavaScript 在运行时动态获取可用日历系统的方法。

Intl.supportedValuesOf() 方法配合 "calendar" 参数,可以返回当前环境支持的所有日历系统标识符数组。这样可以确保您的应用只为用户提供实际可用的日历选项。

什么是日历系统

日历系统是组织和计量时间的不同方法。所有日历都记录天、月和年,但它们在确定一年何时开始、每个月有多长以及如何处理天文周期等方面采用了不同的规则。

格里高利历(公历)于 1582 年引入,是目前全球最广泛使用的民用日历。它采用约 365.25 天的太阳年,分为 12 个月,每四年增加一个闰年(百年年份有例外)。

其他日历系统则遵循不同的规则。伊斯兰历是纯粹的阴历,月份根据月相变化,因此伊斯兰年比太阳年短约 11 天,导致伊斯兰日期会随着时间在季节中轮转。希伯来历和中国农历属于阴阳合历,结合了阴历月份并通过调整与太阳年保持同步。

在 JavaScript 中格式化日期时,所使用的历法系统决定了显示的年份、月份和日期数字。同一时刻在不同的历法系统中会有不同的日期表示方式。

使用 Intl.supportedValuesOf 获取历法系统

Intl.supportedValuesOf() 方法接受一个字符串参数,用于指定要返回哪种类型的值。要获取历法系统,请传入 "calendar"

const calendars = Intl.supportedValuesOf("calendar");
console.log(calendars);
// Output: ["buddhist", "chinese", "coptic", "dangi", "ethioaa",
//          "ethiopic", "gregory", "hebrew", "indian", "islamic",
//          "islamic-civil", "islamic-rgsa", "islamic-tbla",
//          "islamic-umalqura", "iso8601", "japanese", "persian", "roc"]

该方法返回一个字符串数组,每个字符串代表一个历法标识符。这些标识符遵循 Unicode CLDR(通用区域数据存储库)标准,为跨平台和多种编程语言提供了一致的历法系统引用方式。

返回的数组具有以下特点:

  • 值按字母升序排列
  • 重复值会被移除
  • 每个标识符使用小写字母和连字符
  • 列表包含 JavaScript 实现所支持的所有历法系统

不同的浏览器和 JavaScript 运行环境支持的历法系统集合不同,但所有现代浏览器都支持一组常用的核心历法系统。

理解历法标识符

每个历法标识符代表一种被一个或多个文化使用的特定历法系统。以下是最常见的标识符:

"gregory" 标识符代表公历(格里高利历),这是大多数国家使用的标准民用历法。如果你生活在美国、欧洲或世界上大多数其他地区,日常生活中用的就是这种历法。

"buddhist" 标识符代表泰国佛历,该历法的月份和日期结构与公历相同,但年份从佛陀诞生年(公历前 543 年)开始计算。公历 2025 年在佛历中是 2568 年。

"chinese" 标识符表示中国传统农历,这是一种阴阳合历,月份依据月相变化,年份与木星的公转周期相对应。中国农历常用于确定传统节日,如春节。

"islamic" 标识符表示伊斯兰历(Hijri),这是一种纯粹的阴历,一年有 12 个月,每月 29 或 30 天。年份从公元 622 年穆罕默德从麦加迁徙到麦地那(希吉拉)开始计算。

"hebrew" 标识符表示希伯来历,这是一种用于犹太宗教活动的阴阳合历。其年份从传统的创世日期(公历前 3761 年)开始计算。

"japanese" 标识符表示日本历法,其月份和日期结构与公历相同,但以天皇在位时期划分纪元。目前的纪元为令和,自 2019 年开始。

"persian" 标识符表示伊朗和阿富汗使用的太阳希吉拉历。这是一种以太阳年为基础的历法,年份从希吉拉(Hijra)开始,与伊斯兰阴历不同。

其他标识符包括 "coptic"(科普特正教历)、"dangi"(朝鲜传统历)、"ethiopic"(埃塞俄比亚历)、"indian"(印度国历)以及 "roc"(台湾使用的中华民国历)。

部分标识符有变体,如 "islamic-civil""islamic-rgsa""islamic-tbla""islamic-umalqura",它们表示伊斯兰历的不同计算方法。

"iso8601" 标识符表示 ISO 8601 历法,本质上是公历,但始终采用前推公历(即将公历向前扩展到 1582 年引入之前)。

日历系统实际应用演示

为了理解日历系统如何影响日期格式化,请使用不同的日历系统格式化同一个日期:

const date = new Date("2025-10-15");

const gregorian = new Intl.DateTimeFormat("en-US", {
  calendar: "gregory",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const islamic = new Intl.DateTimeFormat("en-US", {
  calendar: "islamic",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const hebrew = new Intl.DateTimeFormat("en-US", {
  calendar: "hebrew",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const buddhist = new Intl.DateTimeFormat("en-US", {
  calendar: "buddhist",
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(gregorian.format(date));
// Output: "October 15, 2025"

console.log(islamic.format(date));
// Output: "Rabi' II 16, 1447 AH"

console.log(hebrew.format(date));
// Output: "Tishrei 23, 5786"

console.log(buddhist.format(date));
// Output: "October 15, 2568 BE"

同一个 JavaScript Date 对象表示同一时刻,但每种日历系统会用不同的年份、月份和日期来表达该时刻。公历日期 2025 年 10 月 15 日,在伊斯兰历中对应 1447 年 Rabi' II 16 日,在希伯来历中对应 5786 年 Tishrei 23 日,在佛历中对应 2568 年 10 月 15 日。

这说明了为什么在为使用不同日历的用户格式化日期时,需要明确指定所用的日历系统。

构建日历选择器

在创建允许用户选择首选日历的用户界面时,应查询可用的日历并动态构建选择器:

function buildCalendarSelector() {
  const calendars = Intl.supportedValuesOf("calendar");
  const select = document.createElement("select");
  select.id = "calendar-selector";

  calendars.forEach(calendar => {
    const option = document.createElement("option");
    option.value = calendar;
    option.textContent = calendar;
    select.appendChild(option);
  });

  return select;
}

const selector = buildCalendarSelector();
document.body.appendChild(selector);

这样会创建一个包含所有支持日历的下拉菜单。但像 "gregory""islamic" 这样的技术标识符对用户来说并不友好。用户需要看到用自己语言显示的描述性名称。

显示可读性强的日历名称

Intl.DisplayNames API 可以将日历标识符转换为可读名称。使用它可以创建更友好的日历选择器:

function buildCalendarSelector(locale = "en-US") {
  const calendars = Intl.supportedValuesOf("calendar");
  const displayNames = new Intl.DisplayNames([locale], { type: "calendar" });

  const select = document.createElement("select");
  select.id = "calendar-selector";

  calendars.forEach(calendar => {
    const option = document.createElement("option");
    option.value = calendar;
    option.textContent = displayNames.of(calendar);
    select.appendChild(option);
  });

  return select;
}

const selector = buildCalendarSelector("en-US");
document.body.appendChild(selector);

现在下拉菜单会显示“公历”、“伊斯兰历”、“希伯来历”等名称,而不是技术标识符。

通过更改 locale,可以用不同语言显示日历名称:

const calendars = ["gregory", "islamic", "hebrew", "buddhist", "chinese"];

const englishNames = new Intl.DisplayNames(["en-US"], { type: "calendar" });
const frenchNames = new Intl.DisplayNames(["fr-FR"], { type: "calendar" });
const arabicNames = new Intl.DisplayNames(["ar-SA"], { type: "calendar" });

calendars.forEach(calendar => {
  console.log(`${calendar}:`);
  console.log(`  English: ${englishNames.of(calendar)}`);
  console.log(`  French: ${frenchNames.of(calendar)}`);
  console.log(`  Arabic: ${arabicNames.of(calendar)}`);
});

// Output:
// gregory:
//   English: Gregorian Calendar
//   French: calendrier grégorien
//   Arabic: التقويم الميلادي
// islamic:
//   English: Islamic Calendar
//   French: calendrier musulman
//   Arabic: التقويم الهجري
// ...

这样用户就能以自己偏好的语言看到日历名称。

检查特定日历是否受支持

在应用中使用某个日历前,应检查 JavaScript 环境是否支持该日历。这可以防止运行时错误,并便于实现降级处理:

function isCalendarSupported(calendar) {
  const supported = Intl.supportedValuesOf("calendar");
  return supported.includes(calendar);
}

if (isCalendarSupported("islamic")) {
  const formatter = new Intl.DateTimeFormat("en-US", {
    calendar: "islamic",
    year: "numeric",
    month: "long",
    day: "numeric"
  });
  console.log(formatter.format(new Date()));
} else {
  console.log("Islamic calendar is not supported");
}

此模式适用于任何日历标识符。您可以用它在用户界面中显示或隐藏日历选项,或在首选日历不可用时回退到默认日历。

创建可复用的特性检测函数

构建一个通用函数,用于检测日历支持情况,并优雅地处理缺失的日历:

function getCalendarOrFallback(preferredCalendar, fallbackCalendar = "gregory") {
  const supported = Intl.supportedValuesOf("calendar");

  if (supported.includes(preferredCalendar)) {
    return preferredCalendar;
  }

  if (supported.includes(fallbackCalendar)) {
    return fallbackCalendar;
  }

  return supported[0];
}

const calendar = getCalendarOrFallback("islamic", "gregory");
const formatter = new Intl.DateTimeFormat("en-US", {
  calendar,
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.format(new Date()));

该函数会尝试使用首选日历,如果不可用则回退到指定的备用日历,若两者都不支持,则最终返回第一个可用的日历。

为您的应用筛选日历

大多数应用无需支持所有日历系统。请筛选列表,仅包含与您的用户相关的日历:

function getRelevantCalendars(allowedCalendars) {
  const supported = Intl.supportedValuesOf("calendar");
  return allowedCalendars.filter(calendar => supported.includes(calendar));
}

const allowedCalendars = ["gregory", "islamic", "hebrew", "buddhist"];
const availableCalendars = getRelevantCalendars(allowedCalendars);

console.log(availableCalendars);
// Output: ["gregory", "islamic", "hebrew", "buddhist"]
// (assuming all are supported in the current environment)

这样可以确保您只提供既满足需求又能在用户浏览器中正常工作的日历。

您可以将此与日历选择结合,打造聚焦的用户界面:

function buildFilteredCalendarSelector(allowedCalendars, locale = "en-US") {
  const supported = Intl.supportedValuesOf("calendar");
  const available = allowedCalendars.filter(cal => supported.includes(cal));
  const displayNames = new Intl.DisplayNames([locale], { type: "calendar" });

  const select = document.createElement("select");

  available.forEach(calendar => {
    const option = document.createElement("option");
    option.value = calendar;
    option.textContent = displayNames.of(calendar);
    select.appendChild(option);
  });

  return select;
}

const selector = buildFilteredCalendarSelector(
  ["gregory", "islamic", "hebrew", "buddhist"],
  "en-US"
);
document.body.appendChild(selector);

这样会创建一个只显示您希望支持的日历的选择器,带有人类可读的名称,并保证在当前环境下可用。

按类别分组日历

对于支持多种日历的应用,可按类型或地区对其分组,以提升可用性:

function groupCalendars() {
  const calendars = Intl.supportedValuesOf("calendar");

  const groups = {
    solar: ["gregory", "iso8601", "persian", "ethiopic", "coptic"],
    lunar: ["islamic", "islamic-civil", "islamic-rgsa", "islamic-tbla", "islamic-umalqura"],
    lunisolar: ["hebrew", "chinese", "dangi"],
    erasBased: ["japanese", "roc", "buddhist"],
    other: []
  };

  const grouped = {
    solar: [],
    lunar: [],
    lunisolar: [],
    erasBased: [],
    other: []
  };

  calendars.forEach(calendar => {
    let placed = false;

    for (const [group, members] of Object.entries(groups)) {
      if (members.includes(calendar)) {
        grouped[group].push(calendar);
        placed = true;
        break;
      }
    }

    if (!placed) {
      grouped.other.push(calendar);
    }
  });

  return grouped;
}

const grouped = groupCalendars();
console.log(grouped);
// Output:
// {
//   solar: ["gregory", "iso8601", "persian", "ethiopic", "coptic"],
//   lunar: ["islamic", "islamic-civil", "islamic-rgsa", ...],
//   lunisolar: ["hebrew", "chinese", "dangi"],
//   erasBased: ["japanese", "roc", "buddhist"],
//   other: ["ethioaa", "indian"]
// }

这种组织方式有助于用户了解不同日历系统的特性,并找到所需的日历。

处理不支持的环境

Intl.supportedValuesOf() 方法于 2022 年添加到 JavaScript 中。旧版浏览器不支持该方法。请在使用前检查该方法是否存在:

function getCalendars() {
  if (typeof Intl.supportedValuesOf === "function") {
    return Intl.supportedValuesOf("calendar");
  }

  return ["gregory"];
}

const calendars = getCalendars();
console.log(calendars);

在现代浏览器中,这将返回支持的全部日历列表;在较旧的环境下,则会回退为仅支持公历(Gregorian calendar)。

为了更好地兼容旧版环境,建议提供更完整的回退日历列表:

function getCalendars() {
  if (typeof Intl.supportedValuesOf === "function") {
    return Intl.supportedValuesOf("calendar");
  }

  return [
    "buddhist",
    "chinese",
    "coptic",
    "ethiopic",
    "gregory",
    "hebrew",
    "indian",
    "islamic",
    "japanese",
    "persian",
    "roc"
  ];
}

这样可以为旧版浏览器提供一组合理的日历选项,但无法保证所有日历在每个环境中都能正常工作。

理解日历与区域设置(locale)的区别

日历和区域设置(locale)相关但并不相同。区域设置决定语言和地区格式规范,而日历决定使用哪种历法系统。

单一的区域设置可以使用多种日历。例如,沙特阿拉伯的阿拉伯语用户通常在宗教场合使用伊斯兰历,在民用场合使用公历。你可以用阿拉伯语格式化任意一种日历:

const date = new Date("2025-10-15");

const arabicGregorian = new Intl.DateTimeFormat("ar-SA", {
  calendar: "gregory",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const arabicIslamic = new Intl.DateTimeFormat("ar-SA", {
  calendar: "islamic",
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(arabicGregorian.format(date));
// Output: "١٥ أكتوبر ٢٠٢٥"

console.log(arabicIslamic.format(date));
// Output: "١٦ ربيع الآخر ١٤٤٧ هـ"

两者都使用阿拉伯文和阿拉伯数字,但根据各自的日历显示不同的日期。

相反,你也可以用不同的语言格式化同一种日历:

const date = new Date("2025-10-15");

const englishIslamic = new Intl.DateTimeFormat("en-US", {
  calendar: "islamic",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const arabicIslamic = new Intl.DateTimeFormat("ar-SA", {
  calendar: "islamic",
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(englishIslamic.format(date));
// Output: "Rabi' II 16, 1447 AH"

console.log(arabicIslamic.format(date));
// Output: "١٦ ربيع الآخر ١٤٤٧ هـ"

两者都显示伊斯兰历日期,但使用不同的语言和数字系统。

何时查询可用日历

在以下场景中应查询可用日历:

在构建日历选择界面时,调用 Intl.supportedValuesOf("calendar") 以填充选项。这可以确保只显示当前环境下可用的日历。

在实现基于日历的功能时,使用前应检查所需日历是否受支持。这可以防止出错,并允许平滑回退到其他日历。

在存储用户偏好设置时,应将日历选择与支持列表进行校验,确保保存的偏好在不同设备和浏览器中始终有效。

在不同环境之间迁移时,请检查源环境和目标环境的日历支持情况。不同浏览器版本、Node.js 版本以及各类 JavaScript 运行时的日历支持可能存在差异。

在加载依赖日历的数据时,请在解析或格式化日期前,先确认所需日历是否可用。这可以防止在特定日历系统下处理日期时出现运行时错误。