Intl.DurationFormat API

使用 JavaScript 自动本地化格式化时间时长

简介

在向用户展示某个操作持续多长时间时,需要对时长值进行格式化。例如,视频播放器显示播放时长,机票预订显示飞行时间,健身应用显示锻炼时长。如果没有本地化,代码可能会这样写:

const hours = 1;
const minutes = 46;
const seconds = 40;
const duration = `${hours}h ${minutes}m ${seconds}s`;

这样无论用户使用什么语言,都会显示 "1h 46m 40s"。法语用户看到 "1h 46m 40s",但他们期望的是 "1 h 46 min 40 s"。德语用户也只能看到英文缩写。西班牙语用户则没有单位之间的 "y" 连词。

Intl.DurationFormat API 解决了这个问题。它可以根据用户的语言和文化习惯自动格式化时间时长,无需外部库或手动拼接字符串。

const duration = { hours: 1, minutes: 46, seconds: 40 };
new Intl.DurationFormat('fr-FR', { style: 'short' }).format(duration);
// "1 h, 46 min et 40 s"

该格式化器会根据不同的语言环境自动处理缩写、连词、词序和空格。

什么是时长

时长表示一段时间的跨度,而不是某一时刻。日期和时间用于标记事件发生的时间点,时长则用于衡量某件事持续了多久。

这种区别对于格式化很重要。日期涉及日历、时区和历史规则,而时长更简单:它只是在标准单位下衡量经过的时间,不涉及日历上下文。

Intl.DurationFormat API 用于处理时长,Intl.DateTimeFormat API 用于处理日期和时间。请根据需求选择合适的工具。

创建时长格式化器

构造函数接收一个 locale 和一个 options 对象。locale 决定输出的语言,options 控制格式化样式和单位显示方式。

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

调用 format() 并传入一个时长对象。该对象包含各时间单位的数值属性,只需包含需要展示的单位。

const duration = { hours: 2, minutes: 30 };
formatter.format(duration);
// "2 hours and 30 minutes"

API 支持以下时间单位:yearsmonthsweeksdayshoursminutessecondsmillisecondsmicrosecondsnanoseconds。请根据你的数据选择合适的单位。

选择格式化样式

style 选项用于控制输出密度。可用四种样式:longshortnarrowdigital

长格式使用完整单词。适用于正文和无障碍场景。

const duration = { hours: 1, minutes: 46, seconds: 40 };
new Intl.DurationFormat('en', { style: 'long' }).format(duration);
// "1 hour, 46 minutes and 40 seconds"

短格式采用常用缩写。适合空间有限但需保证可读性的场景。

new Intl.DurationFormat('en', { style: 'short' }).format(duration);
// "1 hr, 46 min and 40 sec"

窄格式使用最少字符。适用于移动端等紧凑显示。

new Intl.DurationFormat('en', { style: 'narrow' }).format(duration);
// "1h 46m 40s"

数字格式以计时器样式输出,带有冒号。适用于媒体播放器和倒计时显示。

new Intl.DurationFormat('en', { style: 'digital' }).format(duration);
// "1:46:40"

跨语言本地化

同一时长在不同语言中的格式各异。API 会自动处理所有本地化。

const duration = { hours: 1, minutes: 46, seconds: 40 };

new Intl.DurationFormat('en', { style: 'long' }).format(duration);
// "1 hour, 46 minutes and 40 seconds"

new Intl.DurationFormat('fr', { style: 'long' }).format(duration);
// "1 heure, 46 minutes et 40 secondes"

new Intl.DurationFormat('de', { style: 'long' }).format(duration);
// "1 Stunde, 46 Minuten und 40 Sekunden"

new Intl.DurationFormat('es', { style: 'long' }).format(duration);
// "1 hora, 46 minutos y 40 segundos"

new Intl.DurationFormat('ja', { style: 'long' }).format(duration);
// "1時間46分40秒"

请注意,每个语言环境使用不同的词汇、缩写和连接词。例如法语用 "et",德语用 "und",西班牙语用 "y",日语则没有连接词。API 已内置这些规则。

短格式和窄格式同样会正确本地化。

new Intl.DurationFormat('fr', { style: 'short' }).format(duration);
// "1 h, 46 min et 40 s"

new Intl.DurationFormat('de', { style: 'narrow' }).format(duration);
// "1 Std. 46 Min. 40 Sek."

通过时间计算构建时长对象

时长对象是普通的 JavaScript 对象,包含时间单位属性。可通过任意时间计算生成。

将毫秒数除以相应的因子即可转换为时长单位。

