بومی سازی تاریخ و اعداد در جاوا اسکریپت در سال 2020
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هفت دقیقه

احتمالا تا به امروز در برنامه‌های جاوا اسکریپتی خود از کتابخانه‌های ثالث و یا توابع ویژه‌ای برای نمایش شمسی تاریخ، نمایش فارسی اعداد، افزودن جدا کننده‌ی سه رقمی اعداد (جداکننده‌ی هزارگان)، نمایش تاریخ نسبی مانند 1 روز قبل و ... استفاده کرده‌اید. خبر خوب اینکه موتور جاوا اسکریپتی تمام مرورگرهای جدید (از فایرفاکس 29 و کروم 24 به بعد) به صورت توکار یک چنین تبدیل‌هایی را پشتیبانی می‌کنند و برای مثال برای تبدیل تاریخ میلادی به شمسی و نمایش آن، در بسیاری از موارد نیازی به کتابخانه‌ی حجیم moment.js (و یا سایر روش‌های مرسوم خانگی) نیست.


معرفی API استاندارد بومی سازی JavaScript

Internationalization یا به صورت خلاصه i18n (یعنی یک i به همراه 18 حرف و یک n)، پروسه‌ای که در آن برنامه به نحوی طراحی می‌شود تا خروجی آن قابلیت استفاده‌ی برای انواع و اقسام فرهنگ‌ها را داشته باشد. برای مثال دو متد زیر را در نظر بگیرید:
function formatDate(d)
{
  var month = d.getMonth() + 1;
  var date = d.getDate();
  var year = d.getFullYear();
  return month + "/" + date + "/" + year;
}

function formatMoney(amount)
{
  return "$" + amount.toFixed(2);
}
آیا در همه جای دنیا، تاریخ به صورت ماه، روز و سال نمایش داده می‌شود؛ آن هم به صورت میلادی؟ و یا آیا خروجی فرمت شده‌ی یک مقدار پولی، همیشه با دلار شروع می‌شود و نمایش آن نیز با اعداد انگلیسی است؟
پیشتر جاوا اسکریپت برای مدیریت یک چنین مواردی (i18n-aware formatting) از متد toLocaleString استفاده می‌کرد (و هنوز هم برای پشتیبانی از برنامه‌های قدیمی، از API عمومی آن حذف نشده‌است) و خروجی آن از هر مرورگر و پیاده سازی خاصی، به مرورگر دیگری می‌تواند متفاوت باشد؛ حتی اگر جزئیات دقیقی هم درخواست شود. برای رفع این مشکل، استاندارد ECMAScript Internationalization API ارائه شد تا قابلیت‌های توکار i18n جاوا اسکریپت را بهبود بخشیده و همچنین یک‌دست کند. توسط آن امکان انتخاب یک یا چند منطقه‌ی خاص و سپس فرمت کردن تاریخ، اعداد و یا حتی مرتب سازی واژه‌ها و عبارات با معرفی collations، میسر می‌باشد. در اینجا حتی امکان سفارشی سازی این فرمت‌ها نیز پیش‌بینی شده‌است.


معرفی اینترفیس Intl

i18n API در یک شیء سراسری به نام Intl قابل دسترسی است و تعدادی از سازنده‌های آن Intl.Collator ،Intl.DateTimeFormat و Intl.NumberFormat نام دارند؛ مانند:
const result =  new Intl.NumberFormat("fa").format(123456)
برای کار با این شیء، نیازی به import هیچ ماژول و یا کتابخانه‌ای نیست و جزئی از جاوا اسکریپت استاندارد می‌باشد. به همین جهت کار با آن حجمی را به برنامه‌ی شما اضافه نخواهد کرد.

تمام این سازنده‌ها می‌توانند با یک فرهنگ و یا آرایه‌ا‌‌ی از فرهنگ‌های مدنظر شروع شوند:
const portugueseTime =  new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options);
در مثال اول فرهنگ فارسی و در مثال دوم فرهنگ پرتغالی که در برزیل و پرتغال مورد استفاده‌است، ذکر شده‌اند.
پارامتر اختیاری دوم آن‌ها نیز تنظیماتی است که جهت سفارشی سازی این بومی سازی می‌توان تعریف کرد.


نمایش شمسی تاریخ میلادی توسط i18n API

