Convert a number to a string in the format appropriate for the specified locale (toLocaleString)

Number.prototype.toLocaleString() gives you a language-aware string version of a number. It applies the rules for decimal separators, groupings, scripts, currency placement, percent signs, units, and more for the locale you name—or the user’s default if you don’t. Under the hood, modern engines route this to the internationalization APIs so you get consistent, standards-driven behavior across browsers and Node.

// Same number, three locales:
const n = 1234567.89;

n.toLocaleString("en-US"); // "1,234,567.89"
n.toLocaleString("de-DE"); // "1.234.567,89"
n.toLocaleString("hi-IN"); // "12,34,567.89"

Core signature and default behavior

Call toLocaleString([locales[, options]]). Omit both arguments to format with the runtime’s default locale and its default number style. With Intl.NumberFormat available (which it is in all current environments), toLocaleString delegates the work to a formatter configured from your locales and options.

(1234.5).toLocaleString();         // Uses the environment’s default locale
(1234.5).toLocaleString("fr-FR");  // "1 234,5" (thin non-breaking space)

Locales, scripts, and numbering systems

You pass locales using BCP 47 tags like "en-US" or "ar-EG". You can also request a numbering system (the digits themselves) via the Unicode -u-nu-… extension. That lets you keep French punctuation with Latin digits or, say, Arabic punctuation with Arabic-Indic digits. The formatter negotiates the closest supported match.

const x = 123456.789;

// Arabic locale with Arabic-Indic digits:
x.toLocaleString("ar-EG");                    // "١٢٣٬٤٥٦٫٧٨٩"

// Arabic locale but force Latin digits via the "nu" extension:
x.toLocaleString("ar-EG-u-nu-latn");          // "123,456.789"

// Chinese (Simplified) with Han decimal digits:
x.toLocaleString("zh-Hans-CN-u-nu-hanidec");  // "一二三,四五六.七八九"

The options object, in practice

Most of what you want comes from the options parameter. You’ll use it for style, rounding, grouping, and notation. Everything shown here maps to the same knobs you’d find on Intl.NumberFormat, since that’s what powers toLocaleString.

const n = 98765.4321;

// Currency: Pick a currency and the locale handles symbol and placement.
n.toLocaleString("en-GB", { style: "currency", currency: "GBP" }); // "£98,765.43"
n.toLocaleString("de-DE", { style: "currency", currency: "EUR" }); // "98.765,43 €"

// Percent: Scales by 100, adds the percent mark using locale rules.
(0.1234).toLocaleString("en-US", { style: "percent", maximumFractionDigits: 1 }); // "12.3%"

// Unit: Pairs the number with a unit using localized labels.
(88).toLocaleString("pt-PT", { style: "unit", unit: "kilometer-per-hour" }); // "88 km/h"

// Significant digits vs fraction digits:
n.toLocaleString("en-US", { maximumSignificantDigits: 3 }); // "98,800"
n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); // "98,765.43"

Grouping control and compact notation

Thousands separators vary by locale, but you can influence grouping. Beyond simple true/false, modern engines support "min2", "auto", and "always" to express when to show group separators. Compact notation shortens big numbers like “2.3K” or “1.2M”. Check your target browsers for the newest forms.

const v = 1200;

// Grouping behavior:
v.toLocaleString("en-US", { useGrouping: "always" }); // "1,200"
v.toLocaleString("en-US", { useGrouping: "min2"    }); // "1,200" (but tiny numbers may skip grouping)
v.toLocaleString("en-US", { useGrouping: false     }); // "1200"

// Compact notation:
(2300).toLocaleString("en-US", { notation: "compact" }); // "2.3K"
(1250000).toLocaleString("en-US", { notation: "compact" }); // "1.3M"

Rounding, sign handling, and trailing zeros

toLocaleString rounds according to the options you set. Recent Intl revisions let you pick a rounding mode and how to show trailing zeros, plus how to display signs for positive values. These controls are crucial for financial and scientific UIs where tie-breaking or padding matters. Support is broad in modern engines.

const x = 1.005;

// Rounding mode (e.g., bankers’ rounding vs away from zero):
x.toLocaleString("en-US", {
  maximumFractionDigits: 2,
  roundingMode: "halfEven" // ties to even
}); // "1.00"

// Trailing zeros (keep vs trim):
(12).toLocaleString("en-US", {
  minimumFractionDigits: 2,
  trailingZeroDisplay: "stripIfInteger"
}); // "12"

// Sign display:
(42).toLocaleString("en-US", { signDisplay: "always" });     // "+42"
(0).toLocaleString("en-US",  { signDisplay: "exceptZero" }); // "0"

Currency specifics that often trip people up

When style: "currency" is used, you must also supply a valid currency code like "USD" or "JPY". The locale determines symbol placement and spacing. Currencies without minor units (like JPY) will round to whole numbers by default. For accounting displays, set currencySign: "accounting" to get parentheses for negatives where appropriate.

