Temporal في الإنتاج: استبدال كائن Date في جافاسكربت، وإصلاح خطأ «اليوم نفسه»، والحد الذي لا تعبره مكوّنات الخادم
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗أبرز النقاط
Temporal هي واجهة TC39 التي تستبدل كائن Date في جافاسكربت بأنواع غير قابلة للتعديل وواعية بالمناطق الزمنية، وهي: Temporal.PlainDate وTemporal.PlainTime وTemporal.PlainDateTime وTemporal.ZonedDateTime وTemporal.Instant وTemporal.Duration.
Temporal تستبدل Date ولا تستبدل Intl. فـTemporal مسؤولة عن الحساب والمقارنة والمناطق الزمنية، بينما تبقى Intl.DateTimeFormat مسؤولة عن التنسيق المقروء للمستخدم عبر الدالة toLocaleString() المتاحة في كل كائن Temporal.
اختر نوع Temporal بناءً على معنى القيمة في المنطق التجاري: تاريخ الميلاد هو Temporal.PlainDate، والطابع الزمني في سجل التدقيق هو Temporal.Instant، واجتماع الساعة الثالثة عصرًا بتوقيت القاهرة هو Temporal.ZonedDateTime.
كائنات Temporal هي نسخ من أصناف (class instances)، لذلك لا تستطيع React تسلسلها عبر حدود مكوّنات الخادم. استدعِ .toString() داخل مكوّن الخادم، ثم .from() داخل مكوّن العميل.
كل كائن Temporal غير قابل للتعديل، والمعامل === لا يقارن قيمتَي Temporal مقارنة صحيحة أبدًا. استخدم .equals() للمساواة والدالة الساكنة compare() للترتيب.
الدعم الأصلي لـTemporal في المتصفحات وNode.js ما زال متفاوتًا في 2026، لذلك نثبّت حزمة temporal-polyfill ونكتب الواجهة النهائية من اليوم.
ما الذي تستبدله Temporal فعليًا؟
Temporal تستبدل كائن Date في جافاسكربت، وهو النوع القابل للتعديل الذي يعتمد على الميلي ثانية ويعرف منطقة زمنية واحدة، والذي لم يتغيّر جوهريًا منذ 1995. نتعامل في فريقنا مع Temporal باعتبارها أول إصلاح حقيقي وليست غلافًا إضافيًا.
لدى Date ثلاثة عيوب تزيلها Temporal تمامًا. أولًا، Date قابل للتعديل، فالسطر d.setDate(d.getDate() + 1) يغيّر كائنًا قد تكون شيفرة أخرى ما زالت تحتفظ به. ثانيًا، Date يعرف منطقتين زمنيتين فقط: التوقيت العالمي المنسق وما تُبلغ عنه الآلة المضيفة، لذلك تختلف دالة بلا خادم تعمل بتوقيت UTC عن متصفح في منطقة Africa/Cairo حول معنى «اليوم». ثالثًا، تحليل النصوص في Date غير متسق بحكم المواصفة نفسها: فالتعبير new Date('2026-08-22') يُقرأ منتصف ليل بتوقيت UTC لأنه صيغة تاريخ فقط، بينما يُقرأ new Date('2026-08-22T00:00:00') منتصف ليل محليًا لأنه تاريخ ووقت بلا إزاحة.
تحوّل Temporal هذا الفارق إلى نوع صريح. فـTemporal.PlainDate.from('2026-08-22') تاريخ تقويمي لا لحظة زمنية مرتبطة به، وTemporal.Instant.from('2026-08-22T00:00:00Z') نقطة محددة على الخط الزمني. ولأن PlainDate لا يحتوي على الخاصية epochMilliseconds، لا يمكن الخلط بين الاثنين بالخطأ.
أي نوع من Temporal يناسب كل حقل؟
اختر نوع Temporal من معنى القيمة في المنطق التجاري، لا من عمود قاعدة البيانات الذي تصادف وجودها فيه. هذا هو التعيين الافتراضي الذي نطبّقه في الأعمال الجديدة.
تاريخ الميلاد أو تاريخ استحقاق الفاتورة هو Temporal.PlainDate، ويُكتب هكذا: 2026-08-22.
ساعة افتتاح المتجر هي Temporal.PlainTime، وتُكتب هكذا: 09:00:00.
اجتماع الساعة الثالثة عصرًا بتوقيت القاهرة هو Temporal.ZonedDateTime، ويُكتب هكذا: 2026-08-22T15:00:00+03:00[Africa/Cairo].
عمود created_at أو مدخل في سجل التدقيق هو Temporal.Instant، ويُكتب هكذا: 2026-08-22T12:00:00Z.
مدة الجلسة أو عمر التخزين المؤقت هو Temporal.Duration، ويُكتب هكذا: PT2H30M.
تاريخ انتهاء البطاقة البنكية هو Temporal.PlainYearMonth، ويُكتب هكذا: 2026-08.
Temporal.Now هي نقطة الدخول إلى اللحظة الحالية عبر Temporal.Now.instant() وTemporal.Now.zonedDateTimeISO(zone) وTemporal.Now.plainDateISO(zone). وهذه الأخيرة تؤدي دور الواجهة الصحيح: طلب تاريخ اليوم يُجبر المستدعي على الإجابة عن سؤال «اليوم وفق مَن؟»، وهو السؤال ذاته الذي يسمح new Date() بتخطيه.
لماذا ينكسر فحص «هل هو اليوم نفسه؟» عبر المناطق الزمنية؟
تنكسر مقارنة اليوم نفسه لأن لحظتين لا تقعان في اليوم التقويمي نفسه إلا بالنسبة إلى منطقة زمنية محددة، بينما تختار الدالة Date.prototype.toDateString() منطقة الآلة المضيفة بصمت. وعلى دالة بلا خادم تعمل بتوقيت UTC، يكون حدث الساعة 22:30 بتوقيت القاهرة قد انتقل بالفعل إلى اليوم التالي.
نسخة Temporal تسمّي المنطقة صراحةً: حوِّل اللحظتين عبر .toZonedDateTimeISO('Africa/Cairo')، ثم اختزل كلًّا منهما إلى .toPlainDate()، ثم قارنهما بـ.equals(). صارت المنطقة الزمنية وسيطًا مرئيًا في الشيفرة بدل أن تكون خاصية خفية للآلة التي تشغّلها.
الوضوح نفسه يظهر في الحساب عبر تحوّل التوقيت الصيفي، لأن Temporal.ZonedDateTime تفصل الوحدات التقويمية عن الوحدات الدقيقة. فانطلاقًا من 2026-03-28T09:00[Europe/Berlin]، يعيد استدعاء .add({ days: 1 }) الساعة 09:00 من صباح اليوم التالي، أي بعد 23 ساعة حقيقية، بينما يعيد .add({ hours: 24 }) الساعة 10:00، أي ساعة حائط مختلفة. كلا الجوابين صحيح لسؤالين مختلفين، ولم يكن Date قادرًا على التعبير عن هذا الفارق أصلًا.
هل يمكن تمرير كائن Temporal من مكوّن خادم إلى مكوّن عميل؟
لا. ترفض React نسخ الأصناف عند الحد الفاصل بين الخادم والعميل برسالة الخطأ: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. وكل نوع في Temporal هو صنف، لذلك يجب تحويل كل قيمة Temporal إلى نص قبل أن تصبح خاصية (prop).
عمليًا يعني ذلك استدعاء due.toString() داخل مكوّن الخادم، ثم Temporal.PlainDate.from(due) داخل مكوّن العميل. وتقع الفرق في هذا الفخ تحديدًا لأن مُسلسِل React يدعم Date، فهو ضمن القائمة القصيرة من الأنواع المدمجة المسموح بها، ولذلك يكسر استبدال Date بـTemporal خصائص كانت تعمل من قبل بلا أي تحذير.
الصيغة النصية تحمل معلومات أكثر مما تستبدله. فـPlainDate.toString() تُخرج 2026-08-22، وZonedDateTime.toString() تُخرج 2026-08-22T15:00:00+03:00[Africa/Cairo]، وهي صيغة تعود عبر from() والمنطقة الزمنية سليمة. أما رقم الحقبة (epoch) داخل حمولة JSON فلا يستطيع حمل تلك المنطقة إطلاقًا.
كيف تُخزَّن قيم Temporal في Postgres؟
خزّن اللحظة في عمود timestamptz، والتاريخ التقويمي في عمود date، والمنطقة الزمنية للمستخدم بمعيار IANA في عمود text مستقل كلما كانت نية ساعة الحائط مهمة. فقاعدة Postgres لا تملك نوع عمود يحمل منطقة زمنية فعليًا رغم الاسم، لأن timestamptz يُطبّع القيمة إلى UTC عند الكتابة.
تعيد المشغّلات مثل node-postgres، وأدوات الربط المبنية عليها مثل Drizzle، كائن Date من جافاسكربت لعمود timestamptz، لذا حوّل عند الحافة عبر Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())، ثم أعد بناء ساعة حائط المستخدم عبر .toZonedDateTimeISO(row.time_zone).
تختلف الدقة عند كل محطة: Temporal.Instant يحتفظ بالنانو ثانية، وtimestamptz في Postgres يحتفظ بالميكرو ثانية، وDate في جافاسكربت يحتفظ بالميلي ثانية. لذلك فإن قيمة تنتقل من Temporal إلى Date إلى Postgres ثم تعود ليست دائمًا القيمة التي بدأت بها، فقرّبها صراحةً عبر instant.round({ smallestUnit: 'microsecond' }) حين تكون دقة الرحلة مهمة لاختبار ما.
أما ما هو مجدول في المستقبل، مثل تذكير أسبوعي الساعة التاسعة صباحًا بالتوقيت المحلي، فخزّن له PlainDateTime مع منطقة IANA بدل تخزين لحظة. فاللحظة المخزّنة تجمّد إزاحة UTC الحالية، ما يعني انزياح التذكير ساعة كاملة عند أول تغيير في قواعد التوقيت الصيفي لتلك المنطقة، وهي قواعد تتغيّر بإشعار لا يتجاوز أسابيع.
هل يحذف الفريق date-fns أو Day.js أو Luxon الآن؟
ليس في التزام واحد. تغطي Temporal ما وُجدت تلك المكتبات لتغطيته، لكن المقارنة الأمينة أضيق من القول إن Temporal تفوز.
date-fns قابلة للاستبدال في الحساب والمقارنة، لكن دوالها القابلة للتشذيب تعمل كلها على Date، لذلك تحتاج المسارات المختلطة إلى محوّلات طوال فترة الانتقال.
Day.js وMoment قابلتان للاستبدال بالكامل، لأن كلتيهما تغلّف Date وترث نموذجه في المناطق الزمنية، وتحتاج Day.js إلى إضافة timezone لتفعل ما تفعله Temporal.ZonedDateTime أصلًا.
Luxon هي الأقرب روحًا، إذ يشترك Luxon ومقترح Temporal في المؤلف نفسه وفي النموذج غير القابل للتعديل والواعي بالمناطق الزمنية، ما يجعل الانتقال إعادة تسمية في معظمه.
Intl.DateTimeFormat ينبغي الإبقاء عليها، لأن Temporal لا تنسّق للبشر عمدًا وتسلّم المهمة إلى Intl.
ترتيب الانتقال الناجح هو التحويل عند الحواف أولًا: حلّل كل نص وارد إلى نوع Temporal عند حد الواجهة البرمجية أو النموذج، وأعد التنسيق إلى نص عند حد العرض أو قاعدة البيانات، ودع منتصف التطبيق يتوقف عن ملامسة Date نهائيًا. ومخطط Zod موضع جيد لذلك التحليل عبر z.string().transform((s) => Temporal.PlainDate.from(s)) للحصول على قيمة Temporal مُتحقَّق منها برسالة خطأ حقيقية.
Temporal مقترح في المرحلة الثالثة من TC39، وقد بدأت المتصفحات بتفعيله، إذ شحنته Firefox أولًا دون راية تجريبية ثم تبعته Safari، بينما ما زال Chromium وNode.js في طور اللحاق. ولأن التغطية متفاوتة، ثبّت temporal-polyfill وهي التطبيق المدمج، أو @js-temporal/polyfill وهي التطبيق المرجعي، واستوردها من وحدة مشتركة واحدة لا من كل ميزة على حدة، وافحص كلفتها في محلل الحزم. وحين يغطي الدعم الأصلي نسبة كافية من زوّارك، يصبح حذف ذلك الاستيراد الواحد هو النصف الثاني الكامل من الانتقال.
أسئلة شائعة
س: هل Temporal بديل مباشر لـDate؟
ج: لا. تضيف Temporal فضاء أسماء عامًا منفصلًا ولا تغيّر Date إطلاقًا. تبقى الشيفرة القائمة تعمل، ويجري التحويل عبر Temporal.Instant.fromEpochMilliseconds(date.getTime()) وnew Date(instant.epochMilliseconds).
س: لماذا لا يمكن مقارنة قيمتَي Temporal بالمعامل === أو <؟
ج: قيم Temporal كائنات، لذلك يقارن === المراجع لا القيم التقويمية. استخدم a.equals(b) للمساواة، والمقارِن الساكن Temporal.PlainDate.compare(a, b) الذي يعيد -1 أو 0 أو 1 ويمكن تمريره مباشرة إلى Array.prototype.sort.
س: هل تتعامل Temporal مع التوقيت الصيفي بشكل صحيح؟
ج: نعم، عبر Temporal.ZonedDateTime التي تعتمد قاعدة بيانات المناطق الزمنية IANA. وحين يكون الوقت المحلي ملتبسًا أو غير موجود بسبب تحوّل التوقيت الصيفي، تقبل from() خيار disambiguation بقيمة 'compatible' أو 'earlier' أو 'later' أو 'reject'، فيصير السلوك قرارًا لا مصادفة.
س: هل يمكن استخدام Temporal في Node.js وداخل مكوّنات الخادم؟
ج: نعم عبر polyfill. استورد temporal-polyfill في شيفرة الخادم تمامًا كما في المتصفح، فـTemporal تعمل داخل مكوّنات الخادم ومعالجات المسارات وإجراءات الخادم، ولا يحتاج إلى نصوص سوى حد الخصائص.
س: هل نخزّن لحظة أم وقت ساعة حائط في قاعدة البيانات؟
ج: خزّن لحظة في عمود timestamptz لما وقع فعلًا مثل created_at، وخزّن PlainDateTime مع منطقة IANA لما هو مجدول في المستقبل، حتى يتحرك الحدث مع ساعة المستخدم عند تغيّر قواعد التوقيت الصيفي لتلك المنطقة.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.