Conventions

The same rules apply to every surface that renders money — tables, KPIs, receipts, exports.

RuleValueDetail
SymbolPrefix · alwaysThe symbol leads the number. We never trail. $1,234.56 — not 1,234.56 $.
Thousands separatorComma · period · spaceen-US uses comma, de-DE uses period, fr-FR uses space. Locale decides — code never hand-rolls.
Decimalmin 2 · max 2Two decimal places. No truncation, no overflow. JPY and other no-decimal currencies are the exception.
SignPositive bare · negative U+2212Positives carry no plus by default. Negatives render with Unicode minus (−), never hyphen-minus (-).
Zero$0.00Always rendered. Never $0, never an em-dash. Empty data uses a separate empty-state, not a fake zero.
Locale-aware rendering

Intl.NumberFormat does the heavy lifting. Pass a locale and a currency code; the symbol position, separator, and decimal precision follow.

CurrencyLocaleRenderedNotes
USDen-US$1,234.56Default product locale.
EURde-DE1.234,56 €Trailing symbol with non-breaking space.
GBPen-GB£1,234.56Same shape as USD.
JPYja-JP¥1,235No decimals — the rounding is Intl-native.
Helper

The product imports a single formatter. Components never call Intl.NumberFormat directly — they call this.

typescript
// Single source for currency rendering across the product.
export function formatCurrency(
  amount: number,
  locale: string = 'en-US',
  currency: string = 'USD'
): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(amount);
}

formatCurrency(1234.56);                  // "$1,234.56"
formatCurrency(1234.56, 'de-DE', 'EUR');  // "1.234,56 €"
formatCurrency(1234.56, 'en-GB', 'GBP');  // "£1,234.56"
formatCurrency(1234.56, 'ja-JP', 'JPY');  // "¥1,235"
Compact rendering

Use the full helper in tables and ledgers; use compact for hero KPIs and dashboards where space is tight and exactness less useful than scale.

Tables · ledgers
$2,408,210
Full precision. Two decimals when fractional.
Dashboards · KPIs
$2.4M
Compact short. One fractional digit max.
typescript
// Compact rendering for dashboards and KPIs.
export function formatCurrencyCompact(
  amount: number,
  locale: string = 'en-US',
  currency: string = 'USD'
): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    notation: 'compact',
    compactDisplay: 'short',
    maximumFractionDigits: 1
  }).format(amount);
}

formatCurrencyCompact(2408210);  // "$2.4M"
formatCurrencyCompact(982);      // "$982"
Edge cases
  • Trailing zeros stay. $1.00, not $1. Two decimals is the contract for fractional currencies.
  • Ranges use an en-dash with non-breaking spaces. $1,000 – $2,000. Both symbols repeat; the dash never wraps.
  • No ledger parentheses for negatives. ($240) is an accounting tradition we don’t carry — the Unicode minus (−$240) is clearer for non-accountants and matches our screen-reader output.
  • JPY and no-decimal currencies skip the fractional part. Intl handles this — don’t force minimumFractionDigits when the currency disagrees.
  • Cross-currency tables. Convert before rendering. Mixing $1,234.56 and ¥1,235 in the same column breaks the row scan.