واجهة برمجة التطبيقات Intl.RelativeTimeFormat

تنسيق سلاسل الوقت النسبي في جافا سكريبت مع دعم كامل للتدويل

مقدمة

عرض الطوابع الزمنية النسبية مثل "منذ 3 ساعات" أو "خلال يومين" هو متطلب شائع في تطبيقات الويب. تحتاج خلاصات وسائل التواصل الاجتماعي، وأقسام التعليقات، وأنظمة الإشعارات، وسجلات النشاط جميعها إلى عرض الوقت بطريقة مفهومة للإنسان تتحدث تلقائيًا مع تقادم المحتوى.

بناء هذه الوظيفة من الصفر يقدم تحديات. تحتاج إلى حساب الفروق الزمنية، واختيار الوحدات المناسبة، والتعامل مع قواعد الجمع عبر اللغات، والحفاظ على الترجمات لكل لغة مدعومة. هذا التعقيد يفسر سبب لجوء المطورين تقليديًا إلى مكتبات مثل Moment.js، مما يضيف حجمًا كبيرًا للحزمة لما يبدو مجرد تنسيق بسيط.

توفر واجهة برمجة التطبيقات Intl.RelativeTimeFormat حلاً أصليًا. فهي تقوم بتنسيق سلاسل الوقت النسبية مع دعم كامل للتدويل، والتعامل مع قواعد الجمع والاتفاقيات الثقافية تلقائيًا. تعمل واجهة برمجة التطبيقات عبر جميع المتصفحات الرئيسية بتغطية عالمية تبلغ 95%، مما يلغي الحاجة إلى التبعيات الخارجية مع إنتاج مخرجات تبدو طبيعية بعشرات اللغات.

الاستخدام الأساسي

يقوم منشئ Intl.RelativeTimeFormat بإنشاء نسخة منسق تحول القيم الرقمية ووحدات الوقت إلى سلاسل نصية محلية.

const rtf = new Intl.RelativeTimeFormat('en');

console.log(rtf.format(-1, 'day'));
// "1 day ago"

console.log(rtf.format(2, 'hour'));
// "in 2 hours"

console.log(rtf.format(-3, 'month'));
// "3 months ago"

تأخذ طريقة format() معلمتين:

  • value: رقم يشير إلى مقدار الوقت
  • unit: سلسلة نصية تحدد وحدة الوقت

تشير القيم السالبة إلى الأوقات الماضية، وتشير القيم الموجبة إلى الأوقات المستقبلية. تتعامل واجهة برمجة التطبيقات تلقائيًا مع صيغ الجمع، وتنتج "منذ يوم واحد" أو "منذ يومين" بناءً على القيمة.

وحدات الوقت المدعومة

تدعم واجهة برمجة التطبيقات ثماني وحدات زمنية، وكل منها يقبل الصيغ المفردة والجمع:

const rtf = new Intl.RelativeTimeFormat('en');

// هذه تنتج نفس المخرجات
console.log(rtf.format(-5, 'second'));
// "5 seconds ago"

console.log(rtf.format(-5, 'seconds'));
// "5 seconds ago"

الوحدات المتاحة من الأصغر إلى الأكبر:

  • second أو seconds
  • minute أو minutes
  • hour أو hours
  • day أو days
  • week أو weeks
  • month أو months
  • quarter أو quarters
  • year أو years

تعتبر وحدة الربع مفيدة في تطبيقات الأعمال التي تتتبع الفترات المالية، بينما تغطي الوحدات الأخرى احتياجات تنسيق الوقت النسبي النموذجية.

مخرجات اللغة الطبيعية

يتحكم خيار numeric فيما إذا كان المنسق يستخدم القيم الرقمية أو بدائل اللغة الطبيعية.

const rtfNumeric = new Intl.RelativeTimeFormat('en', {
  numeric: 'always'
});

console.log(rtfNumeric.format(-1, 'day'));
// "1 day ago"

console.log(rtfNumeric.format(0, 'day'));
// "in 0 days"

console.log(rtfNumeric.format(1, 'day'));
// "in 1 day"

ضبط numeric على auto ينتج عبارات أكثر اصطلاحية للقيم الشائعة:

const rtfAuto = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
});

console.log(rtfAuto.format(-1, 'day'));
// "yesterday"

console.log(rtfAuto.format(0, 'day'));
// "today"

console.log(rtfAuto.format(1, 'day'));
// "tomorrow"

تنشئ مخرجات اللغة الطبيعية هذه واجهات أكثر محادثة. يعمل خيار auto عبر جميع وحدات الوقت، على الرغم من أن التأثير أكثر وضوحًا مع الأيام. تمتلك اللغات الأخرى بدائلها الاصطلاحية الخاصة التي تتعامل معها واجهة البرمجة تلقائيًا.

