Skip to content
Locale Lab

Chapter 05·The Intl toolbox·~6 min

Lists, ranges & durations

Joining values is grammar. English writes 'A, B, and C', German 'A, B und C', Japanese A、B、C with no conjunction at all. Date ranges collapse shared fields per locale. Intl.ListFormat, formatRange, and selectRange carry the rules.

Intl.ListFormatformatRangeIntl.DurationFormat

The problem

Two lines of code look harmless: items.join(", ") for a list, and format(start) + " – " + format(end) for a range. Both hardcode English grammar. The join ships the wrong connective and comma policy to every other market; the range repeats fields the locale would have collapsed, uses the wrong dash, and skips the plural question: "0–1 items" takes a different plural category in French than in English.

How it works

Even “and” is locale data

Joining three items into a sentence feels like string work. It is grammar. Locales differ in every component: the separator (Japanese uses the ideographic comma 、), the final connective (English "and", German "und", Arabic و, or none: Japanese joins A、B、C with no conjunction), and whether a comma precedes that connective (English long style writes "A, B, and C", German writes "A, B und C"). No template has slots for all of that.

So CLDR stores list patterns per locale, in three flavors: conjunction ("and"-lists), disjunction ("or"-lists), and unit lists ("5 ft, 7 in": no connective, tighter separators). Intl.ListFormat picks the right pattern for the locale and the flavor. Application code supplies the items. The moment the ", " lives in code instead, English grammar ships to every market.

A range is not two values and a dash

The natural implementation of "Jan 3 – 5, 2026" formats both dates and joins them with a dash. It loses three things CLDR encodes. English collapses the shared month and year and says the month once. German writes 3.–5. Jan. 2026 with no spaces around its dash. Japanese does not collapse at all and repeats both full dates, joined with ~. Which fields collapse, which dash character appears, and where the spaces go are CLDR data: Intl.DateTimeFormat.prototype.formatRange reads all three, and concatenation cannot.

Numbers get the same treatment. Intl.NumberFormat.prototype.formatRange produces 2–4 in English, and for currency it decides per locale whether the symbol repeats: en-US writes €2.50 – €4.99, while de-DE writes 2,50–4,99 € (one symbol, trailing). When both endpoints format to the same string, the range collapses to a single approximate value: formatRange(3, 3) is ~3, because "3–3" is not a range any human would write.

Ranges also need one thing engineers rarely budget for: a plural form. "2–4 items" has to pick an arm, and languages disagree about which endpoint decides. Intl.PluralRules.prototype.selectRange answers per locale. English files the range 0–1 under other ("0–1 items") even though 1 alone is one, while French files the same range under one. Pick the form of the end value and one of those markets reads a wrong string.

en-US  Jan 3 – 5, 2026
en-GB  3 – 5 Jan 2026
de-DE  3.–5. Jan. 2026
ja-JP  2026/01/03~2026/01/05
The same two dates through formatRange: collapse behavior is per locale, not a formatting option the caller sets.

The demo

Try these, in order

Each step reproduces one specific failure in the demo below.

  1. 1
    With the default items, compare the en-US and de-DE rows in the results grid. English (long style) writes “apples, oranges, pears, and bananas”, Oxford comma included. German writes “… und …” with no comma before it. Neither is a shareable template.
  2. 2
    Switch the Scenario to “Unit · 'A B C' (use for adjacent units, e.g. '1h 20m')” with Style “narrow”. Items join with spaces and no conjunction: the correct mode for adjacent measurements like “1 h 20 min”. Using conjunction style for units reads wrong everywhere.
  3. 3
    In the date-range section, keep the default Start date (2026-01-03) and End date (2026-01-05) and compare the en-US and ja-JP rows. Then move End date into March (2026-03-05). en-US collapses the shared fields to “Jan 3 – 5, 2026”, while ja-JP repeats both full dates: 2026/01/03~2026/01/05. With the end date in March, en-US names both months (“Jan 3 – Mar 5, 2026”). The formatter decides field by field what to repeat.
  4. 4
    Set Range start to 0 and Range end to 1, then read the selectRange table. English gives other for the range 0–1 even though select(1) is one. French gives one for the same range.
  5. 5
    Set Range start and Range end both to 3, with Format as “Plain number”. The number-range rows collapse to ~3: when both endpoints format identically, the formatter emits a single approximate value instead of a degenerate “3–3”.
  6. 6
    In the durations section, change Hours, Minutes, and the Duration style select. If the browser ships Intl.DurationFormat, each locale spells the duration in four styles. Internally it joins the units with the same unit-list patterns ListFormat exposes. If not, every row reads “(unsupported in this browser)”: the demo feature-detects instead of calling it blind.

Conjunction · 'A, B, and C' · style: long

