# Formatting

Locale-aware presentation of artifact values

Canonical URL: https://docs.asterdoc.com/sdk/formatting/

`@asterdoc/format` presents structured Asterdoc values as localized text. Stored values remain unchanged. The same formatter can be passed to text, PDF, DOCX, and React renderers.

## Installation

```bash
npm install @asterdoc/format
```

The SDK also exports the formatter API:

```ts
import { createFormatter } from '@asterdoc/sdk'
```

## Create a formatter

Use a precise locale. The initial documented locales are `en-US`, `en-GB`, `de-DE`, `fr-FR`, and `ar-SA`.

```ts
import { createFormatter } from '@asterdoc/format'

const formatter = createFormatter({ locale: 'de-DE' })

formatter.formatNumber(1234567.89) // '1.234.567,89'
formatter.formatMoney({ amount: 1500.5, currency: 'EUR' }) // '1.500,50 €'
formatter.formatPercentage(8.25) // '8,25 %'
formatter.formatDate('2026-09-07') // '7. Sept. 2026'
```

Money always carries its currency. Percentages use percentage points, so `8.25` formats as `8.25%`. Address country is independent of document locale.

## Selection values

Booleans, single choices, multiple choices, and ratings go through the same formatter. Each call takes the options the field declares.

```ts
import { createFormatter } from '@asterdoc/format'

const formatter = createFormatter({ locale: 'en-US' })
const options = [
  { value: 'plumbing', label: 'Plumbing' },
  { value: 'roofing', label: 'Roofing' },
]

formatter.formatBoolean(true)                                   // 'Yes'
formatter.formatBoolean(true, { trueLabel: 'Agreed' })          // 'Agreed'
formatter.formatEnum('plumbing', { options })                   // 'Plumbing'
formatter.formatMultiselect(['plumbing', 'roofing'], { options }) // 'Plumbing and Roofing'
formatter.formatRating(4, { max: 5 })                           // '4 of 5'
formatter.formatRating(4, { display: 'value' })                 // '4'
```

| Kind          | Options                                                                                                                                                                       |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `boolean`     | `trueLabel`, `falseLabel` replace the locale's words                                                                                                                          |
| `enum`        | `options` (the declared `{ value, label }` list), `unknownOption`                                                                                                             |
| `multiselect` | the `enum` options, plus `listType` (`'conjunction'`, `'disjunction'`, `'unit'`; default `'conjunction'`) and `listStyle` (`'long'`, `'short'`, `'narrow'`; default `'long'`) |
| `rating`      | `max` (the top of the scale), `display` (`'scale'` or `'value'`; default `'scale'`)                                                                                           |

`UnknownOptionPolicy` decides what happens to a value no option declares: `'error'` (the default) refuses it, and `'value'` prints the raw value.

A multiselect joins its labels with the locale's own list conjunction, so `['plumbing', 'roofing']` prints as "Plumbing and Roofing" in `en-US` and "Plumbing und Roofing" in `de-DE`. Ratings print with their scale in the locale's words: "4 of 5", "4 von 5".

The standalone functions `formatBoolean`, `formatEnum`, `formatMultiselect`, and `formatRating` use the default `en-US` formatter. Each has a `safeFormat*` pair, and `formatValue('rating', 4, { max: 5 })` dispatches by kind.

Two `unsupported` outcomes have exported codes, so you can key a fallback off them:

* `MISSING_RATING_SCALE`: the rating declares no `max`, so it has no scale to print.
* `UNSUPPORTED_LIST_JOIN`: the runtime has no list conjunction for the locale.

```ts
import { MISSING_RATING_SCALE } from '@asterdoc/format'

const result = formatter.safeFormatRating(4)
if (!result.success && result.issues[0]?.code === MISSING_RATING_SCALE) {
  // print the plain number instead
}
```

## Safe results

Strict methods return text or throw `FormatError`. Safe methods distinguish missing, incomplete, invalid, unsupported, and unexpected failures:

```ts
const result = formatter.safeFormatMoney({ amount: 10 })
// { success: false, status: 'incomplete', issues: [...] }
```

A bad option is `invalid` with the code `invalid_options`, like a bad value: a malformed locale, an unknown timezone or calendar, or an Intl option out of range. `unsupported` means the input is right and the runtime or its resources fall short: a locale the runtime has no data for (`unsupported_locale`), a missing package message (`missing_message`), or a country with no address layout.

## Compose policy and overrides

Formatters are immutable. `compose()` and `withOverrides()` return independent instances.

```ts
const amountOnly = formatter.compose({
  money: { currencyDisplay: 'none' },
})

amountOnly.formatMoney({ amount: 12000, currency: 'USD' }) // '12,000.00'
```

`currencyDisplay: 'none'` drops the symbol and keeps the currency's fraction digits, unless you set digits yourself. For a PDF template that pre-prints the symbol, declare it on the layer instead; see [Artifact formatting](/sdk/renderers/pdf#artifact-formatting).

An override receives a delegate for the previous implementation:

```ts
const compact = formatter.withOverrides({
  organization(value, options, context) {
    context.delegate(value, options)
    return String(value.name)
  },
})
```

## In-progress documents

By default a value that has not been supplied renders blank. To mark it in a preview, pass a `progressive` policy to `render()`. `missing` sets the text for an absent value (default: an em dash) and `incomplete` the text for a composite value that is only partly supplied (default: an ellipsis). For a layer `{{fields.name}} / {{fields.total}}` with only `name` filled:

```ts
const preview = await draft.render({ progressive: {} })
// 'Ada / —'

const marked = await draft.render({ progressive: { missing: '___' } })
// 'Ada / ___'
```

React compositions use the `partial` option instead; see [@asterdoc/react](/sdk/react).

## Use one formatter for an artifact

```ts
import { createFormatter, a, createLayerRenderer } from '@asterdoc/sdk'

const formatter = createFormatter({ locale: 'fr-FR' })
const output = await a.form(formDefinition).fill(data).render({
  renderer: createLayerRenderer({ formatter }),
  layer: 'markdown',
})
```

Renderer configuration is explicit. Artifact metadata does not select a locale or formatter profile.

## Related

* [Renderers](/sdk/renderers)
* [Primitives](/sdk/primitives)