پس از معرفی i18n API، اکنون می‌خواهیم در طی مثال‌هایی، تمام کتابخانه‌های ثالث تبدیل تاریخ میلادی به شمسی را کنار گذاشته و با استفاده از جاوا اسکریپت استاندارد، این تبدیل را انجام دهیم. پارامتر دوم سازنده‌ی new Intl.DateTimeFormat که تنظیمات آن‌را مشخص می‌کند، می‌تواند به همراه ترکیبی از موارد زیر باشد که مقادیر مجاز برای آن‌ها را نیز مشاهده می‌کنید:
{
  weekday: 'narrow' | 'short' | 'long',
  era: 'narrow' | 'short' | 'long',
  year: 'numeric' | '2-digit',
  month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long',
  day: 'numeric' | '2-digit',
  hour: 'numeric' | '2-digit',
  minute: 'numeric' | '2-digit',
  second: 'numeric' | '2-digit',
  timeZoneName: 'short' | 'long',
// Time zone to express it in
  timeZone: 'Asia/Shanghai',
  // Force 12-hour or 24-hour
  hour12: true | false,
// Rarely-used options
  hourCycle: 'h11' | 'h12' | 'h23' | 'h24',
  formatMatcher: 'basic' | 'best fit'
}
برای نمونه، ذکر Intl.DateTimeFormat بدون هیچ تنظیمی و فقط با تعیین فرهنگ فارسی:
var dateFormat = new Intl.DateTimeFormat("fa");
console.log(dateFormat.format(Date.now()));
خروجی «۱۳۹۸/۱۲/۱» را نمایش می‌دهد.


نمایش تاریخ شمسی با فرمت «۱۳۹۸ اسفند ۱, پنجشنبه»

برای تبدیل تاریخ میلادی به شمسی می‌توان از سازنده‌ی new Intl.DateTimeFormat با فرهنگ fa استفاده کرد. در اینجا ذکر مقدار long برای نام روز هفته، سبب درج نام روز می‌شود. نمایش سال به صورت عددی تنظیم شده‌است، ماه را به صورت بلند و نام کامل نمایش می‌دهد و مقدار روز را به صورت عددی درج می‌کند. این اعداد نیز فارسی هستند:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const faDate = new Intl.DateTimeFormat("fa", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
}).format(date);
console.log(faDate);
که برای نمونه سبب درج خروجی «۱۳۹۸ اسفند ۱, پنجشنبه» در کنسول توسعه دهندگان مرورگر خواهد شد.

اگر فقط نیاز به نمایش «۱ اسفند ۱۳۹۸» بود، می‌توان از تنظیمات زیر که در آن ماه، روز و سال ذکر شده‌اند و در آن، ماه به صورت کامل و بلند نمایش داده می‌شود، استفاده کرد:
const isoString = new Date().toISOString();
const date = new Date(isoString);
console.log(
  new Intl.DateTimeFormat("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  }).format(date)
);

یک نکته: همین خروجی را با متد قدیمی toLocaleDateString نیز می‌توان به دست آورد؛ اما روش توصیه شده برای برنامه‌های جدید، همان استفاده از new Intl است.
console.log(
  new Date().toLocaleDateString("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  })
);

نمایش تاریخ شمسی با فرمت «۹۸/۱۲/۱،‏ ۶:۳۰»

برای اینکار پس از ذکر فرهنگ fa، تمام اجزای تاریخ را به صورت عددی مشخص می‌کنیم و سال را نیز دو رقمی نمایش خواهیم داد:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const fmt = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});
console.log(fmt.format(date));
در این حالت اگر نیاز بود حتما اعداد ماه و روز، دو رقمی باشند، می‌توان تنظیم 2-digit را صریحا ذکر کرد:
const faDateTime = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short"
}).format;
const now = Date.now();
console.log(faDateTime(now));
با خروجی «۹۸/۱۲/۰۱،‏ ۱۲:۵۹ (‎+۳:۳۰ گرینویچ)»

و یا اگر «۱ اسفند ۱۳۹۸،‏ ۰۹:۲۹ (UTC)» مدنظر بود، می‌توان ماه را به long تنظیم کرد و مقدار timeZone را صریحا ذکر نمود (که البته ذکر تنظیمات timeZone اختیاری است):
const faTime = new Intl.DateTimeFormat("fa", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short",
  timeZone: "UTC"
});
console.log(faTime.format(now));

