Skip to content

Include :date, :datetime, and :time with minimal options #1083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 78 additions & 97 deletions spec/functions/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,31 @@ This subsection describes the _functions_ and _options_ for date/time formatting
> [!IMPORTANT]
> The _functions_ in this section have a status of **Draft**.
> They are proposed for inclusion in a future release and are not Stable.
> The _options_ and _option values_ used by `:datetime`, `:date`, and `:time`
> are based on [Semantic Skeletons], which are in technical preview.
> The set of _options_ and _option values_ will be extended by later versions of this specification.

> [!NOTE]
> Selection based on date/time types is not required by this release of MessageFormat.
> Use care when defining implementation-specific _selectors_ based on date/time types.
> The types of queries found in implementations such as `java.time.TemporalAccessor`
> are complex and user expectations might be inconsistent with good I18N practices.

[Semantic Skeletons]: https://www.unicode.org/reports/tr35/tr35-75/tr35-dates.html#Semantic_Skeletons

#### The `:datetime` function

The function `:datetime` is used to format date/time values, including
the ability to compose user-specified combinations of fields.
The function `:datetime` is used to format a date/time value.
Its formatted result will always include both the date and the time,
and optionally a timezone.

If no options are specified, this function defaults to the following:

- `{$d :datetime}` is the same as `{$d :datetime dateStyle=medium timeStyle=short}`
- `{$d :datetime}` is the same as<br>
`{$d :datetime dateFields=year-month-day timePrecision=minute}`

> [!NOTE]
> The default formatting behavior of `:datetime` is inconsistent with `Intl.DateTimeFormat`
> The formatting behavior of `:datetime` is inconsistent with `Intl.DateTimeFormat`
> in JavaScript and with `{d,date}` in ICU MessageFormat 1.0.
> This is because, unlike those implementations, `:datetime` is distinct from `:date` and `:time`.

Expand All @@ -35,93 +42,42 @@ All other _operand_ values produce a _Bad Operand_ error.

##### Options

The `:datetime` function can use either the appropriate _style options_
or can use a collection of _field options_ (but not both) to control the formatted
output.
_Date/time override options_ can be combined with either _style options_ or _field options_.

If both _style options_ and _field options_ are specified,
a _Bad Option_ error is emitted
and a _fallback value_ used as the _resolved value_ of the _expression_.

If the _operand_ of the _expression_ is an implementation-defined date/time type,
it can include _style options_, _field options_, or other _options_.
These are included in the resolved option values of the _expression_,
with _options_ on the _expression_ taking priority over any options of the _operand_.