أنماط التنسيق

يعدل خيار style إطالة المخرجات لسياقات واجهة مختلفة:

const rtfLong = new Intl.RelativeTimeFormat('en', {
  style: 'long'
});

console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

const rtfShort = new Intl.RelativeTimeFormat('en', {
  style: 'short'
});

console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

const rtfNarrow = new Intl.RelativeTimeFormat('en', {
  style: 'narrow'
});

console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

استخدم نمط long (الافتراضي) للواجهات القياسية حيث تكون القراءة هي الأهم. استخدم نمط short للتخطيطات المقيدة بالمساحة مثل واجهات الجوال أو جداول البيانات. استخدم نمط narrow للعروض المضغوطة للغاية حيث تهم كل حرف.

حساب الفروق الزمنية

تقوم واجهة برمجة التطبيقات Intl.RelativeTimeFormat بتنسيق القيم ولكنها لا تحسبها. يجب عليك حساب الفروق الزمنية واختيار الوحدات المناسبة بنفسك. يمنحك هذا الفصل بين المسؤوليات التحكم في منطق الحساب مع تفويض تعقيد التنسيق إلى واجهة برمجة التطبيقات.

حساب فرق الوقت الأساسي

لوحدة زمنية محددة، احسب الفرق بين تاريخين:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

function formatDaysAgo(date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));

  return rtf.format(diffInDays, 'day');
}

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

console.log(formatDaysAgo(yesterday));
// "yesterday"

const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);

console.log(formatDaysAgo(nextWeek));
// "in 7 days"

يعمل هذا النهج عندما تعرف أي وحدة منطقية لحالة الاستخدام الخاصة بك. قد تستخدم طوابع الوقت للتعليقات دائمًا الساعات أو الأيام، بينما قد يركز جدولة الأحداث على الأيام أو الأسابيع.

اختيار الوحدة التلقائي

لتنسيق الوقت النسبي للأغراض العامة، اختر الوحدة الأنسب بناءً على حجم الفرق الزمني:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

const units = {
  year: 24 * 60 * 60 * 1000 * 365,
  month: 24 * 60 * 60 * 1000 * 365 / 12,
  week: 24 * 60 * 60 * 1000 * 7,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
};

function formatRelativeTime(date) {
  const now = new Date();
  const diffInMs = date - now;
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatRelativeTime(fiveMinutesAgo));
// "5 minutes ago"

const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(threeDaysAgo));
// "3 days ago"

يتكرر هذا التنفيذ عبر الوحدات من الأكبر إلى الأصغر، ويختار الوحدة الأولى حيث يتجاوز الفرق الزمني قيمة الوحدة بالميلي ثانية. يضمن الرجوع إلى الثواني أن الدالة تعيد دائمًا نتيجة.

تستخدم عتبات الوحدة قيمًا تقريبية. يتم حساب الشهور كـ 1/12 من السنة بدلاً من حساب أطوال الشهور المتغيرة. يعمل هذا التقريب بشكل جيد لعرض الوقت النسبي حيث تكون القابلية للقراءة أكثر أهمية من الدقة.

دعم التدويل

يحترم المنسق الاصطلاحات الخاصة باللغة المحلية لعرض الوقت النسبي. تمتلك اللغات المختلفة قواعد جمع مختلفة، وترتيبات كلمات مختلفة، وتعبيرات اصطلاحية مختلفة.

const rtfEnglish = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
});

console.log(rtfEnglish.format(-1, 'day'));
// "yesterday"

const rtfSpanish = new Intl.RelativeTimeFormat('es', {
  numeric: 'auto'
});

console.log(rtfSpanish.format(-1, 'day'));
// "ayer"

const rtfJapanese = new Intl.RelativeTimeFormat('ja', {
  numeric: 'auto'
});

console.log(rtfJapanese.format(-1, 'day'));
// "昨日"

تختلف قواعد الجمع بشكل كبير بين اللغات. تميز اللغة الإنجليزية بين المفرد والجمع (1 day مقابل 2 days). تحتوي اللغة العربية على ستة أشكال للجمع اعتمادًا على العدد. تستخدم اللغة اليابانية نفس الصيغة بغض النظر عن الكمية. تتعامل واجهة البرمجة مع هذه التعقيدات تلقائيًا.

const rtfArabic = new Intl.RelativeTimeFormat('ar');

console.log(rtfArabic.format(-1, 'day'));
// "قبل يوم واحد"

