Intl.DurationFormat: Fix Duration Formatting for French
ملخص
Pixel shares an article on why a simple JavaScript duration formatter can fail when an app adds French and other locales. It explains how plural rules and sentence ordering vary across languages, and why Intl.DurationFormat offers a better approach to localized durations.
DEV Community
My Duration Formatter Broke in French. Intl.DurationFormat Doesn't.
Here's a function that ships in more codebases than anyone wants to admit: function formatDuration(hours, minutes, seconds) {
const parts = [];
if (hours > 0) parts.push(${hours} hour${hours !== 1 ? "s" : ""});
if (minutes > 0) parts.push(${minutes} minute${minutes !== 1 ? "s" : ""});
if (seconds > 0) parts.push(${seconds} second${seconds !== 1 ? "s" : ""});
return parts.length ? parts.join(", ") : "0 seconds";
}
formatDuration(1, 46, 40); // "1 hour, 46 minutes, 40 seconds"
It works. It ships. Code review doesn't blink — the ternary is a little ugly, but it's correct, and everyone's seen worse. Then the product adds a French locale, and formatDuration keeps returning "1 hour, 46 minutes, 40 seconds" to someone whose UI is in French everywhere else. Nobody wrote a bug here. formatDuration does exactly what it was written to do. The bug is that it was only ever written to do one thing: speak English. The ternary that only counts to two The !== 1 ? "s" : "" trick works in English because English plurals are binary — one thing, or not-one things. Pick any other language and that assumption quietly stops holding: French still needs the trailing "s" logic, but also reorders the sentence with "et" before the last item: 1 h, 46 min et 40 s. Russian and Polish have three plural categories, chosen by the last digit of the number, with exceptions for 11–14. Arabic has up to six — zero, one, two, few, many, other — each a genuinely different word shape, not just a suffix. A two-way ternary can express exactly one rule: English's. It's not almost right for everything else; it's correct for one language and silently wrong for the rest, in a way that never throws, never logs, and never shows up until someone files "the app is broken in French" with no repro steps that mean anything to you. This is also why the npm ecosystem has half a dozen duration-formatting packages, most of them shipping a locale-data JSON file to work around exactly this. The browser has had equivalent locale data loaded since the day it started, for Intl.NumberFormat and Intl.RelativeTimeFormat. Duration formatting was the gap. As of 2025, it isn't anymore. Intl.DurationFormat, tested for real 🎮 Try it yourself ▶️ Open the interactive playground → Runs right in your browser — poke at it and watch the concept react live. Intl.DurationFormat takes a locale and a style, and formats a duration object built from plural field names — hours, minutes, seconds, up through years: const dtf = new Intl.DurationFormat("en", { style: "long" });
dtf.format({ hours: 1, minutes: 46, seconds: 40 });
// "1 hour, 46 minutes, 40 seconds"
new Intl.DurationFormat("fr-FR", { style: "short" })
.format({ hours: 1, minutes: 46, seconds: 40 });
// "1 h, 46 min et 40 s"
Same duration, same call shape, correct output in both languages — word order, conjunction, and all. Nothing about your code changes between locales; only the string you pass as the first argument does. Four style values cover the usual cases, and short is the default if you omit it: const d = { hours: 1, minutes: 46, seconds: 40 };
new Intl.DurationFormat("en", { style: "long" }).format(d); // "1 hour, 46 minutes, 40 seconds"
new Intl.DurationFormat("en", { style: "short" }).format(d); // "1 hr, 46 min, 40 sec"
new Intl.DurationFormat("en", { style: "narrow" }).format(d); // "1h 46m 40s"
new Intl.DurationFormat("en", { style: "digital" }).format(d); // "1:46:40"
I ran all four of those in a real browser before putting them in this post, rather than trust a blog post that did the same thing eight months before this API's behavior settled — and they're exactly what's above, character for character. Three sharp edges, found by actually calling it The surface API is simple. A few of its rules only show up when you poke at it, and they're worth knowing before they poke back. Field names are strict, and wrong ones don't warn you. The object is keyed by plural unit names only — hours, not hour or h. Pass t...

Powered by Readybot.io