Showing dates and times to people
On this page
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), so this example is a whole project:
application {}locales { default: Locale.en_gb supported: [Locale.en_gb, Locale.fr_fr]}
/// When an order was placed.type OrderedAt = Instant {}
/// A date as "25 Oct 2026" in `Locale.en_gb`.type ShortDate = TimeFormat { day: ComponentStyle.numeric month: ComponentStyle.short year: ComponentStyle.numeric}
/// When an order was placed, shown to its customer.type OrderPlaced = Output { placed: PresentationText}
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.
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: shortis that pattern with the abbreviated name. numericleaves a number as wide as the locale writes it (dd/MM/ystays), andtwo_digitmakes 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 forshort).
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;
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" |
application {}locales { default: Locale.en_gb supported: [Locale.en_gb, Locale.fr_fr]}
/// When a message was sent.type SentAt = Instant {}
/// When a message was sent, as its reader reads it.type SentText = Output { seen: PresentationText}
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 ) }}
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.
Markdown for agents: /docs/showing-dates-and-times-to-people.md.Built from Jadpo main at a707efb.