(1234.5).toLocaleString("ja-JP", {
  style: "currency",
  currency: "JPY"
}); // "¥1,235"

(-1200).toLocaleString("en-US", {
  style: "currency",
  currency: "USD",
  currencySign: "accounting"
}); // "(\$1,200.00)"

Exponential, engineering, and “plain” formats

If you want scientific output, use notation: "scientific". For engineering notation (exponent is a multiple of 3), use "engineering". If you want regular numbers, keep "standard". You still get locale-aware digits and separators either way.

const y = 1234567;

y.toLocaleString("en-US", { notation: "scientific", maximumFractionDigits: 2 });  // "1.23E6"
y.toLocaleString("en-US", { notation: "engineering", maximumFractionDigits: 2 }); // "1.23E6"
y.toLocaleString("de-DE", { notation: "scientific", maximumFractionDigits: 2 });  // "1,23E6"

Performance and reuse: when to prefer Intl.NumberFormat

If you format many values in a loop, construct an Intl.NumberFormat once and reuse its .format function. toLocaleString creates a fresh formatter each time; the reusable formatter reduces overhead and avoids surprises from ambient defaults changing mid-stream. The end result is the same string.

// Reusable formatter:
const fmt = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 2
}).format;

for (const amount of [12, 3456.78, 0.9]) {
  console.log(fmt(amount)); // "$12.00", "$3,456.78", "$0.90"
}

// toLocaleString is convenient for one-offs:
(3456.78).toLocaleString("en-US", { style: "currency", currency: "USD" });

Inspect what actually happened with resolvedOptions()

Locales are negotiated. Options are normalized. If you ever wonder which settings were ultimately applied, build a formatter, then inspect resolvedOptions(). It reports the concrete locale, numbering system, style, notation, and the rounding and grouping decisions the engine selected.

const nf = new Intl.NumberFormat("ar", {
  style: "decimal",
  maximumFractionDigits: 1,
  useGrouping: "min2"
});

console.log(nf.resolvedOptions());
/*
{
  locale: "ar",
  numberingSystem: "arab",
  style: "decimal",
  notation: "standard",
  useGrouping: "min2",
  maximumFractionDigits: 1,
  // ... more resolved fields
}
*/

Practical recipes you’ll reuse

When you need a plain decimal with predictable padding, set both minimumFractionDigits and maximumFractionDigits. For “friendly” dashboards, combine compact notation with maximumFractionDigits. To align logs without thousands separators on small values, try useGrouping: "min2" so grouping appears only when there are at least two groups. These approaches keep output stable and readable across locales.

// Fixed two decimals, locale punctuation:
(12).toLocaleString("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); // "12.00"

// Compact dashboard numbers:
(987654).toLocaleString("en-US", { notation: "compact", maximumFractionDigits: 1 });   // "987.7K"

// Grouping only when it helps readability:
(1200).toLocaleString("en-US", { useGrouping: "min2" });     // "1,200"
(120).toLocaleString("en-US",  { useGrouping: "min2" });     // "120"

Edge cases and gotchas worth testing

NaN and ±Infinity stringify according to locale but remain their tokens. Negative zero keeps its sign if the sign is visible. Currency requires a valid ISO 4217 currency code; leaving it out with style: "currency" is an error. Not all options exist in very old runtimes, so if you target legacy browsers, test features like "notation" or advanced useGrouping values. The internationalization specification is the source of truth for option names and defaults.

Number.NaN.toLocaleString("en-US");     // "NaN"
Infinity.toLocaleString("en-US");       // "∞" (where supported) or "Infinity"
(-0).toLocaleString("en-US", { signDisplay: "always" }); // "-0"
(10).toLocaleString("en-US", { style: "currency" });     // RangeError: currency required

References

SourceWhat it covers
MDN: Number.prototype.toLocaleString()Method behavior, delegation to Intl.NumberFormat, examples, and notes on locales and options.
MDN: Intl.NumberFormatAll number-formatting options (style, notation, signDisplay, currencySign, unitDisplay, useGrouping, etc.), examples, and browser support.
MDN: Intl.NumberFormat() constructorConstructor usage, notation values, and how options map to output.
MDN: Intl.NumberFormat.prototype.resolvedOptions()Inspecting the finalized locale and options at runtime.
ECMA-402: Internationalization API SpecificationFormal semantics of Intl.NumberFormat, option names, defaults, and rounding behavior.
V8 blog: Intl.NumberFormatPractical guidance on using Intl.NumberFormat vs toLocaleString, and notes on performance and reuse.
Can I use: useGrouping string valuesSupport for "min2", "auto", and "always" values for useGrouping.
MDN: Intl.Locale.prototype.numberingSystemUsing -u-nu-… locale extensions to request a specific numbering system.