Page MenuHomePhabricator

MediaWiki core JS date/time formatter library
Closed, ResolvedPublic

Description

I am proposing a core library which wraps Intl.DateTimeFormat.

Some things that client-side code wants to do with dates are very specific to MediaWiki:

  • Show a date/time in the user's preferred language, respecting uselang
  • Respect the user's date formatting preference
  • Adjust a date/time for the user's preferred timezone

We used to think that a port of a large amount of PHP code would be required to achieve this, but now with the availability of Intl, it would seem that these things can be done more cheaply.

Similar tasks:

Use case review

  • Combined date/time formatting
    • Special:Block Codex table display T388048
    • CampaignEvents
  • Date-only, time-only formatting
    • CampaignEvents GetFormattedTimeHandler
  • Short date formatting
    • GrowthExperiments CScoreCards
    • GrowthExperiments RecentActivity
  • ISO-8601 formatter with timezone awareness
  • MW 14-char formatter
    • DiscussionTools ThreadItem
    • CampaignEvents TimeZoneConverter
  • Range formatting
    • GrowthExperiments CScoreCards
  • Duration formatting
    • moment.duration() callers e.g. UploadWizard
    • MobileFrontend time.js
    • Core BlockLog.vue

Localisation scheme

  • Locale: Intl supports a fallback array. We can pass the MW language code fallback array to it.
  • Date formatting: MW has $dateFormats in Messages*.php. Add by analogy $jsDateFormats containing an array to pass to the Intl.DateTimeFormat() constructor.
  • Supply only user language configuration. No language code parameters.
  • Time zones: MW server has either IANA name or numeric offset, closely matching the JS data model. Try to use the name; if the client doesn't have it, fall back to numeric offset.
  • Duration formatting: Intl.DurationFormat is "newly available", would need a fallback. Fallback would need a separate data bundle. Probably a separate module and task.

Synopsis

I reviewed Moment and its designated successors, looking at their API style. A lot of what they do is about managing complexity, but what we need is pretty simple. We don't need many options, because the point of it is to provide policy. So I'm suggesting a mostly functional interface with native Date objects as inputs.

The function names should be unambiguous so that they can be destructured into the local scope.

The following functions would all be implicitly localised in the user's language and use the user's timezone:

const DateFormatter = require( 'mediawiki.DateFormatter' );
const date = new Date();
DateFormatter.formatTimeAndDate( date );
// => 19:27, 18 March 2025
DateFormatter.formatTime( date );
// => 19:27
DateFormatter.formatDate( date );
// => 18 March 2025
DateFormatter.formatPrettyDate( date );
// => 18 March
DateFormatter.formatIso( date )
// => 2025-03-18T19:27:58+11:00
DateFormatter.formatUnqualifiedIso( date )
// => 2025-03-18T19:27:58
const date2 = new Date( +date + 60 * 60 * 1000 );
DateFormatter.formatTimeAndDateRange( date, date2 );
// => 19:27–20:27, 18 March 2025

If something needs more options than that, then we could have factory methods providing a customised instance:

const utcFormatter = DateFormatter.forUtc();
utcFormatter.formatTime( date );
// => 08:27

MediaWiki 14-char timestamps are an annoying special case since they are conventionally in UTC. So they would have to be accessed with DateFormatter.forUtc().formatMw( date ).

Related Objects

Event Timeline

tstarling renamed this task from MediaWiki core JS date/time library to MediaWiki core JS date/time formatter library.Mar 18 2025, 11:48 AM
tstarling updated the task description. (Show Details)

I obviously strongly support this; also cross-linking the previous conversation in T323193#10634998 and following comments.

As for similar tasks, we also have T21992 from 2009 which is basically a duplicate of this, except older. Normally we would close the new task as duplicate and keep the old one around, but in this case the old one talks about document.write, LQT, and other stuff that would better remain in the past. So, I'm guessing we might want to mark the old one as duplicate instead. Or wait a couple more years, when that task will be of legal age and able to decide for itself.

Also speaking about the current usage patterns in CampaignEvents: we do need combined date+time formatting too, in addition to individual date and individual time. And the 14-char timestamp formatting will no longer be needed once we have this new library. Right now it's only used as an intermediate format between JavaScript and PHP.

I am proposing a core library which wraps Intl.DateTimeFormat.

