Docs

Currency, percent, date, and time formatting reference. Native Intl + a tiny dayjs-token parser. Storage stays canonical; locale + timezone applied at display only.

Formatting

LCAP renders numbers and timestamps via the browser's native Intl APIs plus a small dayjs-compatible token parser — no heavy date library in the SDK base bundle. Storage stays canonical (plain JSON numbers, ISO-8601 UTC strings); display formatting layers on top through the annotation's ui_config_json keys.

This page documents the formatting behavior, the dayjs token vocabulary, and the locale resolution chain.

Locale resolution

The resolver picks a locale via:

  1. Caller-supplied locale prop (rare — host apps rarely set this).
  2. navigator.language (the browser's negotiated locale).
  3. 'en-US' fallback for SSR / Node test environments.

A planned follow-on will wire the tenant's BrandingConfig.locale as the highest-priority source.

Currency

@ui/component = "currency" plus @ui/currency_code (default USD):

AnnotationStored valueDisplay (en-US)
{ "@ui/component": "currency" }1234.56$1,234.56
{ "@ui/component": "currency", "@ui/currency_code": "EUR" }1234.56€1,234.56
{ "@ui/component": "currency", "@ui/currency_code": "JPY" }1234¥1,234

CLDR drives the per-currency intrinsic decimal places — JPY zero, USD/EUR two, BHD three — so JPY renders without a decimal portion automatically.

KeyEffect
@ui/decimal_placesOverride CLDR. decimal_places: 4 on USD → $1,234.5600.
@ui/use_separatorsfalse removes the thousands separator: $1234.56.

Percent

@ui/component = "percent". Storage stays decimal — the display layer multiplies by 100:

Stored valueDisplay
0.077%
0.07 (with @ui/decimal_places: 2)7.00%
1.5 (with explicit min: 0, max: 5)150%

The shipped zod refinement defaults percent fields to [0, 1] storage range. Annotations with explicit min / max override that — useful for "growth %" cases that exceed 1.0.

Timestamps

Storage is always ISO-8601 UTC. Display formatting reads two annotation knobs:

  • @ui/date_format — dayjs-compatible token template.
  • @ui/timezone — IANA tz (e.g. "America/Los_Angeles").

Without @ui/date_format, the resolver falls back to the locale's short date — 04/25/2026 for en-US. @ui/include_time appends 12:00 PM when set.

Token vocabulary

The parser supports the common dayjs / date-fns subset. Unrecognized tokens pass through as literal characters.

TokenExample output (en-US)Intl source
YYYY / yyyy2026year: 'numeric'
YY / yy26year: '2-digit'
MMMMAprilmonth: 'long'
MMMAprmonth: 'short'
MM04month: '2-digit'
M4month: 'numeric'
DD / dd25day: '2-digit'
D / d25day: 'numeric'
HH14hour: '2-digit', hour12: false
H14hour: 'numeric', hour12: false
hh02hour: '2-digit', hour12: true
h2hour: 'numeric', hour12: true
mm30minute: '2-digit'
ss45second: '2-digit'
apmdayPeriod (lowercased)
APMdayPeriod (uppercased)

Literal characters in the format string (commas, dashes, slashes, the T separator, etc.) pass through verbatim. So "yyyy-MM-dd HH:mm" renders as 2026-04-25 19:30.

Timezone behavior

@ui/timezone is applied to every token in the format template. Cross-day boundaries shift correctly:

Stored:        2026-04-26T02:30:00Z
@ui/timezone:  America/Los_Angeles
@ui/date_format: yyyy-MM-dd HH:mm
Displays:      2026-04-25 19:30

(02:30 UTC on the 26th is 19:30 PT on the 25th.)

Numbers (uncomponented)

@ui/component = "number" with no further keys:

AnnotationStoredDisplay
(none)1234567.891,234,567.89
{ "@ui/use_separators": false }1234567.891234567.89
{ "@ui/decimal_places": 0 }1234.561,235

The zod refinement honors @ui/decimal_places + @ui/min + @ui/max automatically.

Boolean

mode="display" renders booleans as Yes / No. Future locale-aware variants (oui/non, sí/no) would attach a @ui/true_label / @ui/false_label pair if the need materializes.

File reference

mode="display" for field_type = "file_ref" renders filename (size) with rounded-KB. The Page Designer Card block (rolling out) wraps <SmartField /> with the existing <FilePayloadView /> so image previews + download links work inline.

Cross-references