console.log(rtfArabic.format(-2, 'day'));
// "قبل يومين"

console.log(rtfArabic.format(-3, 'day'));
// "قبل 3 أيام"

console.log(rtfArabic.format(-11, 'day'));
// "قبل 11 يومًا"

يتعامل المنسق أيضًا مع اتجاه النص للغات التي تُكتب من اليمين إلى اليسار ويطبق اصطلاحات التنسيق المناسبة ثقافيًا. تلغي هذه الترجمة التلقائية الحاجة إلى الاحتفاظ بملفات الترجمة أو تنفيذ منطق جمع مخصص.

التنسيق المتقدم باستخدام formatToParts

تُرجع طريقة formatToParts() السلسلة المنسقة كمصفوفة من الكائنات، مما يسمح بتخصيص نمط أو معالجة المكونات الفردية.

const rtf = new Intl.RelativeTimeFormat('en');

const parts = rtf.formatToParts(-5, 'second');

console.log(parts);
// [
//   { type: 'integer', value: '5', unit: 'second' },
//   { type: 'literal', value: ' seconds ago' }
// ]

يحتوي كل كائن جزئي على:

  • type: إما integer للقيم الرقمية أو literal للنص
  • value: المحتوى النصي لهذا الجزء
  • unit: وحدة الوقت (موجودة في الأجزاء الصحيحة)

تتيح هذه البنية العرض المخصص حيث قد ترغب في تنسيق الأرقام بشكل مختلف عن النص، أو استخراج مكونات محددة للعرض:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

