# Culture formatting — ToString(format, culture)

> Format numbers, currency, percentages and dates for a declared culture — `total.ToString("C", "sv-SE")` → `1 234,56 kr`. A per-call culture overrides; `app.DefaultCulture` sets an app-wide default. Supported formats run in the browser, byte-identical to the server; everything else runs server-side.

<!-- id: stdlib-culture-formatting · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/culture-formatting/ -->

## Summary        {#summary}
Format a number or date for a **declared culture**, spelled exactly as C#'s `IFormattable.ToString(format, provider)` —
the provider is a BCP-47 culture token (`"sv-SE"`, `"de-DE"`), not an ambient setting:

```osy syntax
var price = total.ToString("C", "sv-SE");     // "1 234,56 kr"  — Swedish krona, space grouping, comma decimal
var pct   = rate.ToString("P1", "en-US");     // "12.5 %"
var when  = order.CreatedAt.ToString("D", "de-DE");   // "Freitag, 5. Januar 2024"
```

The culture must be **declared** (a `cultures { }` set; the platform ships a default pack, so `sv-SE`/`de-DE`/… work
with no ceremony). A per-call culture is the override; `app.DefaultCulture` sets the app-wide default so a culture-less
`total.ToString("C")` still formats for it.

## Signature      {#signature}
```osy syntax
string  value.ToString(string format)                  // the app-default culture (else invariant)
string  value.ToString(string format, string culture)  // an explicit culture — the per-call override
// declared once, app-wide:
app.DefaultCulture = "sv-SE";
// per-viewer (on the principal entity) + the read:
[PreferredCulture] Culture Locale;
string cur = Session.CurrentCulture;                   // the current viewer's effective culture token
```

## Description    {#description}
**The culture is explicit, never ambient.** There is no "current culture" read from the machine or the request — a
value formats for the culture you name, or for the app default. This is what makes a price render the same on the
server and in every browser.

**Declared, closed set.** A culture token on a format call must belong to the app's declared `cultures { }` set. The
platform ships a default pack (`en-US`, `en-GB`, `sv-SE`, `de-DE`, `fr-FR`, `es-ES`, `it-IT`, `nl-NL`, `pt-BR`,
`ja-JP`, `zh-CN`); an app can vendor the block to curate. A token outside the set is a **compile error** (with a
"did you mean").

**Supported formats (run in the browser, no round trip).** Over a `Decimal`, `Int32`, `Double` (fixed specifiers only),
`DateTime`, `DateOnly` or `TimeOnly`:

| Kind | Formats | Example (sv-SE) |
|---|---|---|
| Number | `N` `F` `D` (+ digits: `N2`, `F0`) | `1 234,56` |
| Currency | `C` (+ digits) | `1 234,56 kr` |
| Percent | `P` (+ digits) | `12,5 %` |
| Date/time | `d D t T g G M Y` | `2024-01-05` · `fredag 5 januari 2024` |
| Custom numeric | `#,##0.00`, `0.00` | `1 234,56` |
| Custom date | `yyyy-MM-dd`, `dd/MM/yyyy`, `MMMM d` | `2024-01-05` |

The client reproduces .NET exactly — the culture's separators (including a non-breaking-space group separator and a
`−` minus), currency symbol and placement, month/day names (with the **genitive** forms .NET selects when a day number
is adjacent, e.g. German `5. Juni` vs standalone `Jun`), and AM/PM.

A **`Double`** formats client-side too for the fixed specifiers (`N`/`F`/`C`/`P`) — the client rounds the value's
*exact* decimal expansion (a double is a terminating decimal), reproducing .NET including its type-specific rounding
(a double rounds half-to-even, `(2.5).ToString("F0")` → `"2"`, where a `Decimal` rounds half-away-from-zero). Prefer
`Decimal` for money regardless — it is exact end to end.

**Everything else runs on the server** (correct, one round trip): **parsing** a string back to a value; the numeric
`E`/`G`/`R`/`X` specifiers, a *custom pattern over a double*, and a double's plain `.ToString()` — these need
shortest-round-trip / 15-digit rounding that isn't reproduced client-side; non-Gregorian date calendars/eras under a
culture; and any format built at runtime.

**App default.** `app.DefaultCulture = "sv-SE";` makes a culture-**less** `value.ToString("C")` format for `sv-SE`
everywhere — server and client — without repeating the culture at each call. A per-call culture still overrides it. With
no default declared, a culture-less format is culture-neutral (invariant), unchanged.

**Per-viewer culture.** Give the principal entity a `Culture` property marked `[PreferredCulture]`, and a culture-**less**
`value.ToString("C")` renders in *the current signed-in user's* culture. `Culture` is a value-kind that stores a BCP-47
token (`"de-DE"`) — a picker or a plain string sets it — validated to be a real culture when written. The resolution
order is: **a per-call culture > the viewer's `[PreferredCulture]` > `app.DefaultCulture` > invariant**. It resolves at
runtime and, like every other supported format, runs **in the browser** (the viewer's culture is served with the app's
metadata, so there is no round trip); a viewer whose preferred culture is not one the app declared falls back to the
app default.