en-USapples, oranges, pears, and bananas
en-GBapples, oranges, pears and bananas
en-INapples, oranges, pears and bananas
de-DEapples, oranges, pears und bananas
de-CHapples, oranges, pears und bananas
fr-FRapples, oranges, pears et bananas
fr-CAapples, oranges, pears et bananas
es-ESapples, oranges, pears y bananas
pt-BRapples, oranges, pears e bananas
it-ITapples, oranges, pears e bananas
nl-NLapples, oranges, pears en bananas
ru-RUapples, oranges, pears и bananas
pl-PLapples, oranges, pears i bananas
zh-CNapples、oranges、pears和bananas
zh-TWapples、oranges、pears和bananas
ja-JPapples、oranges、pears、bananas
ko-KRapples, oranges, pears 및 bananas
th-THapples oranges pears และbananas
hi-INapples, oranges, pears, और bananas
ar-SAapples وoranges وpears وbananas
ar-EGapples وoranges وpears وbananas
he-ILapples, oranges, pears ו-bananas
fa-IRapples،‏ oranges،‏ pears، و bananas
ur-PKapples، oranges، pears، اور bananas

English keeps the Oxford comma in the "long" style, Chinese joins with 、 (the ideographic comma), and Arabic places و before the last item.

Date ranges · formatRange

Intl.DateTimeFormat.prototype.formatRange collapses the fields the two dates share, per locale. Formatting each date separately and joining them with a dash repeats what the locale would have dropped.

en-USJan 3 – 5, 2026
en-GB3 – 5 Jan 2026
de-DE3.–5. Jan. 2026
fr-FR3–5 janv. 2026
ja-JP2026/01/03~2026/01/05
ar-EG٣–٥ يناير ٢٠٢٦

With the default dates, en-US collapses the shared month and year ("Jan 3 – 5, 2026"). ja-JP repeats both full dates. The fields, the dash, and the spacing all come from CLDR.

Number ranges · formatRange

"2–4 items", "€2.50 – €4.99": Intl.NumberFormat.prototype.formatRange picks the locale's range pattern and decides per locale whether the currency symbol repeats. If both endpoints format to the same string, the output collapses to a single approximate value.

en-US2–4
en-GB2–4
de-DE2–4
fr-FR2–4
ja-JP2~4
ar-EG٢–٤

Which plural form does a range take? · selectRange

"2–4 items" needs a plural form too, and languages disagree on which endpoint decides it. Intl.PluralRules.prototype.selectRange answers per locale. The table reuses the Range start / Range end values above.

Localeselect(2)select(4)selectRange(2, 4)
enotherotherother
frotherotherother
deotherotherother
plfewfewfew
rufewfewfew
artwofewfew
jaotherotherother

Try 0 and 1: English files the range 0–1 under other ("0–1 items"), even though 1 alone is one. French files the same range under one. Code that picks the plural form of the end value gets one of those markets wrong.

Durations · Intl.DurationFormat

Intl.DurationFormat formats a duration record (hours, minutes, and so on) per locale, in four styles. It is newer than the rest of this page, so the demo feature-detects it.

en-US…
en-GB…
de-DE…
fr-FR…
ja-JP…
ar-EG…

When to reach for which

  • • Intl.ListFormat: joining items with "and" / "or". Never write arr.join(", ") in user-facing strings.
  • • DateTimeFormat.formatRange / NumberFormat.formatRange: any two endpoints shown together (dates, prices, counts). The dash and the collapsing are locale data.
  • • PluralRules.selectRange: the plural form for "2–4 items".
  • • Intl.DurationFormat: "1 hr, 20 min". Older engines lack it, so feature-detect before use.
  • • Relative time ("3 days ago") lives in the Formatters demo: Intl.RelativeTimeFormat.

The short version

Joining values into a sentence is grammar work: Intl.ListFormat carries each locale's connectives, formatRange decides how much of a range to repeat, and selectRange answers which plural form a range takes. None of it survives a hardcoded dash or comma.

What to do about it

  • Join arrays with Intl.ListFormat, choosing type: "conjunction" | "disjunction" | "unit" per case; unit is the mode for adjacent measurements like "1 h 20 min".
  • Render date spans with Intl.DateTimeFormat.prototype.formatRange. Which shared fields collapse, which dash appears, and where the spaces go are locale data.
  • Render numeric and price spans with Intl.NumberFormat.prototype.formatRange; it decides per locale whether the currency symbol repeats, and collapses equal endpoints to a single approximate value (~3).
  • Pick the plural form of a range with Intl.PluralRules.prototype.selectRange, not the category of either endpoint.
  • Feature-detect Intl.DurationFormat before calling it; it is not in every engine.

Where this comes up

Who it concerns

EngineeringDesign

Moments

  • ·Building any list, summary, or activity feed
  • ·Rendering date, price, or quantity ranges
  • ·Component library review

Field note

In O'Connor v. Oakhurst Dairy, the First Circuit ruled in 2017 that a missing serial comma in a Maine overtime law left an exemption clause ambiguous. The drivers settled in 2018 for about $5 million in unpaid overtime.

Wikipedia: O'Connor v. Oakhurst Dairy ↗

Terms in this chapter

Where to read more

Related chapters