> [!NOTE]
> The names of _options_ and their _option values_ were derived from the
> [options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#description)
> in JavaScript's `Intl.DateTimeFormat`.

###### Style Options
The following _options_ are REQUIRED to be available on the function `:datetime`:

**_<dfn>Style options</dfn>_** pertain to the overall styling or appearance of the formatted output.

The following _style options_ are REQUIRED to be available on the function `:datetime`:

- `dateStyle`
- `full`
- `dateFields`
- `weekday`
- `day-weekday`
- `month-day`
- `month-day-weekday`
- `year-month-day` (default)
- `year-month-day-weekday`
Comment on lines +47 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super happy with mixing camel case and kebab case, but I don't really want the field sets to be in camel case

Copy link
Member

@sffc sffc Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The casing was a major point of contention in the ICU4X TC, which is one reason we ended up landing on YMD. (I remember where I was when we had that discussion: I was in Helsinki walking to the Future Frontend speaker dinner)

- `dateLength`
- `long`
- `medium`
- `medium` (default)
- `short`
- `timeStyle`
- `full`
- `timePrecision`
- `hour`
- `minute` (default)
- `second`
- `timeZoneStyle`
- `long`
- `medium`
- `short`
- _Date/time override options_

###### Field Options

**_<dfn>Field options</dfn>_** describe which fields to include in the formatted output
and what format to use for that field.

> [!NOTE]
> _Field options_ do not have default values because they are only to be used
> to compose the formatter.
If the `timeZoneStyle` _option_ is not included in the _expression_,
its formatted result will not include a timezone indicator.

The following _field options_ are REQUIRED to be available on the function `:datetime`:
Except for _date/time override options_,
each `:datetime` _option value_ MUST be set by a _literal_.
If such an _option value_ is a _variable_,
a _Bad Option Error_ is emitted and
the _option_ is ignored when formatting the _expression_.

- `weekday`
- `long`
- `short`
- `narrow`
- `era`
- `long`
- `short`
- `narrow`
- `year`
- `numeric`
- `2-digit`
- `month`
- `numeric`
- `2-digit`
- `long`
- `short`
- `narrow`
- `day`
- `numeric`
- `2-digit`
- `hour`
- `numeric`
- `2-digit`
- `minute`
- `numeric`
- `2-digit`
- `second`
- `numeric`
- `2-digit`
- `fractionalSecondDigits`
- `1`
- `2`
- `3`
- `timeZoneName`
- `long`
- `short`
- `shortOffset`
- `longOffset`
- `shortGeneric`
- `longGeneric`
If the _operand_ of the _expression_ is an implementation-defined date/time type,
it can include other option values.
Any _date/time override options_ of the operand are included in the resolved option values of the _expression_,
with _options_ on the _expression_ taking priority over any options of the _operand_.
Any _operand_ options not matching the _date/time override options_ are ignored.

##### Resolved Value

Expand All @@ -136,7 +92,7 @@ The function `:date` is used to format the date portion of date/time values.

If no options are specified, this function defaults to the following:

- `{$d :date}` is the same as `{$d :date style=medium}`
- `{$d :date}` is the same as `{$d :date fields=year-month-day length=medium}`

##### Operands

Expand All @@ -147,19 +103,31 @@ All other _operand_ values produce a _Bad Operand_ error.

##### Options

The function `:date` has these _options_:
The following _options_ are REQUIRED to be available on the function `:date`:

- `style` \[REQUIRED\]
- `full`
- `fields`
- `weekday`
- `day-weekday`
- `month-day`
- `month-day-weekday`
- `year-month-day` (default)
- `year-month-day-weekday`
Comment on lines +108 to +114
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @eemeli removed the day fieldset based on an offhand remark I made at a meeting in June that the data for that fieldset was not high quality.

But, there is data for that fieldset in CLDR. It is just the number like "15", and my comment was that I think it would be better as "the 15th". But that's not my prerogative: it is something CLDR should decide, and it has clearly taken the position that it should be just the integer (this is from en.xml):

					<availableFormats>
						<dateFormatItem id="Bh">h B</dateFormatItem>
						<dateFormatItem id="Bhm">h:mm B</dateFormatItem>
						<dateFormatItem id="Bhms">h:mm:ss B</dateFormatItem>
						<dateFormatItem id="d">d</dateFormatItem>
						<dateFormatItem id="E">ccc</dateFormatItem>
						<dateFormatItem id="EBh">E h B</dateFormatItem>
						<dateFormatItem id="EBhm">E h:mm B</dateFormatItem>
						<dateFormatItem id="EBhms">E h:mm:ss B</dateFormatItem>
						<dateFormatItem id="Ed">d E</dateFormatItem>

I have a similar complaint about day-weekday, which I think should be "Monday the 15th", but the data currently says "15 Monday"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aphillips's comment above #1083 (comment) would indicate a similar sentiment.

Out of an abundance of caution, my preferred solution here would be to leave out day-weekday as well from this PR, so that it may be discussed later.

- `length`
- `long`
- `medium` (default)
- `short`
- _Date/time override options_

The `fields` and `length` _option values_ MUST each be set by a _literal_.
If such an _option value_ is a _variable_,
a _Bad Option Error_ is emitted and
the _option_ is ignored when formatting the _expression_.

If the _operand_ of the _expression_ is an implementation-defined date/time type,
it can include other option values.
Any _operand_ options matching the `:datetime` _style options_ or _field options_ are ignored,
as is any `style` option.
Any _date/time override options_ of the operand are included in the resolved option values of the _expression_,
with _options_ on the _expression_ taking priority over any options of the _operand_.
Any _operand_ options not matching the _date/time override options_ are ignored.

##### Resolved Value

Expand All @@ -173,10 +141,12 @@ is used as an _operand_ or an _option value_.
#### The `:time` function

The function `:time` is used to format the time portion of date/time values.
Its formatted result will always include the time,
and optionally a timezone.

If no options are specified, this function defaults to the following:

- `{$t :time}` is the same as `{$t :time style=short}`
- `{$t :time}` is the same as `{$t :time precision=minute}`

##### Operands

Expand All @@ -187,19 +157,30 @@ All other _operand_ values produce a _Bad Operand_ error.

##### Options

The function `:time` has these _options_:
The following _options_ are REQUIRED to be available on the function `:time`:

- `style` \[REQUIRED\]
- `full`
- `precision`
- `hour`
- `minute` (default)
- `second`
- `timeZoneStyle`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeZoneStyle or zoneStyle?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeZoneStyle. As I mention in #1082 (comment),

I'd be fine with timeZoneStyle or timeZoneDisplay. Abbreviating out the "time" part would be a mistake -- it's not at all obvious to anyone who's not worked closely with datetime formatting that an unspecified "zone" is referring to a time zone [1].

[1] Unscientifically just verified this by asking three non-developer relatives about the meaning of the last option in the example at the top post here. They were quite confused, until I changed the name to timeZoneStyle.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The experimental ICU4X syntax uses zoneStyle and the Rust API uses zone

- `long`
- `medium`
- `short` (default)
- `short`
- _Date/time override options_

If the `timeZoneStyle` _option_ is not included in the _expression_,
its formatted result will not include a timezone indicator.

The `precision` and `timeZoneStyle` _option values_ MUST each be set by a _literal_.
If such an _option value_ is a _variable_,
a _Bad Option Error_ is emitted and
the _option_ is ignored when formatting the _expression_.

If the _operand_ of the _expression_ is an implementation-defined date/time type,
it can include other option values.
Any _operand_ options matching the `:datetime` _style options_ or _field options_ are ignored,
as is any `style` option.
Any _date/time override options_ of the operand are included in the resolved option values of the _expression_,
with _options_ on the _expression_ taking priority over any options of the _operand_.
Any _operand_ options not matching the _date/time override options_ are ignored.

##### Resolved Value

Expand Down
12 changes: 6 additions & 6 deletions spec/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,18 +588,18 @@ option = identifier o "=" o (literal / variable)

> Examples of _functions_ with _options_
>
> A _message_ using the `:datetime` function.
> The _option_ `weekday` has the literal `long` as its value:
> A _message_ using the `:date` function.
> The _option_ `length` has the literal `long` as its value:
>
> ```
> Today is {$date :datetime weekday=long}!
> Today is {$now :date length=long}!
> ```

> A _message_ using the `:datetime` function.
> The _option_ `weekday` has a variable `$dateStyle` as its value:
> A _message_ using the `:date` function.
> The _option_ `length` has a variable `$dateLength` as its value:
>
> ```
> Today is {$date :datetime weekday=$dateStyle}!
> Today is {$now :date length=$dateLength}!
> ```

### Markup
Expand Down
6 changes: 3 additions & 3 deletions test/tests/functions/date.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@
"src": "{|2006-01-02T15:04:06| :date}"
},
{
"src": "{|2006-01-02| :date style=long}"
"src": "{|2006-01-02| :date length=long}"
},
{
"src": ".local $d = {|2006-01-02| :date style=long} {{{$d}}}"
"src": ".local $d = {|2006-01-02| :date length=long} {{{$d}}}"
},
{
"src": ".local $d = {|2006-01-02| :datetime dateStyle=long timeStyle=long} {{{$d :date}}}"
"src": ".local $d = {|2006-01-02| :datetime dateLength=long timePrecision=second} {{{$d :date}}}"
}
]
}
7 changes: 2 additions & 5 deletions test/tests/functions/datetime.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,10 @@
"src": "{|2006-01-02T15:04:06| :datetime}"
},
{
"src": "{|2006-01-02T15:04:06| :datetime year=numeric month=2-digit}"
"src": "{|2006-01-02T15:04:06| :datetime dateLength=long}"
},
{
"src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}"
},
{
"src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}"
"src": "{|2006-01-02T15:04:06| :datetime timePrecision=second}"
},
{
"src": "{$dt :datetime}",
Expand Down
6 changes: 3 additions & 3 deletions test/tests/functions/time.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
"src": "{|2006-01-02T15:04:06| :time}"
},
{
"src": "{|2006-01-02T15:04:06| :time style=medium}"
"src": "{|2006-01-02T15:04:06| :time precision=second}"
},
{
"src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t}}}"
"src": ".local $t = {|2006-01-02T15:04:06| :time precision=second} {{{$t}}}"
},
{
"src": ".local $t = {|2006-01-02T15:04:06| :datetime dateStyle=long timeStyle=long} {{{$t :time}}}"
"src": ".local $t = {|2006-01-02T15:04:06| :datetime dateLength=long timePrecision=second} {{{$t :time}}}"
}
]
}