告别时间显示难题:用 Intl.DateTimeFormat 打造全球通用的日期界面

72 阅读7分钟

大家好,我是CC,在这里欢迎大家的到来~

开场

书接上文,Intl 下的 Segmenter 对象可以实现对文本的分割,Collator 对象可以处理字符串的比较,NumberFormat 对象可以处理数字格式化,除此之外,还有日期格式化等其他功能。

这篇文章先来看看日期格式化,现在来理论加实践一下。

日期格式化

Intl.DateTimeFormat 使日期和时间在特定的语言环境下格式化。

配置项

为了方便阅读,属性列表根据用途划分为多个部分,包括区域选项、样式选项、数字选项和其他选项。

区域选项

  • localeMatcher
    • 使用的区域匹配算法,可能的值包括:
    • 默认值为 best fit,还有 lookup
  • calendar
    • 使用的日历,像 chinese、gregory、persian 等等
    • 默认值取决于 locale 设置
  • hour12
    • 是否使用 12 小时制,可能的值包括:true 和 false,默认值取决于 locale 设置
    • false 是设置 hourCycle 为 h23
    • 同时存在 hc 语言环境扩展标签和 hourCycle 选项时 hour12 会覆盖其中任何一个或两个选项
  • hourCycle
    • 使用的小时制,可能的值包括:
    • h11
    • h12
    • h23
    • h24
    • 默认值取决于 locale 和 hour12 的设置
  • timeZone
    • 使用的时区,任何 IANA 时区名称,包括“UTC”、“America/New_York”和“Etc/GMT+8”
    • 还有像“+01:00”、“-2359”和“+23”之类的偏移
    • 默认值为运行时的时区,即 Date.prototype.toString() 使用的时区。

日期时间组件选项

  • weekday
    • 表示星期几,可能的值包括:
    • long,像 Thursday
    • short,像 Thu
    • narrow,像 T,但是部分地区两天可能有两个相同的格式,像 Tuesday 的 narrow 格式也是 T
  • era
    • 表示世纪,可能的值包括:
    • long,像 Anno Domini(公元)
    • short,像 AD
    • narrow,像 A
  • year
    • 表示年份,可能的值包括:
    • numeric
    • 2-digit
  • month
    • 表示月份,可能的值包括:
    • numeric,像 3
    • 2-digit,像 03
    • long,像 March
    • short,像 Mar
    • narrow,像 M,但是部分地区两月可能有两个相同的格式,像 May 的 narrow 格式也是 M
  • day
    • 表示日期,可能的值包括:
    • numeric
    • 2-digit
  • dayPeriod
    • 表示日期时间段,"in the morning", "am", "noon", "n",可能的值包括:
    • long,像 Anno Domini(公元)
    • short,像 AD
    • narrow,像 A
    • 仅在 12 小时制(hourCycle 为 h12 或 h11 时生效)
  • hour
    • 表示小时,可能的值包括:
    • numeric
    • 2-digit
  • minute
    • 表示分钟,可能的值包括:
    • numeric
    • 2-digit
  • second
    • 表示秒,可能的值包括:
    • numeric
    • 2-digit
  • fractionalSecondDigits
    • 表示秒的小数部分的数字位数,超出部分将被阶段
    • 取值范围是 1~3
  • timeZoneName
    • 表示本地时区名称,,可能的值包括:
    • long,长本地化形式,像 Pacific Standard Time、Nordamerikanische Westkusten-Normalzeit
    • short,短本地化形式,像 PST、GMT-8
    • shortOffset,短本地化 GMT 格式,像 GMT-8
    • longOffset,长本地化 GMT 格式,像 GMT-08:00
    • shortGeneric,短的通用非地点格式化,像 PT、Los Angeles Zeit
    • longGeneric,长的通用非地点格式化,像 Pacific Time、Nordamerikanische Westkusten

日期时间组件默认值

如果指定了任何日期时间组件选项,则 dateStyle 和 timeStyle 必须为 undefined。如果所有日期时间组件选项以及 dateStyle/timeStyle 都未定义,则会设置日期时间组件的一些默认选项,这些选项取决于调用格式化方法时使用的对象:

格式化 Temporal.PlainDate 和 Date 时,年、月和日默认为“numeric”。

