Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Stdlib

Culture formatting — ToString(format, culture)

string value.ToString(format, culture) · app.DefaultCulture = "sv-SE"

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.

stable5 examples compiled by CIculturelocaleformatcurrency

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:

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#

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#

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:

KindFormatsExample (sv-SE)
NumberN F D (+ digits: N2, F0)1 234,56
CurrencyC (+ digits)1 234,56 kr
PercentP (+ digits)12,5 %
Date/timed D t T g G M Y2024-01-05 · fredag 5 januari 2024
Custom numeric#,##0.00, 0.001 234,56
Custom dateyyyy-MM-dd, dd/MM/yyyy, MMMM d2024-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.

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#

Format money and a percentage for an explicit culture:

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:

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):

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:

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]:

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#

Related

Encoding — Base64, URL, HTML

Encode and decode text — Base64, URL percent-encoding, and HTML escaping — with the C#-faithful spellings…

DateTime

A date and time. It is a wall-clock value, not an instant on a timeline, so it is never shifted by anybody's timezone…