All lessons

Lesson 05·Unit 3 · The Intl toolbox·List, range & duration formatting·Lv 101·~6 min

Lists, ranges & durations

Joining values is grammar. English writes 'A, B, and C' where German writes 'A, B und C' and Japanese writes A、B、C with no conjunction at all. Date ranges collapse shared fields per locale ('Jan 3 – 5, 2026' in en-US, '3.–5. Jan. 2026' in de-DE), and even '0–1 items' needs a locale to pick its plural form. Intl.ListFormat, formatRange, and selectRange carry the rules.

Intl.ListFormatformatRangeIntl.DurationFormat

By the end

  • ·Join items with Intl.ListFormat, choosing conjunction, disjunction, or unit on purpose rather than by default.
  • ·Render ranges with formatRange, and see it collapse the repetition that formatting each end separately leaves behind.
  • ·Look up a range's plural category with selectRange, and say why it is not the category of the larger end.

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 ("Jan 3, 2026 – Jan 5, 2026" instead of "Jan 3 – 5, 2026"), uses the wrong dash, and skips the plural question entirely: "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. Your code supplies the items. The moment the ", " lives in your code instead, you have shipped English grammar to every market.

How it works

A range is not two values and a dash

The natural implementation of "Jan 3 – 5, 2026" is to format both dates and join them with a dash. Look at what that loses. 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 all CLDR data. Intl.DateTimeFormat.prototype.formatRange reads them, and your 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 you set.

See it yourself

Try these, in order

Each step triggers a specific failure you should recognize on sight.

  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 template you could share.
  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. Your concatenation would have to reimplement that per locale.
  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. The plural form of a range is its own CLDR question, not the form of either endpoint.
  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 your browser ships Intl.DurationFormat, you see each locale spell 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)”. That is why 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 uses the Oxford comma in the "long" style. Chinese uses (the ideographic comma). Arabic uses و 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. Which fields collapse, which dash appears, and where the spaces go are all CLDR data.

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 answer is not always the category of the larger number. 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. If your code picks the plural form of the end value, one of those markets gets the wrong string.

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: engines without it show the fallback text instead of throwing.

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". Languages disagree on which endpoint decides.
  • Intl.DurationFormat: "1 hr, 20 min". Feature-detect it before use. Older engines lack it.
  • • Relative time ("3 days ago") lives in the Formatters demo: Intl.RelativeTimeFormat.

If you remember one thing

Joining is grammar. 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" deliberately; 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, not template decisions.
  • 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 newer than the rest of this page and not in every engine.

Use this with

Stakeholders

EngineeringDesign

Moments

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

Field note

Commas in lists carry real stakes: O'Connor v. Oakhurst Dairy (2017) centered on a missing serial comma in a Maine overtime statute. The ambiguity cost the dairy about $5 million in settled overtime pay. Locale-correct list punctuation carries meaning.

Wikipedia: O'Connor v. Oakhurst Dairy

Quick check

3 questions · pass at 2+

  1. Question 1/3

    How does Intl.ListFormat('de', {type:'conjunction'}) join A, B, C?

  2. Question 2/3

    With { year:'numeric', month:'short', day:'numeric' }, what does Intl.DateTimeFormat('en-US').formatRange(Jan 3 2026, Jan 5 2026) return?

  3. Question 3/3

    Your UI says “0–1 items”. Which plural category does that range take?

Words you'll hear

Where to read more

Related lessons