نمایش تاریخ‌های نسبی مانند «1 روز بعد»
برای نمایش تاریخ‌های نسبی، می‌توان از شیء new Intl.RelativeTimeFormat استفاده کرد:
const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit", // other values: "lookup"
  numeric: "always", // other values: "auto"
  style: "long", // other values: "short" or "narrow"
});
console.log(rtf.format(-1, "day"));
console.log(rtf.format(1, "day"));
با خروجی‌های «۱ روز پیش» و «۱ روز بعد»


نمایش اعداد فارسی توسط i18n API

احتمالا برای تبدیل اعداد انگلیسی به فارسی و نمایش آن‌ها، متدهایی را برای replace حروف و اعداد طراحی کرده‌اید. به کمک شیء استاندارد Intl.NumberFormat دیگر نیازی به آن‌ها نخواهید داشت!
خروجی شیء Intl.NumberFormat به همراه ذکر فرهنگ فارسی و هیچ تنظیم اضافه‌تری
console.log(new Intl.NumberFormat("fa").format(123456));
به صورت «۱۲۳٬۴۵۶» است که هم اعداد آن فارسی شده‌اند و هم به همراه جداکننده‌ی هزارگان خودکار است.

اگر می‌خواهید این جداکننده‌ی هزارگان نمایش داده نشود، نیاز است تنظیمات آن‌را به همراه useGrouping: false، به صورت زیر ذکر کرد:
console.log(
   new Intl.NumberFormat("fa", { useGrouping: false }).format(123456)
);

این شیء یک مقدار غیرعددی را
console.log(new Intl.NumberFormat("fa").format("تست"));
به صورت «ناعدد» نمایش می‌دهد.

و یا برای نمایش واحد پولی، می‌توان حالت نمایش را به currency و نوع currency را به IRR که ریال است، تنظیم کرد:
const gasPrice = new Intl.NumberFormat("fa", {
  style: "currency",
  currency: "IRR",
  minimumFractionDigits: 3
});
console.log(gasPrice.format(5.2597));
با این خروجی: «‎ریال ۵٫۲۶۰» که در اینجا امکان تنظیم نمایش تعداد اعشار آن نیز میسر است.

برای نمایش درصد پس از اعداد می‌توان از تنظیم زیر استفاده کرد:
const faPercent = new Intl.NumberFormat("fa", {
  style: "percent",
  minimumFractionDigits: 2
}).format;
console.log(faPercent(0.438));
که خروجی «۴۳٫۸۰٪» را نمایش می‌دهد.

