في السابق، كان احتساب الوقت المنقضي أو المتبقي بين الأحداث (مثل انتهاء صلاحية الفترة التجريبية أو العد التنازلي للاشتراك أو نوافذ اتفاقية مستوى الخدمة) وعرض هذه الفترات بتنسيق مترجَم يمكن لشخص عادي قراءته يتطلّب إجراء عمليات حسابية يدوية أو استخدام مكتبات خارجية. يتتبّع عنصر Date القديم فقط الملّي ثانية من بداية العصر، ما يجعل العمليات الحسابية لأشهر التقويم والانتقال إلى نظام التوقيت الصيفي (DST) وموازنة الوحدات عرضة للأخطاء.
تجمع واجهة برمجة التطبيقات Temporal بين Temporal.ZonedDateTime وTemporal.Duration وIntl.DurationFormat لتوفير فروق زمنية آمنة للتوقيت الصيفي، وموازنة الوحدات القابلة للضبط، ومقارنات المدة المستندة إلى التقويم، والتنسيق المترجَم.
كيفية الإعداد
- الحصول على نقاط نهاية
Temporal.ZonedDateTime: استخدِمTemporal.Now.zonedDateTimeISO()للوقت الحالي في المنطقة الزمنية للنظام، أو أدخِل سلسلة منطقة زمنية صريحة من IANA، وهيTemporal.Now.zonedDateTimeISO(timeZone)، عندما تختلف المنطقة الزمنية للحدث المستهدف عن الإعداد التلقائي لنظام المستخدم. - احتساب الفروق باستخدام
.since()و.until():- استخدِم
now.since(start, options)لاحتساب الوقت المنقضي منذ حدث سابق. - استخدِم
now.until(end, options)لاحتساب الوقت المتبقي حتى حدث مستقبلي. - يجب تمرير
largestUnit(مثل'year'و'month'و'day'و'hour') وsmallestUnit(مثل'minute') لموازنة قيمةTemporal.Durationالناتجة وتقريبها.
- استخدِم
- موازنة عناصر
Temporal.Durationالمستقلة أو مقارنتها:- يمكنك إنشاء مُدد مستقلة باستخدام
Temporal.Duration.from({ minutes: 90 })وإعادة موازنة الوحدات باستخدامduration.round({ largestUnit: 'hour' })(مع توفيرrelativeToعند الموازنة بين الأشهر أو السنوات التقويمية أو انتقالات التوقيت الصيفي). - يمكنك مقارنة مدّتين باستخدام
Temporal.Duration.compare(d1, d2, { relativeTo })، مع تمريرPlainDateأوZonedDateTimeكـrelativeTo، وذلك عندما تحتوي أيّ من المدّتين على وحدات تقويم (yearsأوmonthsأوweeks) أو تتجاوز حدود التوقيت الصيفي.
- يمكنك إنشاء مُدد مستقلة باستخدام
- التنسيق المناسب للمستخدمين مقابل التنسيق المناسب للأجهزة:
- استخدِم
duration.toLocaleString(locale, options)(الذي يفوّض إلىIntl.DurationFormat) للنصوص المترجَمة الموجّهة للمستخدمين. - استخدِم السمة
duration.toString()فقط لسلاسل المدة ISO 8601 التي يمكن معالجتها آليًا (مثلP1M2DT4H).
- استخدِم
مثال: اختلاف انتهاء صلاحية الفترة التجريبية والتنسيق المترجَم
// 1. Get the current time in an explicit target IANA time zone (or omit the
// argument to use the system's current time zone).
const tz = 'America/New_York';
const now = Temporal.Now.zonedDateTimeISO(tz);
// 2. Combine separate date and time strings directly.
// Do not build an ISO string with template literals; chain Temporal methods instead.
const startDateStr = '2025-01-01';
const startTimeStr = '12:00:00';
const endDateStr = '2025-01-31';
const endTimeStr = '12:00:00';
const start = Temporal.PlainDate.from(startDateStr).toPlainDateTime(startTimeStr).toZonedDateTime(tz);
const end = Temporal.PlainDate.from(endDateStr).toPlainDateTime(endTimeStr).toZonedDateTime(tz);
// 3. Calculate differences using .since() and .until()
// ZonedDateTime differences default to largestUnit: 'hour' (days are not a fixed
// length across DST, so Temporal won't balance into days unless asked). Pass
// largestUnit: 'day' or a calendar unit ('week', 'month', 'year') when you want a
// human-scale breakdown instead of e.g. PT720H. smallestUnit rounds away
// seconds and sub-second noise so the formatted string stays readable.
const timeActive = now.since(start, { largestUnit: 'year', smallestUnit: 'minute' });
const timeRemaining = now.until(end, { largestUnit: 'year', smallestUnit: 'minute' });
// 4. Format durations for display with .toLocaleString() (Intl.DurationFormat)
// instead of manually concatenating unit numbers and suffixes. 'en-US' is used
// here for deterministic output; pass undefined to use the user's locale.
console.log(`Active: ${timeActive.toLocaleString('en-US', { style: 'narrow' })}`);
console.log(`Remaining: ${timeRemaining.toLocaleString('en-US', { style: 'long' })}`);
// Use .toString() for ISO 8601 serialization (e.g., API payloads or <time datetime="...">)
console.log(`ISO 8601: ${timeRemaining.toString()}`);
// 5. Check status via .sign, and compare durations with Temporal.Duration.compare()
const isExpired = timeRemaining.sign < 0;
// Pass relativeTo when comparing durations that may include calendar months/years or DST shifts
const isPastHalfway = Temporal.Duration.compare(timeActive, timeRemaining, { relativeTo: now }) > 0;
مثال: موازنة مُدد إعلانات الفيديو المستقلة (round وIntl.DurationFormat)
// Convert a raw unit count (e.g., 90 minutes) into a balanced breakdown (1 hour, 30 minutes)
const rawDuration = Temporal.Duration.from({ minutes: 90 });
const balanced = rawDuration.round({ largestUnit: 'hour' });
// Format via duration.toLocaleString() or directly with Intl.DurationFormat
const formatter = new Intl.DurationFormat('en-US', { style: 'long' });
console.log(formatter.format(balanced)); // "1 hour, 30 minutes"
التنفيذ الاستراتيجي وأفضل الممارسات
- يُرجى استخدام
Temporal.ZonedDateTimeلإجراء العمليات الحسابية التي تتضمّن أحداثًا واقعية تحدث في مناطق زمنية محدّدة (مثل تجديد الاشتراكات أو العد التنازلي للأحداث)، وتمريرtimeZoneصريح إلىTemporal.Now.zonedDateTimeISO(timeZone)عند إجراء العمليات الحسابية في منطقة زمنية غير المنطقة التلقائية للنظام. - يجب دمج سلاسل التاريخ والوقت المنفصلة باستخدام طرق Temporal، مثل
Temporal.PlainDate.from(dateStr).toPlainDateTime(timeStr).toZonedDateTime(tz)أوTemporal.PlainDate.from(dateStr).toZonedDateTime({ timeZone: tz, plainTime: timeStr })، بدلاً من استكمال سلسلة ISO مثل`${dateStr}T${timeStr}`. - يجب تمرير
largestUnit(مثل'day'أو'month'أو'year') في.since()أو.until()أو.round()عندما تريد موازنة المدد بعد الساعات، وتكون طرق الفرق فيZonedDateTimeتلقائيًا على'hour'. - يجب استخدام
Temporal.Duration.compare(d1, d2, { relativeTo })عند مقارنة مدّتين، مع توفير نقطة ارتساءrelativeToكلما تضمنت أي من المدّتين وحدات تقويم (monthsأوyears) أو أيامًا تراعي المنطقة الزمنية. - يجب تنسيق المدد لعرضها للمستخدم باستخدام
duration.toLocaleString()أوnew Intl.DurationFormat(locale, { style })('long'أو'short'أو'narrow'أو'digital') بدلاً من الاستكمال اليدوي لـ${duration.years}y ${duration.months}m.... - يجب استخدام
duration.toString()لسلاسل المدة ISO 8601 التي يمكن معالجتها آليًا (مثلPT1H30M)، وليس كنص موجّه للمستخدمين. - يجب عرض
PlainTimeعلى شكلHH:MM(مثلاً، لـ<input type="time">) باستخدام.toPlainTime().toString({ smallestUnit: 'minute' })بدلاً من السلسلة.slice(0, 5). - لا تعدِّل النُسخ مباشرةً، لأنّ كائنات
Temporalغير قابلة للتغيير. تعرض العمليات مثل.add()أو.subtract()أو.round()أو.with()مثيلاً جديدًا. - لا تستخدِم حسابات
Dateأو.getTime()لحساب الفارق الزمني، لأنّDateلا يتضمّن حسابات تقويم تراعي المنطقة الزمنية، بل يقتصر على دقة الملّي ثانية. - يجب استخدام السمة الأصلية
.signللسمةTemporal.Duration(أوTemporal.ZonedDateTime.compare()) المحسوبة للتحقّق مما إذا كانت المدة تمثّل نقطة زمنية سابقة أو منتهية الصلاحية (sign < 0) أو بداية مستقبلية معلّقة.
استراتيجية الإجراءات الاحتياطية
الخيارات الاحتياطية وتوافق المتصفّحات مع Temporal
بالنسبة إلى المتصفّحات التي لا تتيح استخدام واجهة برمجة التطبيقات 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();
})();