Поддержка глобальных календарных систем с помощью Temporal

Веб-функции: 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() для сравнения двух дат в определенной системе календаря вместо сравнения свойств Year/Month/Day вручную.
  • РЕКОМЕНДУЕТСЯ использовать toLocaleString() для форматирования дат вместо объединения строк вручную.
  • РЕКОМЕНДУЕТСЯ проверять, поддерживается ли целевая календарная система средой, с помощью Intl.supportedValuesOf('calendar'), прежде чем создавать объекты Temporal, относящиеся к календарю.
  • УЧИТЫВАЙТЕ, что некоторые календари (например, варианты исламского календаря) могут опираться на визуальные наблюдения, а не на фиксированные расчеты. API Temporal следует реализации Intl среды, которая обычно использует расчетные приближения. Для критически важных расчетов дат, связанных с культурой или религией, проконсультируйтесь с экспертами или используйте специализированные библиотеки.
  • УЧИТЫВАЙТЕ названия эр при использовании календарей, в которых они есть (например, японского или буддийского), с помощью toLocaleString().

Резервная стратегия

Резервные варианты и поддержка Temporal в браузерах

Для браузеров, которые пока не поддерживают нативный API Temporal, используйте обнаружение функций и динамически загружайте temporal-polyfill (около 15 КБ 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();
})();