# How fmtlib Handles Locale-Dependent Formatting with Thousands Separators

> Discover how fmtlib uses the 'n' format specifier and std::locale for locale-dependent number formatting with thousands separators. Learn about digit grouping rules.

- Repository: [Hello World Foundation/fmt](https://github.com/fmtlib/fmt)
- Tags: how-to-guide
- Published: 2026-09-05

---

**`fmt` formats numbers according to a `std::locale` when the locale-aware `'n'` format specifier is used, wrapping the locale in a lightweight `locale_ref` and applying digit grouping rules via the `digit_grouping` class.**

The `fmt` library provides efficient, type-safe formatting while avoiding the bloat of standard C++ iostreams. When you require **locale-dependent formatting with thousands separators**, the library employs a specialized three-stage pipeline that lazily loads locale data only when the `'n'` specifier is requested, minimizing compile-time overhead from the `<locale>` header.

## The Three-Stage Locale Formatting Pipeline

The implementation avoids pulling the heavy `<locale>` header into every translation unit by deferring locale-specific operations until a localized format specifier triggers them.

### Stage 1: Lightweight Locale Abstraction via `locale_ref`

At the entry point, `fmt` wraps the `std::locale` in a `locale_ref` object. This lightweight wrapper, implemented in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) around line 2102, stores a reference to the locale without forcing template instantiations that depend on standard locale facilities. The `locale_ref` propagates through the formatting pipeline, carrying the locale context to downstream formatting functions without introducing heavyweight dependencies in header files.

### Stage 2: Extracting Thousands Separators with `thousands_sep_impl`

When localized number formatting is required, the library calls `thousands_sep_impl`, defined in `src/format.cc` at line 45. This template function accesses the `std::numpunct` facet from the wrapped locale to retrieve two critical pieces of data:

- The **thousands separator character** (e.g., `,` for US English, `.` for German, or a non-breaking space for French)
- The **grouping pattern** (a `std::string` specifying digit group sizes, such as `"3"` for standard thousands or `"3;2"` for the Indian numbering system)

The function returns a `thousands_sep_result<Char>` structure containing these values. This result is cached inside a `digit_grouping` object, constructed in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) between lines 2112 and 2125, ensuring the locale facet lookup happens only once per formatting operation.

### Stage 3: Applying Digit Grouping in `digit_grouping::put`

The actual insertion of separators occurs in `digit_grouping::put`, implemented in [`include/fmt/format-inl.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format-inl.h) from lines 2160 to 2189. This method walks the number's digits from least-significant to most-significant, inserting the separator character whenever the grouping pattern mandates a boundary. If the locale specifies the classic "C" locale (the default), the grouping pattern is empty and no separators are inserted.

## Using Locale-Aware Formatting in Practice

To format numbers with thousands separators, use the `{:n}` specifier combined with `fmt::locale`. The default `std::locale::classic()` produces unlocalized output, while specific locales inject their native separators automatically:

```cpp
#include <fmt/format.h>
#include <locale>

int main() {
    // Classic (C) locale – no separator.
    fmt::print("{:n}\n", 1234567);               // → 1234567

    // US English locale – comma as thousands separator.
    fmt::print(fmt::locale("en_US.UTF-8"), "{:n}\n", 1234567);
    // → 1,234,567

    // German locale – period as thousands separator.
    fmt::print(fmt::locale("de_DE.UTF-8"), "{:n}\n", 1234567);
    // → 1.234.567

    // French locale – non‑breaking space as separator.
    fmt::print(fmt::locale("fr_FR.UTF-8"), "{:n}\n", 1234567);
    // → 1 234 567
}

```

The `fmt::locale` helper constructs a `locale_ref` from the `std::locale` and forwards it into the formatting functions, enabling the three-stage pipeline described above.

## Key Source Files and Implementation References

The locale-dependent formatting logic spans three critical files in the `fmtlib/fmt` repository:

- **[`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h)** – Contains the `locale_ref` class definition, the `digit_grouping` constructor (lines 2112–2125), and the public API entry points for `format` and `vformat`.

- **`src/format.cc`** – Houses `thousands_sep_impl` (line 45) and `decimal_point_impl`, which extract the separator characters and grouping patterns from the `std::numpunct` facet.

- **[`include/fmt/format-inl.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format-inl.h)** – Implements `digit_grouping::put` (lines 2160–2189), the low-level algorithm that inserts thousands separators according to the locale's digit grouping rules.

## Summary

- **`fmt` employs a lazy-loading architecture** where `locale_ref` wraps `std::locale` without pulling `<locale>` into every translation unit.
- **The `'n'` format specifier** triggers locale-aware formatting, extracting separator characters and grouping patterns via `thousands_sep_impl` in `src/format.cc`.
- **Digit grouping logic** resides in `digit_grouping::put` within [`include/fmt/format-inl.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format-inl.h), processing numbers from least-significant to most-significant digit.
- **Default behavior** uses `std::locale::classic()`, producing no separators; specific locales enable comma, period, or space separators automatically.

## Frequently Asked Questions

### How do I enable thousands separators in fmtlib?

Use the `n` format specifier inside your format string (e.g., `{:n}`). To apply locale-specific separators, pass a `fmt::locale` object as the first argument to `fmt::print` or `fmt::format`, such as `fmt::format(fmt::locale("en_US.UTF-8"), "{:n}", 1234567)`.

### What is the default locale behavior in fmtlib?

By default, `fmt` uses `std::locale::classic()` (the "C" locale), which results in no thousands separators being inserted. The number is formatted as a plain digit string regardless of the `n` specifier unless you explicitly provide a different locale via `fmt::locale`.

### Where does fmtlib store the thousands separator character?

The separator character and grouping pattern are stored in a `thousands_sep_result<Char>` structure returned by `thousands_sep_impl`. This result is cached within the `digit_grouping` class, instantiated in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) and populated from the `std::numpunct` facet defined in `src/format.cc`.

### Does fmtlib require the `<locale>` header for all translation units?

No. `fmt` avoids including `<locale>` in headers through the `locale_ref` abstraction. The heavy locale headers are only processed in `src/format.cc` where `thousands_sep_impl` is defined, keeping compile times minimal for code that does not use locale-dependent formatting.