JavaScript Basics: Format Numbers with Intl.NumberFormat
Number displays vary by language and region. Use Intl.NumberFormat to format separators, currencies, and percentages instead of assembling those strings yourself.
These examples use built-in features and were tested on Linux with Node.js v24.19.0 and ICU 78.3. No packages are needed. Assertions check the results before the program finishes.
Node.js uses ICU for internationalization. Official binaries include full ICU data, but custom builds can differ. The outputs below were captured in the tested environment. Explicit locales avoid relying on the machine’s default, but formatting details can still vary across implementations and locale-data versions, including spaces and symbols. Avoid treating a formatted string as a portable data format.
Prepare a scratch directory
You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. These commands stop the shell if setup fails. Save each named source file as UTF-8; .mjs selects ES modules.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Choose the locale and decimal places
Save this as format-decimals.mjs:
import assert from 'node:assert/strict';
const distance = 12345.6;
const options = { minimumFractionDigits: 2, maximumFractionDigits: 2 };
const englishFormatter = new Intl.NumberFormat('en-US', options);
const germanFormatter = new Intl.NumberFormat('de-DE', options);
const englishText = englishFormatter.format(distance);
const germanText = germanFormatter.format(distance);
assert.equal(englishFormatter.resolvedOptions().locale, 'en-US');
assert.equal(germanFormatter.resolvedOptions().locale, 'de-DE');
assert.notEqual(englishText, germanText);
assert.equal(distance, 12345.6);
console.log(`English: ${englishText}`);
console.log(`German: ${germanText}`);
console.log(`Original number: ${distance}`);Run it:
node format-decimals.mjsOutput:
English: 12,345.60
German: 12.345,60
Original number: 12345.6The constructor accepts a locale and options. Here, en-US requests US English conventions and de-DE requests German conventions for Germany. Setting both fraction-digit limits to 2 gives two displayed decimal places. Formatting returns text and leaves the original number unchanged. Reuse a formatter for other values that need the same settings.
2. Display money and percentages
Save this as format-currency-percent.mjs:
import assert from 'node:assert/strict';
const price = 42.5;
const completionRatio = 0.375;
const priceFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const percentFormatter = new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
assert.equal(priceFormatter.resolvedOptions().currency, 'USD');
assert.equal(percentFormatter.resolvedOptions().style, 'percent');
assert.equal(percentFormatter.formatToParts(completionRatio)
.find(part => part.type === 'integer').value, '37');
console.log(`Price: ${priceFormatter.format(price)}`);
console.log(`Completion: ${percentFormatter.format(completionRatio)}`);
console.log(`A ratio of 1: ${percentFormatter.format(1)}`);Run it:
node format-currency-percent.mjsOutput:
Price: $42.50
Completion: 37.5%
A ratio of 1: 100.0%Currency style requires a currency code such as USD; the locale controls its presentation. This does not convert between currencies. Percent style displays a ratio multiplied by 100, so pass 0.375 for 37.5%, not 37.5.
Number formatting is for display, not precise monetary arithmetic. Ordinary JavaScript numbers use binary floating-point, so some decimal fractions are approximations. Use a deliberate calculation strategy, such as safe-range integer minor units with explicit rounding rules, before formatting the final amount. See MDN’s Number reference for numeric precision limits.
3. Read the formatted parts
Save this as format-number-parts.mjs:
import assert from 'node:assert/strict';
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const amount = 1234.5;
const parts = formatter.formatToParts(amount);
const rebuiltText = parts.map(part => part.value).join('');
const fractionPart = parts.find(part => part.type === 'fraction');
assert.equal(rebuiltText, formatter.format(amount));
assert.equal(fractionPart.value, '50');
assert.equal(parts.some(part => part.type === 'currency'), true);
for (const part of parts) {
console.log(`${part.type}: ${JSON.stringify(part.value)}`);
}
console.log(`Rebuilt: ${rebuiltText}`);Run it:
node format-number-parts.mjsOutput:
currency: "$"
integer: "1"
group: ","
integer: "234"
decimal: "."
fraction: "50"
Rebuilt: $1,234.50formatToParts() returns an ordered array of objects with type and value properties. This lets a user interface style the currency or fractional part without guessing where it appears. Keep every part, including any literal spaces, when rebuilding the string. These are parts of a formatted number, not a parser for arbitrary user input.
Clean up
In the same terminal, remove only the example files, return to your original directory, and remove the empty scratch directory:
rm -- format-decimals.mjs format-currency-percent.mjs format-number-parts.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.