const milliseconds = 6400000; // 1 hour, 46 minutes, 40 seconds
const hours = Math.floor(milliseconds / 3600000);
const minutes = Math.floor((milliseconds % 3600000) / 60000);
const seconds = Math.floor((milliseconds % 60000) / 1000);

const duration = { hours, minutes, seconds };
new Intl.DurationFormat('en', { style: 'short' }).format(duration);
// "1 hr, 46 min and 40 sec"

通过相减时间戳计算两个日期间的时长。

const start = new Date('2025-01-01T10:00:00');
const end = new Date('2025-01-01T11:46:40');
const diffMs = end - start;

const hours = Math.floor(diffMs / 3600000);
const minutes = Math.floor((diffMs % 3600000) / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);

const duration = { hours, minutes, seconds };

只需包含所需的单位。若不需要显示零值,可省略。

const duration = { minutes: 5, seconds: 30 };
new Intl.DurationFormat('en', { style: 'long' }).format(duration);
// "5 minutes and 30 seconds"

控制显示哪些单位

默认情况下,格式化器会显示 duration 对象中提供的所有单位。可以通过 options 对象来控制单位的显示。

可以为每个单位分别指定显示样式。可选项包括 longshortnarrownumeric2-digit

const duration = { hours: 1, minutes: 5, seconds: 3 };

new Intl.DurationFormat('en', {
  hours: 'long',
  minutes: 'numeric',
  seconds: '2-digit'
}).format(duration);
// "1 hour, 5:03"

numeric 样式仅显示数字,不带标签。2-digit 样式会补零。适用于需要紧凑显示文本和数字混合内容的场景。

如需隐藏某些单位,只需在 duration 对象和 options 中都省略它们。

const duration = { hours: 2, minutes: 30 };
new Intl.DurationFormat('en', { style: 'short' }).format(duration);
// "2 hr and 30 min"

数字样式要求所有单位从大到小都存在,或显式配置。

const duration = { minutes: 5, seconds: 30 };
new Intl.DurationFormat('en', { style: 'digital' }).format(duration);
// "5:30"

格式化视频播放器时长

视频播放器会在控件中显示时长。建议使用 narrow 或 digital 样式以便紧凑显示。

function formatVideoDuration(seconds) {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = Math.floor(seconds % 60);

  const duration = hours > 0
    ? { hours, minutes, seconds: secs }
    : { minutes, seconds: secs };

  const locale = navigator.language;
  return new Intl.DurationFormat(locale, { style: 'digital' }).format(duration);
}

formatVideoDuration(6400); // "1:46:40"
formatVideoDuration(330);  // "5:30"

该方法可根据需要有条件地包含小时数,适用于长短视频。

格式化航班和旅行时长

旅行预订界面会显示行程时长。建议使用 short 样式,便于在有限空间内阅读。

function formatFlightDuration(departureDate, arrivalDate, locale) {
  const diffMs = arrivalDate - departureDate;
  const hours = Math.floor(diffMs / 3600000);
  const minutes = Math.floor((diffMs % 3600000) / 60000);

  const duration = { hours, minutes };
  return new Intl.DurationFormat(locale, { style: 'short' }).format(duration);
}

const departure = new Date('2025-06-15T10:30:00');
const arrival = new Date('2025-06-15T18:45:00');

formatFlightDuration(departure, arrival, 'en');
// "8 hr and 15 min"

formatFlightDuration(departure, arrival, 'fr');
// "8 h et 15 min"

格式化锻炼和活动时长

健身应用会跟踪锻炼时长。会话摘要建议用 long 样式,列表视图建议用 narrow 样式。

function formatWorkoutDuration(startTime, endTime, locale) {
  const diffMs = endTime - startTime;
  const hours = Math.floor(diffMs / 3600000);
  const minutes = Math.floor((diffMs % 3600000) / 60000);

  const duration = hours > 0
    ? { hours, minutes }
    : { minutes };

  return new Intl.DurationFormat(locale, { style: 'long' }).format(duration);
}

const workoutStart = new Date('2025-06-15T07:00:00');
const workoutEnd = new Date('2025-06-15T08:15:00');

formatWorkoutDuration(workoutStart, workoutEnd, 'en');
// "1 hour and 15 minutes"

获取自定义显示的格式化片段

formatToParts() 方法会返回一个对象数组,表示格式化输出的每个部分。可用于单独样式化各部分或自定义布局。

const duration = { hours: 1, minutes: 46, seconds: 40 };
const parts = new Intl.DurationFormat('en', { style: 'long' }).formatToParts(duration);

每个片段都包含 typevalue。类型包括 hourminutesecondliteral 以及单位标签,如 hourUnitminuteUnit