**A number typed into a box follows the same order.** `NumberField` and `DecimalField` — and any `Field` bound to a
numeric member — render and accept their digits in the viewer's effective culture, not the browser's. So under
`app.DefaultCulture = "sv-SE"` the box shows `12,5` and takes `12,5`, `1 234,50` and `-12,5`, and the member behind it
still holds an exact `decimal`. A number written with another culture's decimal mark is not a number: `12.5` there
answers what an empty box answers, rather than being read as `125` or as `12.5` — either of which would be a wrong
number nobody could see.

`min`/`max` on the numeric controls mark the box invalid the moment the value falls outside, so the reader sees it as
they type rather than when the save is refused.

**Reading the viewer's culture.** `Session.CurrentCulture` returns that effective token — the viewer's `[PreferredCulture]`
if set, else `app.DefaultCulture`, else `""` (invariant) — as a value, so you can branch on it (`if (Session.CurrentCulture
== "de-DE") …`) or pass it on. It is the author-facing sibling of `Session.CurrentUser`, resolves the same way as the
implicit formatting above, and runs in the browser with no round trip.

**Parsing (the inverse).** `Convert.ToDecimal(s, "sv-SE")` reads a number written in a culture (`"1 234,56"` → `1234.56`),
returning `0` for a string it can't parse (the `Convert.To*` contract). It runs **client-side too**, byte-identical to
the server, over a defined profile: leading/trailing whitespace, one sign, group separators, and a decimal separator.
Outside that profile — a currency symbol in the string, parentheses for a negative, an exponent — parses server-side (a
round trip). A plain `Convert.ToDecimal(s)` (no culture) already parses invariantly in the browser.

`Convert.ToInt(s, "de-DE")` is the integer twin — it runs **client-side too**, over the same profile minus a decimal
point *and* minus a trailing sign (a `"1234-"` is a value for `ToDecimal` but `0` for `ToInt`). A value outside the
32-bit integer range parses to `0`, like every unparseable string.

**Parsing a date exactly.** `DateTime.ParseExact(s, "yyyy-MM-dd", "sv-SE")` reads a date/time written to a specific
pattern — the inverse of the date formatter. It is STRICT: a string that does not match the pattern *throws* (unlike the
number parses, which return `0`), exactly as .NET's `DateTime.ParseExact` does. It runs **client-side** for a literal
custom pattern that carries a full date — a 4-digit year (`yyyy`), a month, and a day-of-month — plus optional time,
using the supported tokens `y M d H h m s t` with separators and quoted literals; the field widths are strict (`MM`
wants two digits, `"2024-6-15"` is rejected), month/day names honour the culture (`MMMM`/`dddd`, genitive included), and
a day-name token must agree with the date. A **single-char standard specifier** (`"d"`, `"D"`), a **2-digit year**
(`yy` — its value depends on the culture's century window), or a **dynamic** pattern/culture stays server-side. The
lenient, multi-pattern `DateTime.Parse(s)` (no explicit pattern) also stays server-side.

## Examples       {#examples}

Format money and a percentage for an explicit culture:

```osy title="currency + percent" test app=stdlib-culture-formatting
string Price(decimal amount) {
  return amount.ToString("C", "sv-SE");     // "1 234,56 kr"
}

string Rate(decimal ratio) {
  return ratio.ToString("P1", "en-US");     // "12.5 %"
}
```

Format a date, standard and custom:

```osy title="dates" test app=stdlib-culture-formatting
string LongDate(DateTime when) {
  return when.ToString("D", "de-DE");        // "Freitag, 5. Januar 2024"
}

string Iso(DateTime when) {
  return when.ToString("yyyy-MM-dd", "sv-SE");
}
```

Parse a number a user typed in their culture (runs in the browser):

```osy title="culture parse" test app=stdlib-culture-formatting
decimal ReadPrice(string entered) {
  return Convert.ToDecimal(entered, "sv-SE");    // "1 234,56" → 1234.56, "" → 0
}

int ReadQuantity(string entered) {
  return Convert.ToInt(entered, "de-DE");        // "1.234" → 1234, "12,5" → 0 (no decimal point)
}

DateTime ReadDate(string entered) {
  return DateTime.ParseExact(entered, "dd/MM/yyyy", "en-GB");   // "25/12/2024"; a mismatch throws
}
```

Set an app-wide default so a culture-less format still localises:

```osy title="app default culture" test app=stdlib-culture-formatting
app.DefaultCulture = "sv-SE";

string LocalPrice(decimal amount) {
  return amount.ToString("C");               // uses sv-SE → "1 234,56 kr"
}
```

Let each viewer see prices in their own culture — mark a `Culture` property on the principal `[PreferredCulture]`:

```osy title="per-viewer culture" test app=stdlib-culture-formatting-viewer
app.DefaultCulture = "sv-SE";

[Principal] entity Member {
  [Required, MaxLength(60)] string Email;
  [PreferredCulture] Culture Locale;         // a BCP-47 token, e.g. "de-DE"
}

string ViewerPrice(decimal amount) {
  return amount.ToString("C");               // renders in the signed-in member's Locale, else sv-SE
}

string ViewerCulture() {
  return Session.CurrentCulture;             // the effective token: the member's Locale, else "sv-SE"
}
```

## See also       {#see-also}
- [DateTime](https://osysharp.com/reference/types/datetime/) — the date/time values these formats render
- [Encoding — Base64, URL, HTML](https://osysharp.com/reference/stdlib/encoding/) — the other C#-faithful text surfaces