格式化 Temporal.PlainTime 时,小时、分钟和秒默认为“numeric”。

格式化 Temporal.PlainYearMonth 时,年和月默认为“numeric”。

格式化 Temporal.PlainMonthDay 时,月份和日期默认为“数字”。

格式化 Temporal.PlainDateTime 和 Temporal.Instant 时,年、月、日、小时、分钟和秒默认为“数字”。

  • formatMatcher
    • 格式匹配,可能的值包括:
    • basic
    • best fit,默认值
    • “best fit”算法由实现定义,“basic”算法由规范定义。仅当dateStyle和timeStyle均未定义时才使用此选项(以便每个日期时间组件的格式都可以单独自定义)

实现必须支持显示至少以下日期时间组件子集:

星期几、年、月、日、小时、分钟、秒

星期几、年、月、日

年、月、日

年、月

月、日

小时、分钟、秒

小时、分钟

请求的日期时间组件样式可能与当前区域设置支持的有效格式不完全匹配,因此格式匹配器允许您指定如何将请求的样式与最接近的支持格式进行匹配。

样式快捷键选项

  • dateStyle
    • 日期格式化样式,可能的值包括:
    • full
    • long
    • medium
    • short
    • 包含星期、日期、月份、年份和纪元的样式
  • timeStyle
    • 时间格式化样式,可能的值包括:
    • full
    • long
    • medium
    • short
    • 包含小时、分钟、秒和时区名称设置

dateStyletimeStyle 可以互相使用,但是不能与日期时间组件选项weekday``hour``month等一起使用

格式化

format()方法会基于区域和格式化选项进行日期格式化。支持数字、大数和字符串。

const options = {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
};
const dateTimeFormat = new Intl.DateTimeFormat("zh-CN", options);
console.log(dateTimeFormat.format(new Date())); // 2025年12月28日星期日

格式化分割成多部分

formatToParts()将会返回一个对象数组,每个对象表示通过 format() 格式化后字符串的各个部分。可用于构建自定义字符串。

const date = Date.UTC(2012, 11, 17, 3, 0, 42);

const formatter = new Intl.DateTimeFormat("zh-CN", {
  weekday: "long",
  year: "numeric",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
  fractionalSecondDigits: 3,
  hour12: true,
  timeZone: "UTC",
});

console.log(formatter.formatToParts(date));
// [
//     {
//         "type": "year",
//         "value": "2012"
//     },
//     {
//         "type": "literal",
//         "value": "年"
//     },
//     {
//         "type": "month",
//         "value": "12"
//     },
//     {
//         "type": "literal",
//         "value": "月"
//     },
//     {
//         "type": "day",
//         "value": "17"
//     },
//     {
//         "type": "literal",
//         "value": "日"
//     },
//     {
//         "type": "weekday",
//         "value": "星期一"
//     },
//     {
//         "type": "literal",
//         "value": " "
//     },
//     {
//         "type": "dayPeriod",
//         "value": "上午"
//     },
//     {
//         "type": "hour",
//         "value": "3"
//     },
//     {
//         "type": "literal",
//         "value": ":"
//     },
//     {
//         "type": "minute",
//         "value": "00"
//     },
//     {
//         "type": "literal",
//         "value": ":"
//     },
//     {
//         "type": "second",
//         "value": "42"
//     },
//     {
//         "type": "literal",
//         "value": "."
//     },
//     {
//         "type": "fractionalSecond",
//         "value": "000"
//     }
// ]

农历和藏历使用一个 60 年的六十甲子循环周期来命名年份。这些历法没有一种通用的方法来无歧义地为每一年编号,因此年份通过与公历年份的对应关系来区分。在这种情况下,当 DateTimeFormat 被配置为输出年份部分时,会输出 relatedYear 的标记而不是 year

const opts = { year: "numeric", month: "long", day: "numeric" };
const df = new Intl.DateTimeFormat("zh-u-ca-chinese", opts);

console.log(df.formatToParts(Date.UTC(2012, 11, 17, 3, 0, 42)));
// [
//   { type: "relatedYear", value: "2012" },
//   { type: "yearName", value: "壬辰" },
//   { type: "literal", value: "年" },
//   { type: "month", value: "十一月" },
//   { type: "day", value: "5" },
// ];

