通过 Temporal 支持全球日历系统

Web 功能: Temporal

传统的 JavaScript Date 对象基于预推格里历,因此很难为依赖其他历法系统(例如伊斯兰历 [阴历]、希伯来历 [阴阳历] 或农历 [阴阳历])的用户构建应用。以前,开发者必须依赖复杂的第三方库或手动计算来支持这些系统。

Temporal API 为多种日历系统提供一流的支持。通过将日历标识符与日期对象相关联,Temporal 可以原生处理不同文化背景所需的复杂算术和格式。

如何实现

如需支持使用 Temporal 的全球日历系统,请执行以下操作:

  1. 关联日历(强制性):创建或转换 Temporal 对象(例如 Temporal.PlainDate)时,您必须使用 withCalendar() 指定所需的日历系统,才能执行与日历相关的操作。
  2. 使用稳定标识符(对于阴阳历,此项为强制要求):您必须使用 monthCode 而非数字 month 索引来标识阴阳历中不同年份的特定月份。仅适用于使用闰月的日历,例如希伯来历和农历。
  3. 遵循日历不变量(强制性):在遍历月份或日期时,不得假设固定值(例如一年 12 个月或一个月 31 天)。使用 monthsInYear 和 daysInMonth 等属性,确保您的代码适用于所有日历。
  4. 使用感知日历的比较(强制性):在比较特定日历系统中的日期时,您必须使用 Temporal.PlainDate.compare(),而不是手动比较年/月/日属性。这可以正确处理日历规则中的时间顺序。

示例代码:转换和迭代日历

// 1. Helper to check calendar support
function isCalendarSupported(calendarId) {
  try {
    return Intl.supportedValuesOf('calendar').includes(calendarId);
  } catch {
    // Fallback for environments where supportedValuesOf is not available
    return false;
  }
}

// 2. Get current date in default ISO 8601 calendar
const isoDate = Temporal.Now.plainDateISO();

// 3. Convert to Hebrew calendar if supported
const calendarId = 'hebrew';
const targetDate = isCalendarSupported(calendarId) 
  ? isoDate.withCalendar(calendarId)
  : isoDate; // Fallback to ISO if not supported

if (targetDate.calendar.id !== calendarId) {
  console.warn(`Calendar ${calendarId} not supported; falling back to ISO 8601`);
}

// 4. Log properties specific to the calendar
console.log(`Calendar: ${targetDate.calendar.id}`);
console.log(`Year: ${targetDate.year}`);
console.log(`Month Code: ${targetDate.monthCode}`); // Stable across leap years

// 5. Safely iterate through months in the current year
for (let m = 1; m <= targetDate.monthsInYear; m++) {
  console.log(`Month ${m} has ${targetDate.with({ month: m }).daysInMonth} days.`);
}

// 6. Compare dates within the same calendar
const today = Temporal.Now.plainDateISO().withCalendar(calendarId);
const comparison = Temporal.PlainDate.compare(targetDate, today);
const relative = comparison < 0 ? 'Past' : comparison > 0 ? 'Future' : 'Today';
console.log(`Timeline: ${relative}`);

// 7. Format for display using toLocaleString (including era for era-based calendars like 'japanese')
const localizedDisplay = targetDate.toLocaleString('en-u-ca-hebrew', {
  day: 'numeric',
  month: 'long',
  year: 'numeric',
  era: 'long'
});

策略性实施和最佳实践

  • 务必在循环遍历月份时使用 monthsInYear 作为上限,而不是假设为 12。
  • 请使用 monthCode 标识使用闰月的日历(例如希伯来历或农历)中的特定月份,无论年份如何。
  • 请勿假设 date.month === 12 是一年中的最后一个月。使用 date.month === date.monthsInYear。
  • 请勿假设 inLeapYear === true 表示一年仅多一天。在阴阳历中,可能会增加一个完整的闰月。
  • 要在比较特定日历系统中的两个日期时使用 Temporal.PlainDate.compare(),而不是手动比较年/月/日属性。
  • 务必使用 toLocaleString() 为用户设置日期格式,而不是手动串联字符串。
  • 务必先验证目标日历系统是否受使用 Intl.supportedValuesOf('calendar') 的环境支持,然后再创建特定于日历的 Temporal 对象。
  • 务必注意,某些日历(例如伊斯兰历的变体)可能依赖于视觉观察,而不是固定计算。Temporal API 遵循环境的 Intl 实现,通常使用计算出的近似值。对于重要的文化或宗教日期计算,请向领域专家核实或使用专业库。
  • 务必在使用纪元的日历(例如日本日历、佛历)时,使用 toLocaleString() 考虑纪元名称。

后备策略

Temporal 的回退和浏览器支持

对于尚不支持原生 Temporal API 的浏览器,请使用功能检测并动态加载 temporal-polyfill(约 15 KB min+gz)。

请注意,导入 temporal-polyfill 不会自动将 Temporal 对象分配给全局范围。如果您的代码依赖于 globalThis.Temporal,请明确分配它。

// Check if Temporal is supported natively
(async () => {
  if (typeof Temporal === 'undefined') {
    // Load the polyfill conditionally
    const module = await import('https://esm.sh/temporal-polyfill');
    globalThis.Temporal = module.Temporal;
    // Extend Date.prototype if needed
    Date.prototype.toTemporalInstant = module.toTemporalInstant;
  }
  initializeApp();
})();