Skip to content
aviral gupta

// I5.4 · ~34 min · Intermediate

Formatting for people with Intl

After this lesson you show numbers, euro prices, dates, relative times and lists the way people in Germany or the US expect, with the same output on every computer.

Lesson 4 of 5 in I5 Working with data

You will be able to

  • Format numbers, euro prices, percentages and compact numbers for a locale with Intl.NumberFormat
  • Format dates and times with Intl.DateTimeFormat and a fixed timeZone
  • Write relative times, plural forms and lists with Intl, and handle the no-break spaces in the output
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson I5.3. Building a date text by hand. What does this print?

    const d = new Date(Date.UTC(2026, 9, 5));
    console.log(d.getUTCMonth() + 1 + "/" + d.getUTCDate());
  2. Predict · Activity 2 of 7

    Predict before you read on. The same number, two locales. What does this print?

    const n = 1234.5;
    console.log(n.toLocaleString("de-DE"), n.toLocaleString("en-US"));
  3. Practice · Activity 3 of 7

    Fill in the option name that says which currency to use.

    const eur = new Intl.NumberFormat("de-DE", { style: "currency", ____: "EUR" });
    console.log(eur.format(5));
    { style: "currency",: "EUR" }
  4. Practice · Activity 4 of 7

    Match each Intl tool to what it writes.

  5. Practice · Activity 5 of 7

    What does this print?

    console.log(new Intl.NumberFormat("en-US", { style: "percent" }).format(0.256));
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Five euros, formatted in German, compared with typed text. What does this print?

    const price = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(5);
    console.log(price === "5,00 €", price.length);
  7. Apply · Activity 7 of 7

    Mini-task: a German receipt. You have items = [{name: "Tea", price: 3.5}, {name: "Cake", price: 4.2}, {name: "Coffee", price: 2.8}]. Print the date 5 October 2026 in German long form, each item with its price in euros, the sum, and a German list of the item names.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

One value, many formats

The same price is formatted for Germany and for the US, then a percentage and a compact number. A date with a time is written in German with timeZone: "UTC", and the same moment is shown as a Berlin time. Relative time, plural rules and a list follow. The last line looks inside a formatted price: it has six characters, and the fifth is not an ordinary space.

main.js

const when = new Date(Date.UTC(2026, 9, 5, 14, 30));

// 1. Numbers and prices: the same number for two locales.
const eurDE = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
const eurUS = new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" });
console.log(eurDE.format(1234.56), "|", eurUS.format(1234.56));
console.log(new Intl.NumberFormat("en-US", { style: "percent" }).format(0.256));
console.log(new Intl.NumberFormat("en-US", { notation: "compact" }).format(1234567));

// 2. Dates: always give a timeZone, or the output depends on the computer.
const long = new Intl.DateTimeFormat("de-DE", { dateStyle: "long", timeStyle: "short", timeZone: "UTC" });
console.log(long.format(when));
const berlin = new Intl.DateTimeFormat("en-GB", { timeStyle: "short", timeZone: "Europe/Berlin" });
console.log("in Berlin:", berlin.format(when));

// 3. Relative time, plural rules and lists.
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtf.format(-1, "day"), "|", rtf.format(3, "day"));
const plural = new Intl.PluralRules("en");
for (const n of [1, 2]) console.log(n, plural.select(n) === "one" ? "file" : "files");
console.log(new Intl.ListFormat("en", { type: "conjunction" }).format(["Map", "Set", "Intl"]));

// 4. The space before € is not an ordinary space.
const price = eurDE.format(5);
console.log(price.length, "U+" + price.codePointAt(4).toString(16).padStart(4, "0"));

Run it with

node main.js

Output

1.234,56 € | €1,234.56
26%
1.2M
5. Oktober 2026 um 14:30
in Berlin: 16:30
yesterday | in 3 days
1 file
2 files
Map, Set, and Intl
6 U+00a0
  • German puts € after the amount, American English before it, for the same currency.
  • 14:30 UTC is 16:30 in Berlin in October; the timeZone option changed only the text, not the moment.
  • U+00A0 is the no-break space; an ordinary space would be U+0020.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

Prices in euros