格式化日期范围

formatRange()返回一个字符串表示日期范围格式化后的内容。

const date1 = new Date(Date.UTC(1906, 0, 10, 10, 0, 0));
const date2 = new Date(Date.UTC(1906, 0, 10, 11, 0, 0));

const fmt1 = new Intl.DateTimeFormat("zh-CN", {
  year: "2-digit",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric",
});
console.log(fmt1.formatRange(date1, date2));
// 06/1/10 18:00–19:00

const fmt2 = new Intl.DateTimeFormat("zh-CN", {
  year: "numeric",
  month: "short",
  day: "numeric",
});
console.log(fmt2.formatRange(date1, date2));
// 1906年1月10日

格式化日期范围分割成多部分

formatRangeToParts()返回一个对象数组,每个对象表示通过 format() 格式化后字符串的各个部分。可用于构建自定义字符串。

const date1 = new Date(Date.UTC(1906, 0, 10, 10, 0, 0));
const date2 = new Date(Date.UTC(1906, 0, 10, 11, 0, 0));

const fmt = new Intl.DateTimeFormat("zh-CN", {
  hour: "numeric",
  minute: "numeric",
});

console.log(fmt.formatRangeToParts(date1, date2));
// [
//     {
//         "type": "hour",
//         "value": "18",
//         "source": "startRange"
//     },
//     {
//         "type": "literal",
//         "value": ":",
//         "source": "startRange"
//     },
//     {
//         "type": "minute",
//         "value": "00",
//         "source": "startRange"
//     },
//     {
//         "type": "literal",
//         "value": "–",
//         "source": "shared"
//     },
//     {
//         "type": "hour",
//         "value": "19",
//         "source": "endRange"
//     },
//     {
//         "type": "literal",
//         "value": ":",
//         "source": "endRange"
//     },
//     {
//         "type": "minute",
//         "value": "00",
//         "source": "endRange"
//     }
// ]

source 会区分出 startRange、endRange 和 shared,而 shared 如果起始和结束日期在输出精度上是等同的,那么输出的标记列表与对起始日期调用 formatToParts() 时的标记列表相同,所有标记均被标记为 source: "shared"

const formatter = new Intl.DateTimeFormat('zh-CN', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});
const startDate = new Date('2024-01-15T10:00:00');
const endDate = new Date('2024-01-15T14:30:00');

console.log(formatter.formatRangeToParts(startDate, endDate));
// [
//     {
//         "type": "year",
//         "value": "2024",
//         "source": "shared"
//     },
//     {
//         "type": "literal",
//         "value": "年",
//         "source": "shared"
//     },
//     {
//         "type": "month",
//         "value": "1",
//         "source": "shared"
//     },
//     {
//         "type": "literal",
//         "value": "月",
//         "source": "shared"
//     },
//     {
//         "type": "day",
//         "value": "15",
//         "source": "shared"
//     },
//     {
//         "type": "literal",
//         "value": "日",
//         "source": "shared"
//     }
// ]

获取配置项

const germanFakeRegion = new Intl.DateTimeFormat("de-XX", { timeZone: "UTC" });

const usedOptions = germanFakeRegion.resolvedOptions();
console.log(usedOptions.locale); // "de" (because "de-XX" does not exist)
console.log(usedOptions.calendar); // "gregory"
console.log(usedOptions.numberingSystem); // "latn"
console.log(usedOptions.timeZone); // "UTC"
console.log(usedOptions.month); // "numeric"

判断返回支持的 locale

在给定的 locales 数组中判断出 DateTimeFormat 支持的 locales。但是可能每个浏览器支持的不大一样。

const locales = ["ban", "id-u-co-pinyin", "de-ID"];
const options = { localeMatcher: "lookup" };

console.log(Intl.DateTimeFormat.supportedLocalesOf(locales, options));
// ["id-u-co-pinyin", "de-ID"]

总结

Intl.DateTimeFormat用于根据语言和地区格式化日期和日期范围,可以轻松实现国际化的日期时间显示,提升应用的用户体验,特别是在全球化应用开发中具有重要价值;其日期范围格式化功能特别适合需要显示时间跨度的应用场景。