We are using Intl.DateTimeFormat directly in production (gracefully falling back on browsers that don't support it to no formatting)
https://gerrit.wikimedia.org/g/mediawiki/core/+/835f364c5206d0f967fb4cb4f40a18b9da74e89f/resources/src/mediawiki.htmlform/timezone.js#28

A few questions

  • I think it's acceptable to use that in production provided you feature detect it. What features would be missing if you just used that?
  • Could we consider a temporary polyfill library while browsers catch up?
  • I think it's acceptable to use that in production provided you feature detect it. What features would be missing if you just used that?
  • Could we consider a temporary polyfill library while browsers catch up?

All grade A supported browsers listed in the isCompatible() doc comment in startup.js have Intl.DateTimeFormat, according to mozdev.

BrowserisCompatible()Intl.DateTimeFormat
Chrome6324
Edge7912
Opera5015
Firefox5829
Safari11.110
Mobile Safari11.210
Android5.04.4

https://gerrit.wikimedia.org/g/mediawiki/core/+/835f364c5206d0f967fb4cb4f40a18b9da74e89f/resources/src/mediawiki.htmlform/timezone.js#28

When that code was introduced in 2020, many grade A supported browsers did not have Intl.DateTimeFormat. If I understand correctly, the fallback could be removed now.

@Krinkle any thoughts on performance, RL integration?

Aha, I just wrote https://gerrit.wikimedia.org/r/c/mediawiki/extensions/RevisionSlider/+/1130163 and then found this; as you can see, it's a little messy to convert MW's minute time offsets into an input for Intl, so providing this particularly, and other helpers in general, in core would be helpful.

Static proxy methods add some bytes to the file size. It will be a little bit smaller if require() returns an instance of the class, already configured for user date formatting. The instance would also have factory methods providing different configurations of the same class. Usage will still be as in the synopsis. It's not the way I'd do it in PHP but it seems like a reasonable way to optimise the bundle size.

User configuration will probably be ~500 bytes and can't be in the same module as the code if we want the code to be cacheable.

This comment was removed by tstarling.

Change #1130955 had a related patch set uploaded (by Tim Starling; author: Tim Starling):

[mediawiki/core@master] Date formats test page

https://gerrit.wikimedia.org/r/1130955

Change #1130956 had a related patch set uploaded (by Tim Starling; author: Tim Starling):

[mediawiki/core@master] Client-side date/time formatter library

https://gerrit.wikimedia.org/r/1130956

I wrote a PHP parser for the sprintfDate() format which outputs an Intl.DateTimeFormat options array and a parts template string. On the client side it is ~10 lines of code to call formatToParts() and to substitute the parts into the template.

I'm looking at the output and thinking about whether I should do anything else to resolve the differences and support more languages. How many languages is enough?

I sent month names from MediaWiki instead of using the browser's month names. That fixes hundreds of languages. I tested every MW language using Firefox 135. The remaining differences between PHP and JS are so few as to be enumerable:

  • Wants custom weekday messages: ami, pwn, szy, tay, trv, wuu, zh-tw. I get it, we all want weekdays. Weekdays are great. It's just shorter and simpler if you don't have weekdays. This can be fixed by removing weekdays from the date formats, bringing these languages into line with almost all other languages.
  • The "hebrew date" format in fa wants a day number from the Persian calendar and a month and year from the Hebrew calendar. The browser can't do that, not with a single formatter anyway. Affects all languages that fall back to fa.
  • Algorithmic numbering systems are not supported, needed by he, yi and et. In the case of et, it wants Roman month numbers but Latin everything else, which isn't supported with a single formatter.
  • Wants Iranian/Hebrew calendar months with a custom message: bgn, kk, tg. I'm only sending full, genitive and abbreviated months, not Hebrew and Iranian months.
  • Leading zeroes in "numeric" format: ku-arab, lrc, mzn, rm. Possibly a browser bug.
  • Iranian month name appears in a Latin script despite the browser having a resolved locale: ckb, mzn. Possibly a browser or CLDR bug.

I considered getting month names from mediawiki.language.months, but the code for that module alone is bigger than the data, and it brings in a heavy mediawiki.language dependency. It's cheaper and easier to include month names in our config blob.

I tested it on Chromium 134, and it was the same as Firefox except with fewer locales. The locales with the leading zero bug on Firefox didn't exist on Chromium.

I renamed the class from DateTimeFormatter to DateFormatter, because it's shorter and because there already was a DateTimeFormatter in mw.widgets.

I was misled by mozdev. It says:

Time zone names correspond to the Zone and Link names of the IANA Time Zone Database, such as "UTC", "Asia/Shanghai", "Asia/Kolkata", and "America/New_York". Additionally, time zones can be given as UTC offsets in the format "±hh:mm", "±hhmm", or "±hh", for example as "+01:00", "-2359", or "+23".

The offset format was only introduced in the 11th edition of ECMA-402, published 2024. It's only supported by very recent browsers. In older browsers, we will need to use IANA identifiers like Etc/GMT+10, but they are only defined for integer hours.

MediaWiki 14-char timestamps are an annoying special case since they are conventionally in UTC. So they would have to be accessed with DateFormatter.forUtc().formatMw( date ).

I can see this being a foot gun as it exposes a formatMw method that "should" not be called and/or depending on how the object is made and passed cannot quickly and statically be code reviewed as obviously correct (depending on whether something called forUtc).

Ideas:

  1. Make it throw if zone is not UTC. This would not avoid the mistake but at least it would be detected. Client side exceptions are nasty though and it might not be covered by CI (you're not going to test every single scenario in UTC and non-UTC), so it'd break for end users and then we'd eventually hear about it and it, for individual violations. Also, creating a culture of uncertainty where people "have" to try-catch and fallback is not user friendly.
  2. Make the method not exist (forUtc returns a subclass where it does exist), same as 1, but might reduce chances of mistake and improves potential static analysis, at cost of a worse error message.
  3. Implement formatMw such that it is always in UTC. Could use forUtc internally followed by its current code.
  4. As 3, but rename to eg formatMwUtc, so that it isn't surprising when this method returns a different hour, day, or year than other methods.

I'd recommend 4 (ideas for better name notwithstanding).

  1. Make it throw if zone is not UTC. This would not avoid the mistake but at least it would be detected. Client side exceptions are nasty though and it might not be covered by CI (you're not going to test every single scenario in UTC and non-UTC), so it'd break for end users and then we'd eventually hear about it and it, for individual violations. Also, creating a culture of uncertainty where people "have" to try-catch and fallback is not user friendly.

I considered this previously, and the main reason I didn't do it is because it adds bytes to the code.

The current solution is that there is no formatMw() in the static interface, it only exists in the prototype. So you have to call DateFormatter.forUser().formatMw() to get an MW timestamp in the user timezone, which seems explicit enough to me. If you manage to do that when you didn't mean to, despite the doc comment warning in formatMw(), then maybe you deserve what you get.

Also, this solution used a negative number of bytes of code.

MW timestamps are not a common date format. They are not used for input or output in the action API. JS is not writing to the database. Clients have few ways to share them with clients from other timezones. I only identified one potential caller for this function, and that is DiscussionTools.

DiscussionTools switched from ISO 8601 timestamps to MW timestamps in T304595 because MW timestamps are more compact, a rationale which is independent of timezone. Maybe some future use case will emerge in which a compact timestamp in the user's timezone is desired. I don't want to add bytes to the bundle just to stop them from doing that.

Change #1130956 merged by jenkins-bot:

[mediawiki/core@master] Client-side date/time formatter library

https://gerrit.wikimedia.org/r/1130956

Change #1133512 had a related patch set uploaded (by MusikAnimal; author: MusikAnimal):

[mediawiki/core@master] DateFormatter.js: change JSDoc tags to better advertise the module/class

https://gerrit.wikimedia.org/r/1133512

Change #1133512 merged by jenkins-bot:

[mediawiki/core@master] DateFormatter.js: change JSDoc tags to better advertise the module/class

https://gerrit.wikimedia.org/r/1133512

I think the QA treatment here is probably best done through T388048. It's up to QA if they want to test the library directly.

Belatedly, thanks for implementing this! I made an attempt to adopt the library in CampaignEvents, I've got a few questions from that:

  • The main reason why the JS code in question exists (T362639) is that some people do not apparently set their MW timezone preference, and logged-out users obviously don't have preferences at all. What we do is first check if a timezone preference is set explicitly (or really, if the preference does not use the system value); if so, we do nothing (the date was formatted on the server). Else, we convert the date using the browser timezone (from Intl) and format it using the MW default for the current language. Would it make sense to implement similar behaviour in the DateFormatter? It could be a variant of forUser, or just be what forUser does. The only differences with the current code would be to use the browser timezone instead of wiki timezone.
  • There doesn't seem to be a way to get the IANA timezone name from Intl using the library. This is a one-liner anyway (Intl.DateTimeFormat().resolvedOptions().timeZone), but I wonder if it would make sense to have the library provide it for consistency.
  • For the TODO on formatting ranges, I assume that's something we'd want to also implement in PHP, right? Is there a task for it or should I file one?
  • The main reason why the JS code in question exists (T362639) is that some people do not apparently set their MW timezone preference, and logged-out users obviously don't have preferences at all. What we do is first check if a timezone preference is set explicitly (or really, if the preference does not use the system value); if so, we do nothing (the date was formatted on the server). Else, we convert the date using the browser timezone (from Intl) and format it using the MW default for the current language. Would it make sense to implement similar behaviour in the DateFormatter? It could be a variant of forUser, or just be what forUser does. The only differences with the current code would be to use the browser timezone instead of wiki timezone.

The goal of forUser() was to match the server-side behaviour, which of course doesn't fall back to the browser's timezone. That way, we can remove the timezone from the date formats, since every date the user sees is in the same timezone. If we allow extensions to choose their timezone policy, then it becomes necessary to append timezones to all formats, to avoid confusion.

If the browser timezone doesn't match the preference timezone, I think MediaWiki core should pop up a prompt asking the user if they want to change the preference. Compare similar features in Phabricator and Google Calendar. That way, server-side dates will benefit from client-side detection and will remain consistent with client-side dates. If the user dismisses the popup without changing the preference, the client side should continue to use the preference.

  • There doesn't seem to be a way to get the IANA timezone name from Intl using the library. This is a one-liner anyway (Intl.DateTimeFormat().resolvedOptions().timeZone), but I wonder if it would make sense to have the library provide it for consistency.

Sure, that could be added. I added getShortZoneName() and I think additional methods could be added by analogy. For brevity, a time zone is usually referred to as a zone within the library.

  • For the TODO on formatting ranges, I assume that's something we'd want to also implement in PHP, right? Is there a task for it or should I file one?

You can file a task.

Change #1130955 abandoned by Tim Starling:

[mediawiki/core@master] [DNM] Date formats test page

https://gerrit.wikimedia.org/r/1130955

Is there support for other languages (especially the site's) than the user's?

Change #1149440 had a related patch set uploaded (by Simon04; author: Simon04):

[mediawiki/core@master] DateFormatter.formatTimeAgo using Intl.RelativeTimeFormat

https://gerrit.wikimedia.org/r/1149440

Change #1149473 had a related patch set uploaded (by Simon04; author: Simon04):

[mediawiki/skins/MinervaNeue@master] Use DateFormatter.formatTimeAgo for mw-diff-timestamp

https://gerrit.wikimedia.org/r/1149473

  • The main reason why the JS code in question exists (T362639) is that some people do not apparently set their MW timezone preference, and logged-out users obviously don't have preferences at all. What we do is first check if a timezone preference is set explicitly (or really, if the preference does not use the system value); if so, we do nothing (the date was formatted on the server). Else, we convert the date using the browser timezone (from Intl) and format it using the MW default for the current language. Would it make sense to implement similar behaviour in the DateFormatter? It could be a variant of forUser, or just be what forUser does. The only differences with the current code would be to use the browser timezone instead of wiki timezone.

The goal of forUser() was to match the server-side behaviour, which of course doesn't fall back to the browser's timezone.

That's a good point. But although it wasn't very clear, I was indeed asking for an extension to the existing functionality; something that obviously cannot exist server-side. When I wrote «a variant of forUser», I meant a separate method (say forUserWithBrowserFallback).

If we allow extensions to choose their timezone policy, then it becomes necessary to append timezones to all formats, to avoid confusion.

I should point out that we do append the timezone to the displayed time. But I also recognize that our use case is a bit non-standard in a MediaWiki context. Unlike the typical MW stuff like edits, log actions, etc., events need to be stored in local time, and sometimes displayed as such. Which is why we always include a timezone. Nonetheless, could this be resolved by means of documentation? E.g., a few lines in the method doc comment explaining how it is typically unnecessary to use a different timezone policy, and that callers should display the timezone name when doing so.

If the browser timezone doesn't match the preference timezone, I think MediaWiki core should pop up a prompt asking the user if they want to change the preference. Compare similar features in Phabricator and Google Calendar. That way, server-side dates will benefit from client-side detection and will remain consistent with client-side dates. If the user dismisses the popup without changing the preference, the client side should continue to use the preference.

I agree with you on this, and in general I support having a single source of truth (in this case, user preferences). And also believe that if something prevents people from keeping that single source of truth up-to-date, then that "something" should be identified and fixed, rather than worked around by introducing a new source of truth. The problem with all of this is that logged-out users don't have preferences, so there's just no way for them to choose a timezone. For events in particular, the problem is exacerbated by many events being on meta, which uses GMT and is therefore not useful for many.

  • For the TODO on formatting ranges, I assume that's something we'd want to also implement in PHP, right? Is there a task for it or should I file one?

You can file a task.

I surely have the ability to do it: T395288.

Change #1149440 merged by jenkins-bot:

[mediawiki/core@master] DateFormatter.formatRelativeTimeOrDate using Intl.RelativeTimeFormat

https://gerrit.wikimedia.org/r/1149440

Change #1149473 merged by jenkins-bot:

[mediawiki/skins/MinervaNeue@master] Use DateFormatter.formatRelativeTimeOrDate for mw-diff-timestamp

https://gerrit.wikimedia.org/r/1149473

Change #1175889 had a related patch set uploaded (by Krinkle; author: Krinkle):

[mediawiki/core@master] mediawiki.DateFormatter: Fix test failure on Chrome

https://gerrit.wikimedia.org/r/1175889

Change #1175889 merged by jenkins-bot:

[mediawiki/core@master] mediawiki.DateFormatter: Fix test failure on Chrome

https://gerrit.wikimedia.org/r/1175889