[
  { type: "integer", value: "1" },
  { type: "literal", value: " " },
  { type: "unit", value: "hour" },
  { type: "literal", value: ", " },
  { type: "integer", value: "46" },
  { type: "literal", value: " " },
  { type: "unit", value: "minutes" },
  { type: "literal", value: " and " },
  { type: "integer", value: "40" },
  { type: "literal", value: " " },
  { type: "unit", value: "seconds" }
]

遍历各部分以应用自定义样式。

function StyledDuration({ duration }) {
  const parts = new Intl.DurationFormat('en', { style: 'long' }).formatToParts(duration);

  return parts.map((part, i) => {
    if (part.type === 'integer') {
      return <strong key={i}>{part.value}</strong>;
    }
    return <span key={i}>{part.value}</span>;
  });
}

复用 formatter 实例以提升性能

为每个 duration 创建新的 formatter 会增加开销。建议只创建一次 formatter 并复用。

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

const durations = [
  { hours: 1, minutes: 30 },
  { hours: 2, minutes: 15 },
  { hours: 0, minutes: 45 }
];

durations.map(d => formatter.format(d));
// ["1 hr and 30 min", "2 hr and 15 min", "45 min"]

在循环或渲染多个 duration 时,这种模式可以提升格式化性能。

从手动字符串拼接迁移

用 API 替换手动字符串拼接。这样可以减少代码量并增强本地化支持。

替换前:

function formatDuration(hours, minutes) {
  if (hours > 0 && minutes > 0) {
    return `${hours}h ${minutes}m`;
  } else if (hours > 0) {
    return `${hours}h`;
  } else {
    return `${minutes}m`;
  }
}

替换后:

function formatDuration(hours, minutes, locale) {
  const duration = {};
  if (hours > 0) duration.hours = hours;
  if (minutes > 0) duration.minutes = minutes;

  return new Intl.DurationFormat(locale, { style: 'narrow' }).format(duration);
}

API 版本会自动处理所有条件逻辑,并支持多语言。

从第三方库迁移

Moment.js 和 date-fns 等库可以格式化 duration,但会增加 bundle 体积。原生 API 可消除这些依赖。

替换 Moment.js 的 duration 格式化:

// Before: Moment.js
const duration = moment.duration(6400, 'seconds');
const formatted = duration.hours() + 'h ' + duration.minutes() + 'm';

// After: Intl.DurationFormat
const duration = {
  hours: Math.floor(6400 / 3600),
  minutes: Math.floor((6400 % 3600) / 60)
};
new Intl.DurationFormat('en', { style: 'narrow' }).format(duration);

替换 date-fns 的 duration 格式化:

// Before: date-fns
import { formatDuration, intervalToDuration } from 'date-fns';
const duration = intervalToDuration({ start: 0, end: 6400000 });
const formatted = formatDuration(duration);

// After: Intl.DurationFormat
const ms = 6400000;
const duration = {
  hours: Math.floor(ms / 3600000),
  minutes: Math.floor((ms % 3600000) / 60000),
  seconds: Math.floor((ms % 60000) / 1000)
};
new Intl.DurationFormat('en', { style: 'long' }).format(duration);

原生 API 提供相同功能且无需依赖。

浏览器支持与 polyfill

Intl.DurationFormat API 于 2025 年 3 月成为 Baseline。现已支持最新版本的 Chrome、Edge、Firefox 和 Safari。

使用前请检查支持情况:

if (typeof Intl.DurationFormat !== 'undefined') {
  const formatter = new Intl.DurationFormat('en', { style: 'short' });
  return formatter.format(duration);
} else {
  // Fallback for older browsers
  return `${duration.hours}h ${duration.minutes}m`;
}

为了获得更广泛的支持,请使用 polyfill。FormatJS 项目提供了 @formatjs/intl-durationformat

npm install @formatjs/intl-durationformat

如有需要,请导入并进行 polyfill:

if (!Intl.DurationFormat) {
  await import('@formatjs/intl-durationformat/polyfill');
}

该 polyfill 压缩后大约为 15KB。请按需加载,以避免在现代浏览器中产生额外开销。

何时使用 DurationFormat

在显示已用时间、剩余时间或时长等场景下,请使用 Intl.DurationFormat。这包括视频播放器、计时器、倒计时、追踪应用、旅行预订和会话时长显示等。

不要将其用于日期、时间或时间戳。请使用 Intl.DateTimeFormat。不要将其用于诸如“2 小时前”这样的相对时间表达。请使用 Intl.RelativeTimeFormat

该 API 仅用于格式化时长,不进行时间计算。您需要自行根据日期、时间戳或其他来源计算时长数值,格式化器只负责展示。