formatPrice(cents, locale) gets an amount in cents and a locale, and returns the price in euros as that locale writes it: formatPrice(1250, "de-DE") is "12,50 €" with a no-break space, and formatPrice(1250, "en-US") is "€12.50". The starter builds the text by hand and ignores the locale. Use Intl.NumberFormat.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: the German result has a dot and an ordinary space. Which tool knows each locale’s rules?

  2. Hint 2

    new Intl.NumberFormat(locale, { style: "currency", currency: "EUR" })

  3. Hint 3

    Divide by 100 before you format: eur.format(cents / 100).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function formatPrice(cents, locale) {
  const eur = new Intl.NumberFormat(locale, { style: "currency", currency: "EUR" });
  return eur.format(cents / 100);
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function formatPrice(cents, locale) {
  return (cents / 100).toFixed(2) + " €";
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatPrice} from './main.js';

test('German: 12,50 and a no-break space before the euro sign', () => {
  const got = formatPrice(1250, 'de-DE');
  assert.equal(got, '12,50\u00a0€', `formatPrice(1250, 'de-DE') gave ${JSON.stringify(got)}`);
});

test('American English: the euro sign comes first', () => {
  const got = formatPrice(1250, 'en-US');
  assert.equal(got, '€12.50', `formatPrice(1250, 'en-US') gave ${JSON.stringify(got)}`);
});

test('Thousands are grouped', () => {
  const got = formatPrice(123456, 'de-DE');
  assert.equal(got, '1.234,56\u00a0€', `formatPrice(123456, 'de-DE') gave ${JSON.stringify(got)}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Yesterday, today, tomorrow

daysAgo(timestamp, now) gets two timestamps at UTC midnight and returns English text: "yesterday" one day back, "today" for the same day, "tomorrow" one day ahead, and otherwise "3 days ago" or "in 2 days". The starter always writes "N days ago", even for one day or for the future. Use Intl.RelativeTimeFormat with numeric: "auto".

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    RelativeTimeFormat wants a negative number for the past: format(-1, "day").

  2. Hint 2

    So compute (timestamp - now) / DAY, not (now - timestamp) / DAY.

  3. Hint 3

    const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); return rtf.format(days, "day");

Show a solution

One way to solve it. Yours can look different and still pass the checks.

const DAY = 24 * 60 * 60 * 1000;
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });

export function daysAgo(timestamp, now) {
  const days = Math.round((timestamp - now) / DAY);
  return rtf.format(days, "day");
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

const DAY = 24 * 60 * 60 * 1000;

export function daysAgo(timestamp, now) {
  const days = Math.round((now - timestamp) / DAY);
  return `${days} days ago`;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {daysAgo} from './main.js';

const now = Date.UTC(2026, 9, 5);
const day = (n) => Date.UTC(2026, 9, 5 + n);

test('One day back is yesterday', () => {
  assert.equal(daysAgo(day(-1), now), 'yesterday', `got ${daysAgo(day(-1), now)}`);
});

test('Three days back is 3 days ago', () => {
  assert.equal(daysAgo(day(-3), now), '3 days ago', `got ${daysAgo(day(-3), now)}`);
});

test('The same day is today', () => {
  assert.equal(daysAgo(now, now), 'today', `got ${daysAgo(now, now)}`);
});

test('Two days ahead is in 2 days', () => {
  assert.equal(daysAgo(day(2), now), 'in 2 days', `got ${daysAgo(day(2), now)}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Splitting a formatted price on a space

const price = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(12.5);
const [amount, symbol] = price.split(" ");
console.log(symbol.toUpperCase());

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Why, and the fix

The space in "12,50 €" is a no-break space, U+00A0, not " ", so split(" ") finds nothing to split and symbol is undefined. Do not take formatted text apart; it is for people. If you need the parts, use formatToParts(12.5), which returns them as objects, or keep the number and the currency code separately.

The currency style without a currency

const price = new Intl.NumberFormat("de-DE", { style: "currency" });
console.log(price.format(5));

What Node.js prints

TypeError: Currency code is required with currency style.

Why, and the fix

Intl cannot guess the currency from the locale: Germany and Austria use euros, but a German shop may sell in dollars. Always give the code: { style: "currency", currency: "EUR" }.

A city name as the time zone

const time = new Intl.DateTimeFormat("de-DE", { timeStyle: "short", timeZone: "Berlin" });
console.log(time.format(new Date()));

What Node.js prints

RangeError: Invalid time zone specified: Berlin

Why, and the fix

timeZone takes an IANA time zone name, which is region and city: "Europe/Berlin", "America/New_York". "UTC" and offsets such as "+01:00" work too. A city on its own is not a valid name.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

One formatter per locale and style

Every Intl tool works the same way: create a formatter with a locale, such as "de-DE" or "en-US", and options, then call format(value). Intl.NumberFormat writes 1234.56 as 1.234,56 in German and 1,234.56 in American English. { style: "currency", currency: "EUR" } adds the euro sign and two decimals, and the currency option is required for that style. style: "percent" multiplies by 100, and notation: "compact" writes 1234567 as 1.2M. number.toLocaleString(locale, options) takes the same options, but for many values MDN advises creating one formatter and reusing its format().

Dates need a time zone

Intl.DateTimeFormat formats a Date for a locale: dateStyle and timeStyle ("full", "long", "medium", "short") pick ready-made patterns, and options like weekday: "long" or hour: "2-digit" pick parts. Without a timeZone option, the formatter uses the computer’s time zone, so the same Date can show another day on another machine. Give timeZone: "UTC" when the value is stored in UTC, or the place you mean, such as "Europe/Berlin". The same timestamp then prints the same text everywhere. date.toLocaleDateString(locale, options) works too.

Relative time, plurals, lists and hidden spaces

Intl.RelativeTimeFormat writes format(-3, "day") as "3 days ago"; with numeric: "auto", -1 becomes "yesterday". Intl.PluralRules tells you which form a number needs: select(1) is "one" in English, select(2) is "other". Intl.ListFormat joins ["a", "b", "c"] as "a, b, and c" in English and "a, b und c" in German. The output may contain no-break spaces: German writes the space before € and % as U+00A0, and French groups thousands with U+202F (1234.5 becomes "1\u202f234,5" in fr-FR). They look like spaces but are different characters, so never split formatted text on " " or compare it with a typed string. Compare with "\u00a0" in the expected text, or compare numbers before formatting.

Sources

Last reviewed October 5, 2026