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());// I5.4 · ~34 min · Intermediate
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.
You will be able to
Warm-up · Activity 1 of 7
const d = new Date(Date.UTC(2026, 9, 5));
console.log(d.getUTCMonth() + 1 + "/" + d.getUTCDate());Predict · Activity 2 of 7
const n = 1234.5;
console.log(n.toLocaleString("de-DE"), n.toLocaleString("en-US"));Practice · Activity 3 of 7
const eur = new Intl.NumberFormat("de-DE", { style: "currency", ____: "EUR" });
console.log(eur.format(5));Practice · Activity 4 of 7
Practice · Activity 5 of 7
console.log(new Intl.NumberFormat("en-US", { style: "percent" }).format(0.256));Brain teaser · Activity 6 of 7
const price = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(5);
console.log(price === "5,00 €", price.length);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
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.jsOutput
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+00a0Tab 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.
Exercise 1 of 2
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.
Run the tests: the German result has a dot and an ordinary space. Which tool knows each locale’s rules?
new Intl.NumberFormat(locale, { style: "currency", currency: "EUR" })
Divide by 100 before you format: eur.format(cents / 100).
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);
}
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 2
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.
RelativeTimeFormat wants a negative number for the past: format(-1, "day").
So compute (timestamp - now) / DAY, not (now - timestamp) / DAY.
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); return rtf.format(days, "day");
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");
}
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsconst 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.
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" }.
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: BerlinWhy, 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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.