使用 Temporal 支援全球日曆系統

網頁功能: Temporal

傳統 JavaScript Date 物件是以預測公曆為基礎,因此如果使用者採用其他曆法系統 (例如伊斯蘭曆 (陰曆)、希伯來曆 (陰陽曆) 或農曆 (陰陽曆)),就很難為他們建構應用程式。開發人員以往必須仰賴複雜的第三方程式庫或手動計算,才能支援這些系統。

Temporal API 為多種日曆系統提供一流的支援。Temporal 會將日曆 ID 與日期物件建立關聯,以原生方式處理不同文化背景所需的複雜算術和格式設定。

導入方式

如要使用 Temporal 支援全球日曆系統:

  1. 建立關聯日曆 (必要):建立或轉換 Temporal 物件 (例如 Temporal.PlainDate) 時,必須使用 withCalendar() 指定所需日曆系統,才能執行與日曆相關的操作。
  2. 使用穩定 ID (陰陽曆必填):您必須使用 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();
})();