Skip to content

Format helpers ​

Seven helpers that turn a value into something a reader sees, in their language. Every formatter is memoized, because these run inside table and chart renders.

Import ​

ts
import {
  activeLocale,
  formatCompact,
  formatCurrency,
  formatCurrencyCompact,
  formatDate,
  formatNumber,
  kpiCurrencyFormatter,
} from '@shared/utils/format'

Every function takes an optional trailing locale. Leave it out and you get the app's active one.

activeLocale ​

ts
function activeLocale(override?: AvailableLocale): AvailableLocale

The locale the others will format in. The override exists so one component can pin a language — the invoice module does, because its PDF fonts cannot draw Eastern Arabic numerals.

formatDate ​

ts
function formatDate(date: DateInput, opts?: FormatDateOptions): string

type DateInput = Date | number | string
type DateFormat = 'short' | 'medium' | 'long' | 'full'

format defaults to 'medium'. A string or a timestamp is accepted as well as a Date, which matters because a persisted date comes back as an ISO string.

It does not guard the input. An unparseable string reaches date-fns and throws RangeError: Invalid time value, which in a template takes the whole component down with it. Check the value before you format one a user typed — the example above does, which is why editing the date field there never blanks the preview.

formatNumber ​

ts
function formatNumber(value: number, opts?: Intl.NumberFormatOptions, locale?: AvailableLocale): string

A plain number in the active locale: 1,234.56 in en, 1.234,56 in de, and 1,234.56 in ar — Western digits, because that is what the locale's Intl tag asks for.

opts is passed straight to Intl.NumberFormat, so { minimumFractionDigits: 2 } and friends work.

formatCurrency ​

ts
function formatCurrency(value: number, currency: string, locale?: AvailableLocale): string
ts
formatCurrency(1234.5, 'EUR') // '€1,234.50' in en, '1.234,50 €' in de

The currency is explicit and required. The locale decides grouping and where the symbol goes; it does not decide which money this is. Those are two different questions, and conflating them is how a German page ends up claiming a dollar figure is euros.

formatCompact ​

ts
function formatCompact(value: number, locale?: AvailableLocale): string

2.3M in en, 2,3 Mio. in de. For a KPI tile where the exact figure is not the point.

formatCurrencyCompact ​

ts
function formatCurrencyCompact(value: number, currency: string, locale?: AvailableLocale): string

$2.3M in en, 2.3 مليون US$ in ar.

One Intl call covers the whole figure. Writing a literal $ in front of a locale-compact number mixes two languages in one string, and puts the symbol on the wrong side in half of them.

kpiCurrencyFormatter ​

ts
function kpiCurrencyFormatter(
  rowValues: number[],
  currency?: string,
  locale?: AvailableLocale,
): (value: number) => string

Returns the formatter for one row of KPI tiles: compact when any figure in the row needs it, grouped when none does.

ts
const format = kpiCurrencyFormatter([2_300_000, 572_092, 48_120], 'USD')

format(2_300_000) // '$2.3M'
format(572_092) // '$572.1K'

That is the point of it. Formatting each tile independently puts $2.3M next to $572,092, and the row reads as though the two were measured differently. currency defaults to USD.

Counts and percentages stay on formatNumber — this is for money.