function formatWithStyledNumber(value, unit) {
  const parts = rtf.formatToParts(value, unit);

  return parts.map(part => {
    if (part.type === 'integer') {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  }).join('');
}

console.log(formatWithStyledNumber(-5, 'hour'));
// "<strong>5</strong> hours ago"

عند استخدام numeric: 'auto' مع القيم التي لها بدائل لغوية طبيعية، تُرجع formatToParts() جزءًا حرفيًا واحدًا:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

const parts = rtf.formatToParts(-1, 'day');

console.log(parts);
// [
//   { type: 'literal', value: 'yesterday' }
// ]

يتيح لك هذا السلوك اكتشاف متى يتم استخدام اللغة الطبيعية مقابل التنسيق الرقمي، مما يسمح لك بتطبيق أنماط أو سلوكيات مختلفة بناءً على نوع الإخراج.

تحسين الأداء

إنشاء نسخ من Intl.RelativeTimeFormat يتضمن تحميل بيانات اللغة وتهيئة قواعد التنسيق. هذه العملية مكلفة بما يكفي لتجنب تكرارها دون ضرورة.

تخزين نسخ المنسق مؤقتًا

قم بإنشاء المنسقات مرة واحدة وأعد استخدامها:

const formatterCache = new Map();

function getFormatter(locale, options = {}) {
  const cacheKey = `${locale}-${JSON.stringify(options)}`;

  if (!formatterCache.has(cacheKey)) {
    formatterCache.set(
      cacheKey,
      new Intl.RelativeTimeFormat(locale, options)
    );
  }

  return formatterCache.get(cacheKey);
}

// Reuse cached formatters
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "yesterday"

تصبح استراتيجية التخزين المؤقت هذه مهمة عند تنسيق العديد من الطوابع الزمنية، مثل عرض موجزات النشاط أو سلاسل التعليقات.

تقليل النفقات العامة للحساب

قم بتخزين الطوابع الزمنية بدلاً من حساب الأوقات النسبية بشكل متكرر:

// Store the creation date
const comment = {
  text: "Great article!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// Calculate relative time only when rendering
function renderComment(comment, locale) {
  const rtf = getFormatter(locale, { numeric: 'auto' });
  const units = {
    day: 24 * 60 * 60 * 1000,
    hour: 60 * 60 * 1000,
    minute: 60 * 1000,
    second: 1000
  };

  const diffInMs = comment.createdAt - new Date();
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

يفصل هذا النهج تخزين البيانات عن العرض، مما يتيح لك إعادة حساب الأوقات النسبية عند تغيير لغة المستخدم أو عند تحديث العرض دون تعديل البيانات الأساسية.

التنفيذ العملي

يؤدي الجمع بين منطق الحساب والتنسيق إلى وظيفة مساعدة قابلة لإعادة الاستخدام مناسبة لتطبيقات الإنتاج:

class RelativeTimeFormatter {
  constructor(locale = 'en', options = { numeric: 'auto' }) {
    this.formatter = new Intl.RelativeTimeFormat(locale, options);

    this.units = [
      { name: 'year', ms: 24 * 60 * 60 * 1000 * 365 },
      { name: 'month', ms: 24 * 60 * 60 * 1000 * 365 / 12 },
      { name: 'week', ms: 24 * 60 * 60 * 1000 * 7 },
      { name: 'day', ms: 24 * 60 * 60 * 1000 },
      { name: 'hour', ms: 60 * 60 * 1000 },
      { name: 'minute', ms: 60 * 1000 },
      { name: 'second', ms: 1000 }
    ];
  }

  format(date) {
    const now = new Date();
    const diffInMs = date - now;
    const absDiff = Math.abs(diffInMs);

    for (const unit of this.units) {
      if (absDiff >= unit.ms || unit.name === 'second') {
        const value = Math.round(diffInMs / unit.ms);
        return this.formatter.format(value, unit.name);
      }
    }
  }
}

// Usage
const formatter = new RelativeTimeFormatter('en');

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatter.format(fiveMinutesAgo));
// "5 minutes ago"

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatter.format(tomorrow));
// "tomorrow"

تغلف هذه الفئة كلاً من المنسق ومنطق اختيار الوحدة، مما يوفر واجهة نظيفة تقبل كائنات Date وتعيد سلاسل منسقة.

التكامل مع أطر العمل

في تطبيقات React، قم بإنشاء المنسق مرة واحدة ومرره عبر السياق أو الخصائص:

import { createContext, useContext } from 'react';

const RelativeTimeContext = createContext(null);

export function RelativeTimeProvider({ locale, children }) {
  const formatter = new RelativeTimeFormatter(locale);

  return (
    <RelativeTimeContext.Provider value={formatter}>
      {children}
    </RelativeTimeContext.Provider>
  );
}

export function useRelativeTime() {
  const formatter = useContext(RelativeTimeContext);
  if (!formatter) {
    throw new Error('useRelativeTime must be used within RelativeTimeProvider');
  }
  return formatter;
}

// Component usage
function CommentTimestamp({ date }) {
  const formatter = useRelativeTime();
  return <time>{formatter.format(date)}</time>;
}

يضمن هذا النمط إنشاء المنسقات مرة واحدة لكل لغة ومشاركتها عبر جميع المكونات التي تحتاج إلى تنسيق الوقت النسبي.

دعم المتصفحات

تعمل واجهة Intl.RelativeTimeFormat في جميع المتصفحات الحديثة بتغطية عالمية تصل إلى 95%:

  • Chrome 71+
  • Firefox 65+
  • Safari 14+
  • Edge 79+

لا يدعم Internet Explorer هذه الواجهة البرمجية. بالنسبة للتطبيقات التي تتطلب دعم IE، تتوفر البدائل (polyfills)، على الرغم من أن التنفيذ الأصلي يوفر أداءً أفضل وأحجام حزم أصغر.

متى تستخدم هذه الواجهة البرمجية

تعمل واجهة Intl.RelativeTimeFormat بشكل أفضل من أجل:

  • عرض عمر المحتوى في التغذيات والجداول الزمنية
  • إظهار طوابع زمنية للتعليقات أو المنشورات
  • تنسيق جدولة الأحداث بالنسبة للوقت الحالي
  • بناء أنظمة إشعارات بطوابع زمنية نسبية
  • إنشاء سجلات نشاط بأوقات مقروءة للإنسان

الواجهة البرمجية غير مناسبة لـ:

  • تنسيق التاريخ والوقت المطلق (استخدم Intl.DateTimeFormat)
  • تتبع الوقت الدقيق الذي يتطلب دقة بالميلي ثانية
  • عدادات تنازلية تتحدث كل ثانية
  • حسابات التاريخ أو التقويم

بالنسبة للتطبيقات التي تتطلب عرض كل من الوقت النسبي والمطلق، قم بدمج Intl.RelativeTimeFormat مع Intl.DateTimeFormat. أظهر الأوقات النسبية للمحتوى الحديث وانتقل إلى التواريخ المطلقة للمحتوى الأقدم:

function formatTimestamp(date, locale = 'en') {
  const now = new Date();
  const diffInMs = Math.abs(date - now);
  const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;

  if (diffInMs < sevenDaysInMs) {
    const rtf = new RelativeTimeFormatter(locale);
    return rtf.format(date);
  } else {
    const dtf = new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    });
    return dtf.format(date);
  }
}

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(formatTimestamp(yesterday));
// "yesterday"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "Sep 14, 2025"

يوفر هذا النهج الهجين فوائد اللغة الطبيعية للأوقات النسبية للمحتوى الحديث مع الحفاظ على الوضوح للطوابع الزمنية الأقدم.