# Showing dates and times to people

To show a date or a time to a person, format it with
`text.format(value, zone:, locale:, format:)`. Formatting needs three things
the server must never guess: the zone, the language and the format. The
call takes all three. The format is either built in or declared once as a
named type.

The language must be one your app supports, declared in `app.jadpo`
([Locales](locales.md)), so this example is a whole project:

```jadpo
application {}

locales {
    default: Locale.en_gb
    supported: [Locale.en_gb, Locale.fr_fr]
}
```

```jadpo
/// When an order was placed.
type OrderedAt = Instant {}
```

```jadpo
/// A date as "25 Oct 2026" in `Locale.en_gb`.
type ShortDate = TimeFormat {
    day: ComponentStyle.numeric
    month: ComponentStyle.short
    year: ComponentStyle.numeric
}
```

```jadpo
/// When an order was placed, shown to its customer.
type OrderPlaced = Output {
    placed: PresentationText
}
```

```jadpo
use shop/types { OrderPlaced, OrderedAt, ShortDate }

/// The order's placing, in the customer's zone and language.
action show_placed(ordered_at: OrderedAt, zone: Zone, reader_locale: Locale) -> OrderPlaced {
    return OrderPlaced {
        placed: text.format(ordered_at, zone: zone, locale: reader_locale, format: ShortDate)
    }
}
```

The zone decides the date, and the locale the words and their order: at
23:30 UTC on 25 October 2026 it is already the 26th in Paris.

```jadpo
test "the placing is shown in the customer's zone and language" {
    assert show_placed(
        "2026-10-25T09:30:00Z",
        Zone.europe_london,
        Locale.en_gb
    ).placed == "25 Oct 2026"
    assert show_placed(
        "2026-10-25T09:30:00Z",
        Zone.europe_paris,
        Locale.fr_fr
    ).placed == "25 oct. 2026"
    assert show_placed(
        "2026-10-25T23:30:00Z",
        Zone.europe_paris,
        Locale.fr_fr
    ).placed == "26 oct. 2026"
}
```

Every standard type is formatted for people by this one call,
`text.format(value, locale:)`. The time types' version also takes the
`zone:` and `format:` options described here. `temporal.format`, a name
you might expect, is `semantic.standard_operation_moved`, whose suggested
edit writes `text.format`. `temporal.format_relative`, below, keeps its
name.

## Where the words come from

The words come from Unicode CLDR 48, the standard data on how each
language writes dates and numbers, compiled into Jadpo. So the
interpreter, a native deployable and WebAssembly on Workers write the same
text, and nothing is read from the platform. `Locale` was made from the
same release, so every variant has data.

The calendar is always the Gregorian one, as `calendar: "gregory"` gives in
`Intl.DateTimeFormat`, so a Thai or Arabic reader of a `CalendarDate` sees
the same year the database holds. The digits are always the ones the locale
writes (`ar_eg`, `fa`, `bn`, `mr` and `ne` write their own). CLDR's narrow
no-break space before `PM` is written as a plain space, as browsers and
Node write it, so a test is written with the space it shows.

## Built-in formats

A built-in format is written `format: TimeFormat.date_time_long`. The
built-in formats are `date_`, `time_` and `date_time_`, each with `full`,
`long`, `medium` or `short`, CLDR's four lengths. Each is the locale's own
pattern for a date, a time, or both, joined with the locale's own words. In
`en_gb`, for 14:30:05 on Sunday 27 September 2026 in London, where the
clocks are on British Summer Time:

| Format | `en_gb` |
|---|---|
| `date_full` | Sunday, 27 September 2026 |
| `date_long` | 27 September 2026 |
| `date_medium` | 27 Sept 2026 |
| `date_short` | 27/09/2026 |
| `time_full` | 14:30:05 British Summer Time |
| `time_long` | 14:30:05 BST |
| `time_medium` | 14:30:05 |
| `time_short` | 14:30 |
| `date_time_full` | Sunday, 27 September 2026 at 14:30:05 British Summer Time |
| `date_time_long` | 27 September 2026 at 14:30:05 BST |
| `date_time_medium` | 27 Sept 2026, 14:30:05 |
| `date_time_short` | 27/09/2026, 14:30 |

`date_full` is the long date with the weekday, and `date_time_long` has
seconds, as `timeStyle: "long"` does. For a handful of locales (Japanese
and Korean `time_` formats, and the punctuation of a few `date_` formats)
the pattern isn't always the one `Intl.DateTimeFormat` writes: the two can
differ in punctuation, joining words, number widths or the padding of a
24-hour clock.

A zone's name comes from the zone and the instant: "BST" or "British Summer
Time" in summer, "GMT" or "Greenwich Mean Time" in winter. Where CLDR has no
name in the locale, it is the offset ("GMT+1", "UTC+1").

A hidden project on this page checks the table.

## Declared formats

A declared format, a type built on `TimeFormat` such as `ShortDate` above,
lists the parts it shows, each once, with a `ComponentStyle`. The locale
decides their order and punctuation, and whether the clock has 12 or 24
hours, as JavaScript's `Intl.DateTimeFormat` does:

| Part | Takes |
|---|---|
| `weekday` | `long`, `short`, `narrow` |
| `day`, `year` | `numeric`, `two_digit` |
| `month` | `numeric`, `two_digit`, `long`, `short`, `narrow` |
| `hour`, `minute`, `second` | `numeric`, `two_digit` |
| `zone_name` | `long` ("British Summer Time"), `short` ("BST") |

These are refused:

- a format with no parts (`type.temporal_format_components_empty`);
- a part twice (`type.temporal_format_component_duplicate`);
- a style the part doesn't take (`type.temporal_format_component_invalid`);
- a name that isn't a part (`type.unknown_setting`).

A format holds no value. `format:` names it written out, and it is never
the type of a field, a parameter or a variable, nor a value passed in
(`type.temporal_format_invalid`). The operation receives it as text, such
as `day:numeric month:short year:numeric` or `date_time_long`.

A declared format takes the locale's pattern for its parts, then sets each
part to its style:

- A month written as a name is taken from the long date pattern, or the
  medium one where that also writes the month as a name. A month written as
  a number is taken from the short pattern. `month: short` is that pattern
  with the abbreviated name.
- `numeric` leaves a number as wide as the locale writes it (`dd/MM/y`
  stays), and `two_digit` makes it at least two digits.
- A date and a time are joined with the words the locale uses for that
  month style ("at" for `long`, a comma for `short`).

So the output is the locale's own. It matches `Intl.DateTimeFormat` for
most locales and most options, but not all of them. Where `Intl` shortens
`01:59` to `1:59` for `hour: numeric minute: two_digit`, Jadpo writes the
locale's `01:59`, and a Japanese or Korean weekday beside a long date has
no brackets.

Parts no pattern holds together, such as a year with a day and no month, a
weekday and a year, a minute without an hour, or a month with a time, are
written one after another with a space: `27 2026`, `30:05`,
`September, 14:30`. `zone_name` on its own writes only the name ("British
Summer Time"), where `Intl` adds the date.

## `zone:` and `locale:`

Give `zone:` exactly when the value is an `Instant`
(`type.temporal_zone_required` otherwise). A `ZonedTime` is shown in its
own zone, and a `CalendarDate` has none, so `zone:` with either is
`type.temporal_option_unused`.

A `CalendarDate` has no clock time, so a `time_` or `date_time_` format, or
one showing `hour`, `minute`, `second` or `zone_name`, is
`type.temporal_format_requires_time`.

`locale:` is a `Locale` your app supports ([Locales](locales.md);
`type.temporal_locales_required`, `type.locale_unsupported`).

## Relative times: `temporal.format_relative`

For text such as "3 minutes ago", use
`temporal.format_relative(value, now: clock.now, zone:, locale:)`, for an
`Instant` or a `ZonedTime`, with `zone:` as for `text.format`. Its rules are
fixed, so two readers of the same value see the same words:

| `value` against `now` | Text, in `en_gb` |
|---|---|
| under a minute, either way | "just now" |
| under an hour | "4 minutes ago", "in 10 minutes" |
| the same date | "Today at 13:00" |
| the date before, or after | "Yesterday at 14:30", "Tomorrow at 14:30" |
| two to six days away | "Friday at 07:30" |
| anything else | "20 September 2026" |

```jadpo
application {}

locales {
    default: Locale.en_gb
    supported: [Locale.en_gb, Locale.fr_fr]
}
```

```jadpo
/// When a message was sent.
type SentAt = Instant {}
```

```jadpo
/// When a message was sent, as its reader reads it.
type SentText = Output {
    seen: PresentationText
}
```

```jadpo
use chat/types { SentAt, SentText }

/// When a message was sent, against now in London.
action show_sent(sent_at: SentAt, reader_locale: Locale) -> SentText {
    return SentText {
        seen: temporal.format_relative(
            sent_at,
            now: clock.now,
            zone: Zone.europe_london,
            locale: reader_locale
        )
    }
}
```

```jadpo
test "a time is shown against now" {
    clock: "2026-09-27T13:30:00Z"

    assert show_sent("2026-09-27T13:29:40Z", Locale.en_gb).seen == "just now"
    assert show_sent("2026-09-27T13:26:00Z", Locale.en_gb).seen == "4 minutes ago"
    assert show_sent("2026-09-27T12:00:00Z", Locale.en_gb).seen == "Today at 13:00"
    assert show_sent("2026-09-26T13:30:00Z", Locale.en_gb).seen == "Yesterday at 14:30"
    assert show_sent("2026-09-20T13:30:00Z", Locale.en_gb).seen == "20 September 2026"
    assert show_sent("2026-09-27T13:26:00Z", Locale.fr_fr).seen == "il y a 4 minutes"
    assert show_sent("2026-09-26T13:30:00Z", Locale.fr_fr).seen == "Hier à 14:30"
}
```

The words are the locale's own for "now", "4 minutes ago", "yesterday" and
"tomorrow"; "just now" is English's, and every other locale says what CLDR
says for "now". Under an hour counts whole minutes, a past value down and a
future one up, so a value is never said to be further away than it is.
Dates are the dates on the clock in the zone the value is shown in, not
blocks of 24 hours, so a day with 25 hours is still one day. The first
letter of "Today at", "Yesterday at", "Tomorrow at" and a weekday is a
capital where the script has them. A locale with no word for yesterday or
tomorrow shows the long date. "Two to six days away" is a weekday and the
time, in the past or the future alike; a week or more is the long date,
with no time.

## `PresentationText`

Formatting gives a `PresentationText`, a built-in type for text meant only
for people. It can go into a response, an email or a page, and a function's
signature may use it directly. Text written in quotes can stand for one,
which is how a test compares formatted text with what it expects.

It can't be stored. An entity field holding it, directly or through a named
type, a list, an object or a variant's data, is
`type.presentation_text_persistence`, and it can't be a setting. It can't be
turned back into a time, ordered, added to or rebuilt into `Text`. Two
compare with `==` and `!=`.

On the wire it is a JSON string. Clients that format times for themselves
receive the `Instant` instead.
