JAVASCRIPT / NUMBERS, MATH, AND BIGINT
Number formatting with toLocaleString
Format numbers as locale-aware text: grouped digits, percents, currency amounts, and compact notation, all with toLocaleString.
What you will learn
- Format one number for any locale with toLocaleString('en-US'), 'de-DE', or 'en-IN'
- Switch output between decimal, percent, currency, and compact with the options object
- Override the default three-fraction-digit rounding with maximumFractionDigits
- Reuse a single Intl.NumberFormat instead of calling toLocaleString in a loop
Understanding Number formatting with toLocaleString
Number.prototype.toLocaleString is a front door to Intl.NumberFormat. When you call (1234.5).toLocaleString('de-DE'), the engine reads the locale data compiled into the runtime and builds a display string from it: '1.234,5'. The number itself is untouched, because numbers are immutable primitives, and what comes back is text produced by a renderer configured from the locale plus your options.
The first argument is a BCP 47 language tag, and it controls the things you cannot guess: which character groups thousands, which one marks the decimal point, and how wide the groups are. en-US groups by threes, de-DE swaps the two separators, and en-IN groups the leading digits in twos, so 1234567 becomes 12,34,567. The second argument, the options object, controls what you are formatting: style ('decimal', 'percent', 'currency', 'unit'), how many digits to keep, notation ('standard' or 'compact'), and how the sign is shown.
Two defaults catch people out. Omitting the locale means whatever this browser or Node process happens to be set to, so identical code produces different strings on different machines; pass a locale explicitly whenever the output matters. Decimal style also caps output at three fraction digits, so 1.23456 renders as '1.235' while your variable still holds every digit. Keep the number for arithmetic, comparison, and storage, and treat the string as something only a human reads.
const n = 1234567.891;
console.log(n.toLocaleString('en-US'));
console.log(n.toLocaleString('de-DE'));
console.log(n.toLocaleString('en-IN'));
console.log(n.toLocaleString('en-US', { maximumFractionDigits: 0 }));
console.log((1234.5).toLocaleString('en-US', { style: 'currency', currency: 'USD' }));
console.log((0.256).toLocaleString('en-US', { style: 'percent', minimumFractionDigits: 1 }));
console.log(n);toLocaleString hands your number to Intl.NumberFormat and returns a locale-specific display string, leaving the numeric value completely unchanged.
Worked examples
One formatter, many values
Builds an Intl.NumberFormat once and reuses it, which is the right shape for lists and tables.
const money = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
for (const price of [4.5, 19, 1234.567]) {
console.log(money.format(price));
}
console.log((4.5).toLocaleString('en-US', { style: 'currency', currency: 'USD' }) === money.format(4.5));Example explained
Line 1new Intl.NumberFormat resolves the locale and options once, so format() only has to render; toLocaleString redoes that setup work on every call.
Line 219 prints as $19.00 because currency style sets both minimum and maximumFractionDigits to the currency's minor-unit count, which is 2 for USD.
Line 31234.567 becomes $1,234.57 through the default half-expand rounding, and the array still holds 1234.567.
Line 4The final true shows toLocaleString is a one-shot wrapper around exactly the same formatter.
Currency defaults and negatives
Shows that the currency code, not the locale, decides the decimal count, and how negatives can be rendered.
const amount = 1234.5;
console.log(amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' }));
console.log(amount.toLocaleString('en-US', { style: 'currency', currency: 'JPY' }));
console.log((-amount).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
currencySign: 'accounting'
}));
console.log(amount.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0
}));Example explained
Line 1JPY has zero minor units, so the same options that produce two decimals for USD produce none here and 1234.5 rounds to 1,235.
Line 2currencySign: 'accounting' asks for the locale's accounting form of a negative, which in en-US is parentheses instead of a minus sign.
Line 3An explicit maximumFractionDigits of 0 overrides the currency default, and minimumFractionDigits follows it down to 0 rather than staying at 2.
Compact notation and padding
Uses notation, grouping, and integer padding to produce short labels and fixed-width numbers.
const views = 1234567;
console.log(views.toLocaleString('en-US', { notation: 'compact' }));
console.log(views.toLocaleString('en-US', { notation: 'compact', compactDisplay: 'long' }));
console.log(views.toLocaleString('en-US', { useGrouping: false }));
console.log((7).toLocaleString('en-US', { minimumIntegerDigits: 2 }));Example explained
Line 1notation: 'compact' keeps about two significant digits by default, so 1234567 collapses to 1.2M.
Line 2compactDisplay: 'long' swaps the short suffix for the locale's word, 'million' in English.
Line 3useGrouping: false removes separators, which is what you want for a year, a port number, or an identifier.
Line 4minimumIntegerDigits pads the integer part with leading zeros, giving the '07' form used in clocks.
Important notes
Formatted output can contain non-breaking spaces and, in some locales, non-Latin digits, so parseFloat or === on the result is unreliable; keep the original number around.
Locale data ships with the runtime, so an unsupported or misspelled tag quietly falls back to a supported one, and a minimal-ICU build may effectively only support English.
Common mistakes
Calling toLocaleString() with no locale and then asserting the exact string in a test: a machine configured for German produces '1.234,5' instead of '1,234.5', so the test fails depending on where it runs.
Assuming style: 'percent' just appends a % sign: it multiplies by 100 first, so passing 25 for '25 percent' renders '2,500%'; pass 0.25 instead.
Using style: 'currency' without a currency code: the call throws a TypeError about a required currency code, so the whole render blows up rather than showing an unformatted number.
Try it yourself
Change, predict, then run
In a browser console, write formatPrice(amount, locale, currency) that returns amount.toLocaleString(locale, { style: 'currency', currency }), then log it for 1999.5 with ('en-US','USD'), ('de-DE','EUR'), and ('en-US','JPY'). Explain which result has no decimal places and what decided that.
Open the JavaScript workspaceCheck your understanding
A cart total of 1234.5678 is shown with total.toLocaleString('en-US') and appears on the page as '1,234.568'. What has happened to the value in the variable total?
- Nothing: toLocaleString returned a new string, and the rounding exists only in that string
- It was rounded to three decimals in place, so later math uses 1234.568
- It was converted to a string, so later arithmetic on it yields NaN
- It was rounded to the nearest cent because the value is being displayed as money
Show answer
toLocaleString reads the number and returns a fresh string; numbers are immutable primitives, so total still holds 1234.5678. Option 2 is tempting because the rounding is real, the decimal style does default to a maximum of three fraction digits, but that rounding happens inside the formatter and never touches the variable.