و یا برای نمایش ممیز به همراه تنظیم دقت آن داریم:
const persianDecimal = new Intl.NumberFormat("fa", {
  minimumIntegerDigits: 2,
  maximumFractionDigits: 2
});
console.log(persianDecimal.format(3.1416));
با این خروجی: «۰۳٫۱۴»
  • #
    ‫۴ سال و ۶ ماه قبل، جمعه ۲ اسفند ۱۳۹۸، ساعت ۰۴:۱۸
    برای فارسی کردن تاریخ‌های نسبی با این API چه راهکاری هست؟
    • #
      ‫۴ سال و ۶ ماه قبل، جمعه ۲ اسفند ۱۳۹۸، ساعت ۰۵:۵۳
      همان new Intl.RelativeTimeFormat مطلب فوق:
      function timeDiff(current, prev) {
        const millisecond = current - prev;
        const second = millisecond / 1000;
        const minute = second / 60;
        const hour = minute / 60;
        const day = hour / 24;
        const week = day / 7;
        const month = week / 4.3;
        const year = month / 12;
        const quarter = year / 4;
        const unitValues = {
          millisecond,
          second,
          minute,
          hour,
          day,
          week,
          month,
          year,
          quarter
        };
        return function diffValueByUnit(unitKey) {
          return unitValues[unitKey];
        };
      }
      
      
      const from = new Date();
      from.setDate(from.getDate() - 2);
      console.log(from);
      const to = new Date(Date.now());
      console.log(to);
      const diff = timeDiff(from, to);
      const result = parseFloat(Math.round(diff("hour")));
      console.log(result);
      const rtf = new Intl.RelativeTimeFormat("fa");
      console.log(rtf.format(result, "hour"));
      با این خروجی:
      > Wed Feb 19 2020 02:24:36 GMT+0330 (Iran Standard Time)
      > Fri Feb 21 2020 02:24:36 GMT+0330 (Iran Standard Time)
      > -48
      > "۴۸ ساعت پیش"
  • #
    ‫۴ سال و ۶ ماه قبل، یکشنبه ۴ اسفند ۱۳۹۸، ساعت ۱۶:۳۶
    سلام؛ برای تبدیل معکوس (از شمسی به میلادی یا DateTime مربوط به JavaScript) هم می‌توان از این API استفاده کرد؟
  • #
    ‫۴ سال و ۶ ماه قبل، یکشنبه ۴ اسفند ۱۳۹۸، ساعت ۲۳:۴۵
    یک نکته‌ی تکمیلی: روش سفارشی سازی خروجی نهایی تاریخ

    همانطور که در متن نیز عنوان شد، خروجی تبدیل زیر
    const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
    const faDate = new Intl.DateTimeFormat("fa", {
       weekday: "long",
       year: "numeric",
       month: "long",
       day: "numeric"
    }).format(date);
    console.log(faDate);
    معادل «۱۳۹۸ اسفند ۱, پنجشنبه» است که ... آنچنان مطلوب نیست. در یک چنین حالتی برای دسترسی به اجزای خروجی این فرمت کننده‌، می‌توان از متد استاندارد formatToParts استفاده کرد:
    const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
    const faDateParts = new Intl.DateTimeFormat("fa", {
      weekday: "long",
      year: "numeric",
      month: "long",
      day: "numeric"
    }).formatToParts(date);
    console.log(faDateParts);
    که این خروجی را باز می‌گرداند:
    [​
      { type: "year", value: "۱۳۹۸" },
     ​ { type: "literal", value: " " },
     ​ { type: "month", value: "اسفند" },
     ​ { type: "literal", value: " " },
     ​ { type: "day", value: "۱" },
     ​ { type: "literal", value: ", " },
     ​ { type: "weekday", value: "پنجشنبه" }​
    ]
    که آرایه‌ای است از اشیاء type و value. اکنون با استفاده از قطعه کد زیر، این آرایه را تبدیل به یک شیء می‌کنیم که نام هر خاصیت آن، یک type است و مقدار آن value شیء متناظر:
    const { year, literal, month, day, weekday } = Object.fromEntries(
      new Intl.DateTimeFormat("fa", {
        weekday: "long",
        year: "numeric",
        month: "long",
        day: "numeric"
      })
        .formatToParts(date)
        .map(item => [item.type, item.value])
    );
    سپس با استفاده از Objects Destructuring این شیء را به چند متغیر نسبت داده‌ایم. اکنون با دسترسی به مقادیر سال و ماه و غیره، هر طور که نیاز بود می‌توان آن‌ها را فرمت کرد. برای مثال:
    const faDate = `${weekday}${literal}${day} ${month} ${year}`;
    console.log(faDate);
    با این خروجی:
    «پنجشنبه, ۱ اسفند ۱۳۹۸»
  • #
    ‫۴ سال و ۵ ماه قبل، یکشنبه ۱۰ فروردین ۱۳۹۹، ساعت ۱۹:۰۵
    با سلام؛ خروجی تاریخ رو گرفتم اما درصورتی که اعداد تاریخ خروجی با کاراکتر عددی انگلیسی باشد چه روشی را پیشنهاد می‌کنید. آیا باید از تابع NumberFormat استفاده کرد. چگونه؟
    • #
      ‫۴ سال و ۵ ماه قبل، یکشنبه ۱۰ فروردین ۱۳۹۹، ساعت ۲۰:۰۰
      می‌توان نوع عدد و تقویم را به صورت زیر تنظیم کرد:
      const faWithLatinNumbersFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian-nu-latn');
      const today = faWithLatinNumbersFormatter.format(Date.now());
      console.log(today); // = 1399/1/10
      مقادیر ca و nu این‌ها می‌توانند باشند:
      nu's possible values can be:
      arab, arabext, bali, beng, deva, fullwide, gujr, guru, hanidec, khmr, knda, laoo, latn, limb, mlym, mong, mymr, orya, tamldec, telu, thai, tibt.
      
      ca's possible values include:
      buddhist, chinese, coptic, ethiopia, ethiopic, gregory, hebrew, indian, islamic, iso8601, japanese, persian, roc.
  • #
    ‫۴ سال و ۵ ماه قبل، سه‌شنبه ۱۲ فروردین ۱۳۹۹، ساعت ۲۰:۳۱
    با سلام وتشکر؛ آیا امکان تبدیل تاریخ شمسی به میلادی وجود دارد؟ که البته تاریخ شمسی حاضر از نوع string می‌باشد برای نمونه "1399/01/11"؟ ممنون