Add ECMA402 datetime format to package:intl4x (#680)

* Adding Datetime format

* Add tests

* Rename Datetime -> DateTime

* Add readme check

* Add changelog entry

* Fix tests

* Revert name change

* Switch to stable health workflow

* Allow number in package names

* Add checkmark

* Fix merge problems

* Rev pubspec

* Add platforms to pubspec

* Renaming options

* Fixes

* Export Options from all formatters

* fix readme example
diff --git a/pkgs/intl4x/CHANGELOG.md b/pkgs/intl4x/CHANGELOG.md
index b88d09b..8290fd8 100644
--- a/pkgs/intl4x/CHANGELOG.md
+++ b/pkgs/intl4x/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.5.0
+
+- Add `DateTime` formatting.
+
 ## 0.4.0
 
 - Add a `Locale` class.
diff --git a/pkgs/intl4x/README.md b/pkgs/intl4x/README.md
index e6c6751..b94a3d0 100644
--- a/pkgs/intl4x/README.md
+++ b/pkgs/intl4x/README.md
@@ -17,7 +17,7 @@
 
 |   | Number format  | List format  | Date format  | Collation  | Display names |
 |---|:---:|:---:|:---:|:---:|:---:|
-| **ECMA402 (web)** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |  | :heavy_check_mark: |
+| **ECMA402 (web)** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
 | **ICU4X (web/native)**  |   |   |   |   |   | 
 
 ## Implementation and Goals
@@ -36,10 +36,12 @@
 import 'package:intl4x/intl4x.dart';
 import 'package:intl4x/number_format.dart';
 
-final numberFormat = Intl(
-  ecmaPolicy: const AlwaysEcma(),
-  defaultLocale: Locale(language: 'en', country: 'US'),
-).numberFormat(NumberFormatOptions.percent());
+void main() {
+  final numberFormat = Intl(
+    ecmaPolicy: const AlwaysEcma(),
+    locale: const Locale(language: 'en', region: 'US'),
+  ).numberFormat(NumberFormatOptions.percent());
 
-print(numberFormat.format(0.5)); // prints 50%
+  print(numberFormat.format(0.5)); // prints 50%
+}
 ```
diff --git a/pkgs/intl4x/lib/collation.dart b/pkgs/intl4x/lib/collation.dart
index b2446dd..cc987ac 100644
--- a/pkgs/intl4x/lib/collation.dart
+++ b/pkgs/intl4x/lib/collation.dart
@@ -4,3 +4,4 @@
 
 export 'src/collation/collation.dart';
 export 'src/collation/collation_options.dart';
+export 'src/options.dart';
diff --git a/pkgs/intl4x/lib/datetime_format.dart b/pkgs/intl4x/lib/datetime_format.dart
new file mode 100644
index 0000000..6735df8
--- /dev/null
+++ b/pkgs/intl4x/lib/datetime_format.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/datetime_format/datetime_format.dart';
+export 'src/datetime_format/datetime_format_options.dart';
+export 'src/options.dart';
diff --git a/pkgs/intl4x/lib/display_names.dart b/pkgs/intl4x/lib/display_names.dart
index 2acd7de..4c1ba3f 100644
--- a/pkgs/intl4x/lib/display_names.dart
+++ b/pkgs/intl4x/lib/display_names.dart
@@ -4,3 +4,4 @@
 
 export 'src/display_names/display_names.dart';
 export 'src/display_names/display_names_options.dart';
+export 'src/options.dart';
diff --git a/pkgs/intl4x/lib/intl4x.dart b/pkgs/intl4x/lib/intl4x.dart
index 6e1a9ff..35bd87f 100644
--- a/pkgs/intl4x/lib/intl4x.dart
+++ b/pkgs/intl4x/lib/intl4x.dart
@@ -7,6 +7,9 @@
 import 'number_format.dart';
 import 'src/collation/collation_impl.dart';
 import 'src/data.dart';
+import 'src/datetime_format/datetime_format.dart';
+import 'src/datetime_format/datetime_format_impl.dart';
+import 'src/datetime_format/datetime_format_options.dart';
 import 'src/display_names/display_names_impl.dart';
 import 'src/ecma/ecma_policy.dart';
 import 'src/ecma/ecma_stub.dart' if (dart.library.js) 'src/ecma/ecma_web.dart';
@@ -16,7 +19,6 @@
 import 'src/list_format/list_format_options.dart';
 import 'src/locale.dart';
 import 'src/number_format/number_format_impl.dart';
-import 'src/options.dart';
 
 export 'src/locale.dart';
 
@@ -66,6 +68,12 @@
         DisplayNamesImpl.build(locale, localeMatcher, ecmaPolicy),
       );
 
+  DateTimeFormat datetimeFormat([DateTimeFormatOptions? options]) =>
+      DateTimeFormat(
+        options ?? const DateTimeFormatOptions(),
+        DateTimeFormatImpl.build(locale, localeMatcher, ecmaPolicy),
+      );
+
   /// Construct an [Intl] instance providing the current [locale] and the
   /// [ecmaPolicy] defining which locales should fall back to the browser
   /// provided functions.
diff --git a/pkgs/intl4x/lib/list_format.dart b/pkgs/intl4x/lib/list_format.dart
index f67fa01..a22f8a7 100644
--- a/pkgs/intl4x/lib/list_format.dart
+++ b/pkgs/intl4x/lib/list_format.dart
@@ -4,3 +4,4 @@
 
 export 'src/list_format/list_format.dart';
 export 'src/list_format/list_format_options.dart';
+export 'src/options.dart';
diff --git a/pkgs/intl4x/lib/number_format.dart b/pkgs/intl4x/lib/number_format.dart
index e977cfc..facd400 100644
--- a/pkgs/intl4x/lib/number_format.dart
+++ b/pkgs/intl4x/lib/number_format.dart
@@ -4,3 +4,4 @@
 
 export 'src/number_format/number_format.dart';
 export 'src/number_format/number_format_options.dart';
+export 'src/options.dart';
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format.dart
new file mode 100644
index 0000000..0110cea
--- /dev/null
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../test_checker.dart';
+import 'datetime_format_impl.dart';
+import 'datetime_format_options.dart';
+
+/// `DateTime` formatting, for example:
+///
+/// ```dart
+/// final date = DateTime.utc(2021, 12, 17, 4, 0, 42);
+/// Intl(locale: const Locale(language: 'fr'))
+///     .datetimeFormat(const DateTimeFormatOptions(
+///       hour: TimeRepresentation.numeric,
+///       hourCycle: HourCycle.h12,
+///       dayPeriod: DayPeriod.narrow,
+///       timeZone: 'UTC',
+///     ))
+///     .format(date); // Output: '4 mat.'
+/// ```
+class DateTimeFormat {
+  final DateTimeFormatOptions _options;
+  final DateTimeFormatImpl impl;
+
+  DateTimeFormat(this._options, this.impl);
+
+  String format(DateTime datetime) {
+    if (isInTest) {
+      return '$datetime//${impl.locale}';
+    } else {
+      return impl.formatImpl(datetime, _options);
+    }
+  }
+}
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_4x.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_4x.dart
new file mode 100644
index 0000000..4c136c3
--- /dev/null
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_4x.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../locale.dart';
+import 'datetime_format_impl.dart';
+import 'datetime_format_options.dart';
+
+DateTimeFormatImpl getDateTimeFormatter4X(Locale locale) =>
+    DateTimeFormat4X(locale);
+
+class DateTimeFormat4X extends DateTimeFormatImpl {
+  DateTimeFormat4X(super.locale);
+
+  @override
+  String formatImpl(DateTime datetime, DateTimeFormatOptions options) {
+    throw UnimplementedError('Insert diplomat bindings here');
+  }
+}
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_ecma.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_ecma.dart
new file mode 100644
index 0000000..4df5494
--- /dev/null
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_ecma.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart';
+
+import '../locale.dart';
+import '../options.dart';
+import 'datetime_format_impl.dart';
+import 'datetime_format_options.dart';
+
+DateTimeFormatImpl? getDateTimeFormatterECMA(
+  Locale locale,
+  LocaleMatcher localeMatcher,
+) =>
+    _DateTimeFormatECMA.tryToBuild(locale, localeMatcher);
+
+@JS('Intl.DateTimeFormat')
+class _DateTimeFormatJS {
+  external factory _DateTimeFormatJS([List<String> locale, Object options]);
+  external String format(Object num);
+}
+
+@JS('Intl.DateTimeFormat.supportedLocalesOf')
+external List<String> _supportedLocalesOfJS(
+  List<String> listOfLocales, [
+  Object options,
+]);
+
+@JS('Date')
+class DateJS {
+  external factory DateJS(
+    int year,
+    int monthIndex,
+    int day,
+    int hours,
+    int minutes,
+    int seconds,
+    int milliseconds,
+  );
+
+  external factory DateJS.fromTimeStamp(int timeStamp);
+}
+
+@JS('Date.UTC')
+// ignore: non_constant_identifier_names
+external int UTC(
+  int year,
+  int monthIndex,
+  int day,
+  int hours,
+  int minutes,
+  int seconds,
+  int milliseconds,
+);
+
+class _DateTimeFormatECMA extends DateTimeFormatImpl {
+  _DateTimeFormatECMA(super.locale);
+
+  static DateTimeFormatImpl? tryToBuild(
+    Locale locale,
+    LocaleMatcher localeMatcher,
+  ) {
+    final supportedLocales = supportedLocalesOf(localeMatcher, locale);
+    return supportedLocales.isNotEmpty
+        ? _DateTimeFormatECMA(supportedLocales.first)
+        : null; //TODO: Add support to force return an instance instead of null.
+  }
+
+  static List<Locale> supportedLocalesOf(
+    LocaleMatcher localeMatcher,
+    Locale locale,
+  ) {
+    final o = newObject<Object>();
+    setProperty(o, 'localeMatcher', localeMatcher.jsName);
+    return List.from(_supportedLocalesOfJS([locale.toLanguageTag()], o))
+        .whereType<String>()
+        .map(Locale.parse)
+        .toList();
+  }
+
+  @override
+  String formatImpl(DateTime datetime, DateTimeFormatOptions options) {
+    final datetimeFormatJS = _DateTimeFormatJS(
+      [locale.toLanguageTag()],
+      options.toJsOptions(),
+    );
+    return datetimeFormatJS.format(datetime.toJs());
+  }
+}
+
+extension on DateTime {
+  DateJS toJs() {
+    if (isUtc) {
+      return DateJS.fromTimeStamp(
+          UTC(year, month - 1, day, hour, minute, second, millisecond));
+    } else {
+      return DateJS(year, month - 1, day, hour, minute, second, millisecond);
+    }
+  }
+}
+
+extension on DateTimeFormatOptions {
+  Object toJsOptions() {
+    final o = newObject<Object>();
+    setProperty(o, 'localeMatcher', localeMatcher.jsName);
+    if (dateFormatStyle != null) {
+      setProperty(o, 'dateStyle', dateFormatStyle!.name);
+    }
+    if (timeFormatStyle != null) {
+      setProperty(o, 'timeStyle', timeFormatStyle!.name);
+    }
+    if (calendar != null) setProperty(o, 'calendar', calendar!.jsName);
+    if (dayPeriod != null) setProperty(o, 'dayPeriod', dayPeriod!.name);
+    if (numberingSystem != null) {
+      setProperty(o, 'numberingSystem', numberingSystem!.name);
+    }
+    if (timeZone != null) setProperty(o, 'timeZone', timeZone!);
+    if (clockstyle != null) {
+      setProperty(o, 'hour12', clockstyle!.is12Hour);
+      if (clockstyle!.startAtZero != null) {
+        setProperty(o, 'hourCycle', clockstyle!.hourStyleJsString());
+      }
+    }
+    if (weekday != null) setProperty(o, 'weekday', weekday!.name);
+    if (era != null) setProperty(o, 'era', era!.name);
+    if (year != null) setProperty(o, 'year', year!.jsName);
+    if (month != null) setProperty(o, 'month', month!.jsName);
+    if (day != null) setProperty(o, 'day', day!.jsName);
+    if (hour != null) setProperty(o, 'hour', hour!.jsName);
+    if (minute != null) setProperty(o, 'minute', minute!.jsName);
+    if (second != null) setProperty(o, 'second', second!.jsName);
+    if (fractionalSecondDigits != null) {
+      setProperty(o, 'fractionalSecondDigits', fractionalSecondDigits!);
+    }
+    if (timeZoneName != null) {
+      setProperty(o, 'timeZoneName', timeZoneName!.name);
+    }
+    setProperty(o, 'formatMatcher', formatMatcher.jsName);
+    return o;
+  }
+}
+
+extension on ClockStyle {
+  String hourStyleJsString() {
+    // The four possible values are h11, h12, h23, h24.
+    final firstDigit = is12Hour ? 1 : 2;
+
+    final subtrahend = startAtZero! ? 1 : 0;
+    final secondDigit = firstDigit * 2 - subtrahend;
+
+    /// The cases are
+    /// * firstDigit == 1 && subtrahend == 1  --> h11
+    /// * firstDigit == 1 && subtrahend == 0  --> h12
+    /// * firstDigit == 2 && subtrahend == 1  --> h23
+    /// * firstDigit == 2 && subtrahend == 0  --> h24
+    return 'h$firstDigit$secondDigit';
+  }
+}
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart
new file mode 100644
index 0000000..8d85adc
--- /dev/null
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../ecma/ecma_policy.dart';
+import '../locale.dart';
+import '../options.dart';
+import '../utils.dart';
+import 'datetime_format_4x.dart';
+import 'datetime_format_options.dart';
+import 'datetime_format_stub.dart'
+    if (dart.library.js) 'datetime_format_ecma.dart';
+
+/// This is an intermediate to defer to the actual implementations of
+/// datetime formatting.
+abstract class DateTimeFormatImpl {
+  final Locale locale;
+
+  DateTimeFormatImpl(this.locale);
+
+  String formatImpl(DateTime datetime, DateTimeFormatOptions options);
+
+  factory DateTimeFormatImpl.build(
+    Locale locale,
+    LocaleMatcher localeMatcher,
+    EcmaPolicy ecmaPolicy,
+  ) =>
+      buildFormatter(
+        locale,
+        localeMatcher,
+        ecmaPolicy,
+        getDateTimeFormatterECMA,
+        getDateTimeFormatter4X,
+      );
+}
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_options.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_options.dart
new file mode 100644
index 0000000..24c0de4
--- /dev/null
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_options.dart
@@ -0,0 +1,170 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../options.dart';
+
+typedef WeekDayStyle = Style;
+typedef DayPeriod = Style;
+typedef EraStyle = Style;
+typedef DateFormatStyle = TimeFormatStyle;
+
+/// DateTime formatting functionality of the browser.
+class DateTimeFormatOptions {
+  /// The date formatting style.
+  final DateFormatStyle? dateFormatStyle;
+
+  /// The time formatting style.
+  final TimeFormatStyle? timeFormatStyle;
+
+  final Calendar? calendar;
+
+  /// The formatting style used for day periods - only used when the
+  /// [clockstyle] parameter is true.
+  final DayPeriod? dayPeriod;
+  final NumberingSystem? numberingSystem;
+  final String? timeZone;
+
+  /// Whether to use a 12- or 24-hour style clock.
+  final ClockStyle? clockstyle;
+  final WeekDayStyle? weekday;
+  final EraStyle? era;
+  final TimeStyle? year;
+  final MonthStyle? month;
+  final TimeStyle? day;
+  final TimeStyle? hour;
+  final TimeStyle? minute;
+  final TimeStyle? second;
+
+  /// The number of digits used to represent fractions of a second.
+  final int? fractionalSecondDigits;
+
+  /// The localized representation of the time zone name.
+  final TimeZoneName? timeZoneName;
+  final FormatMatcher formatMatcher;
+  final LocaleMatcher localeMatcher;
+
+  const DateTimeFormatOptions({
+    this.dateFormatStyle,
+    this.timeFormatStyle,
+    this.calendar,
+    this.dayPeriod,
+    this.numberingSystem,
+    this.timeZone,
+    this.clockstyle,
+    this.weekday,
+    this.era,
+    this.year,
+    this.month,
+    this.day,
+    this.hour,
+    this.minute,
+    this.second,
+    this.fractionalSecondDigits,
+    this.timeZoneName,
+    this.formatMatcher = FormatMatcher.bestfit,
+    this.localeMatcher = LocaleMatcher.bestfit,
+  });
+}
+
+class ClockStyle {
+  final bool is12Hour;
+  final bool? startAtZero;
+
+  const ClockStyle({required this.is12Hour, this.startAtZero});
+}
+
+enum TimeFormatStyle {
+  full,
+  long,
+  medium,
+  short,
+}
+
+enum NumberingSystem {
+  arab,
+  arabext,
+  bali,
+  beng,
+  deva,
+  fullwide,
+  gujr,
+  guru,
+  hanidec,
+  khmr,
+  knda,
+  laoo,
+  latn,
+  limb,
+  mlym,
+  mong,
+  mymr,
+  orya,
+  tamldec,
+  telu,
+  thai,
+  tibt;
+}
+
+enum HourCycle {
+  h11,
+  h12,
+  h23,
+  h24;
+}
+
+enum FormatMatcher {
+  basic,
+  bestfit('best fit');
+
+  final String? _jsName;
+
+  String? get jsName => _jsName ?? name;
+
+  const FormatMatcher([this._jsName]);
+}
+
+enum MonthStyle {
+  numeric,
+  twodigit('2-digit'),
+  long,
+  short,
+  narrow;
+
+  String get jsName => _jsName ?? name;
+
+  final String? _jsName;
+
+  const MonthStyle([this._jsName]);
+}
+
+enum TimeStyle {
+  numeric,
+  twodigit('2-digit');
+
+  String get jsName => _jsName ?? name;
+
+  final String? _jsName;
+
+  const TimeStyle([this._jsName]);
+}
+
+enum TimeZoneName {
+  /// Example: `Pacific Standard Time`
+  long,
+
+  /// Example: `PST`
+  short,
+
+  /// Example: `GMT-8`
+  shortOffset,
+
+  /// Example: `GMT-0800`
+  longOffset,
+
+  /// Example: `PT`
+  shortGeneric,
+
+  /// Example: `Pacific Time`
+  longGeneric;
+}
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_stub.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_stub.dart
new file mode 100644
index 0000000..470952c
--- /dev/null
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_stub.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../locale.dart';
+import '../options.dart';
+import 'datetime_format_impl.dart';
+
+DateTimeFormatImpl? getDateTimeFormatterECMA(
+  Locale locales,
+  LocaleMatcher localeMatcher,
+) =>
+    throw UnimplementedError('Cannot use ECMA outside of web environments.');
diff --git a/pkgs/intl4x/lib/src/display_names/display_names_4x.dart b/pkgs/intl4x/lib/src/display_names/display_names_4x.dart
index 03fd666..8f419d3 100644
--- a/pkgs/intl4x/lib/src/display_names/display_names_4x.dart
+++ b/pkgs/intl4x/lib/src/display_names/display_names_4x.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import '../locale.dart';
+import '../options.dart';
 import 'display_names_impl.dart';
 import 'display_names_options.dart';
 
diff --git a/pkgs/intl4x/lib/src/display_names/display_names_options.dart b/pkgs/intl4x/lib/src/display_names/display_names_options.dart
index 7a15512..26dd398 100644
--- a/pkgs/intl4x/lib/src/display_names/display_names_options.dart
+++ b/pkgs/intl4x/lib/src/display_names/display_names_options.dart
@@ -19,12 +19,6 @@
   });
 }
 
-enum Style {
-  narrow,
-  short,
-  long,
-}
-
 enum DisplayType {
   calendar,
   currency,
@@ -57,30 +51,3 @@
   minute,
   second,
 }
-
-enum Calendar {
-  buddhist,
-  chinese,
-  coptic,
-  dangi,
-  ethioaa,
-  ethiopic,
-  gregory,
-  hebrew,
-  indian,
-  islamic,
-  islamicUmalqura('islamic-umalqura'),
-  islamicTbla('islamic-tbla'),
-  islamicCivil('islamic-civil'),
-  islamicRgsa('islamic-rgsa'),
-  iso8601,
-  japanese,
-  persian,
-  roc;
-
-  String get jsName => _jsName ?? name;
-
-  final String? _jsName;
-
-  const Calendar([this._jsName]);
-}
diff --git a/pkgs/intl4x/lib/src/list_format/list_format.dart b/pkgs/intl4x/lib/src/list_format/list_format.dart
index 786d6e6..aaadadf 100644
--- a/pkgs/intl4x/lib/src/list_format/list_format.dart
+++ b/pkgs/intl4x/lib/src/list_format/list_format.dart
@@ -2,7 +2,6 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import '../options.dart';
 import '../test_checker.dart';
 import 'list_format_impl.dart';
 import 'list_format_options.dart';
@@ -17,12 +16,7 @@
   /// ```dart
   /// format(['A', 'B', 'C']) == 'A, B, and C'
   /// ```
-  String format(
-    List<String> list, {
-    LocaleMatcher localeMatcher = LocaleMatcher.bestfit,
-    Type type = Type.conjunction,
-    ListStyle style = ListStyle.long,
-  }) {
+  String format(List<String> list) {
     if (isInTest) {
       return '${list.join(', ')}//${_listFormatImpl.locale}';
     } else {
diff --git a/pkgs/intl4x/lib/src/list_format/list_format_options.dart b/pkgs/intl4x/lib/src/list_format/list_format_options.dart
index f8faa3e..aa9381c 100644
--- a/pkgs/intl4x/lib/src/list_format/list_format_options.dart
+++ b/pkgs/intl4x/lib/src/list_format/list_format_options.dart
@@ -4,8 +4,16 @@
 
 import '../options.dart';
 
+typedef ListStyle = Style;
+
 class ListFormatOptions {
   final Type type;
+
+  /// Indicates the grouping style (for example, whether list separators and
+  /// conjunctions are included).
+  /// * long: "A, B, and C".
+  /// * short: "A, B, C".
+  /// * narrow: "A B C".
   final ListStyle style;
   final LocaleMatcher localeMatcher;
 
@@ -27,16 +35,3 @@
   /// Grouping the list items as a unit: "A, B, C".
   unit;
 }
-
-/// Indicates the grouping style (for example, whether list separators and
-/// conjunctions are included).
-enum ListStyle {
-  /// Example: "A, B, and C".
-  long,
-
-  /// Example: "A, B, C".
-  short,
-
-  /// Example: "A B C".
-  narrow;
-}
diff --git a/pkgs/intl4x/lib/src/number_format/number_format_options.dart b/pkgs/intl4x/lib/src/number_format/number_format_options.dart
index 24e6de7..8fc5a57 100644
--- a/pkgs/intl4x/lib/src/number_format/number_format_options.dart
+++ b/pkgs/intl4x/lib/src/number_format/number_format_options.dart
@@ -6,9 +6,11 @@
 
 import '../options.dart';
 
+typedef UnitDisplay = Style;
+
 /// Number formatting functionality of the browser.
 class NumberFormatOptions {
-  final Style style;
+  final FormatStyle style;
   final String? currency;
   final CurrencyDisplay? currencyDisplay;
   final Unit? unit;
@@ -131,7 +133,7 @@
   factory NumberFormatOptions.compact({
     CompactDisplay compactDisplay = CompactDisplay.short,
     //General options
-    Style style = const DecimalStyle(),
+    FormatStyle style = const DecimalStyle(),
     LocaleMatcher localeMatcher = LocaleMatcher.bestfit,
     SignDisplay signDisplay = SignDisplay.auto,
     Grouping useGrouping = Grouping.auto,
@@ -155,7 +157,7 @@
     );
   }
 
-  static Digits? getDigits(Style style, Digits? digits) {
+  static Digits? getDigits(FormatStyle style, Digits? digits) {
     final fractionDigits = digits?.fractionDigits;
     if (fractionDigits != null) {
       final int newMin;
@@ -324,12 +326,6 @@
   accounting;
 }
 
-enum UnitDisplay {
-  long,
-  short,
-  narrow;
-}
-
 enum SignDisplay {
   auto,
   always,
@@ -419,20 +415,20 @@
   String get name => 'engineering';
 }
 
-sealed class Style {
+sealed class FormatStyle {
   String get name;
 
-  const Style();
+  const FormatStyle();
 }
 
-final class DecimalStyle extends Style {
+final class DecimalStyle extends FormatStyle {
   const DecimalStyle();
 
   @override
   String get name => 'decimal';
 }
 
-final class CurrencyStyle extends Style {
+final class CurrencyStyle extends FormatStyle {
   final String currency;
   final CurrencySign sign;
   final CurrencyDisplay display;
@@ -446,13 +442,13 @@
   String get name => 'currency';
 }
 
-final class PercentStyle extends Style {
+final class PercentStyle extends FormatStyle {
   const PercentStyle();
   @override
   String get name => 'percent';
 }
 
-final class UnitStyle extends Style {
+final class UnitStyle extends FormatStyle {
   final Unit unit;
   final UnitDisplay unitDisplay;
 
diff --git a/pkgs/intl4x/lib/src/options.dart b/pkgs/intl4x/lib/src/options.dart
index 090f3a4..93622a9 100644
--- a/pkgs/intl4x/lib/src/options.dart
+++ b/pkgs/intl4x/lib/src/options.dart
@@ -26,3 +26,36 @@
 
   const LocaleMatcher([this._jsName]);
 }
+
+enum Calendar {
+  buddhist,
+  chinese,
+  coptic,
+  dangi,
+  ethioaa,
+  ethiopic,
+  gregory,
+  hebrew,
+  indian,
+  islamic,
+  islamicUmalqura('islamic-umalqura'),
+  islamicTbla('islamic-tbla'),
+  islamicCivil('islamic-civil'),
+  islamicRgsa('islamic-rgsa'),
+  iso8601,
+  japanese,
+  persian,
+  roc;
+
+  String get jsName => _jsName ?? name;
+
+  final String? _jsName;
+
+  const Calendar([this._jsName]);
+}
+
+enum Style {
+  narrow,
+  short,
+  long,
+}
diff --git a/pkgs/intl4x/pubspec.yaml b/pkgs/intl4x/pubspec.yaml
index 5696874..c5e6f88 100644
--- a/pkgs/intl4x/pubspec.yaml
+++ b/pkgs/intl4x/pubspec.yaml
@@ -1,8 +1,11 @@
 name: intl4x
 description: >-
   A lightweight modular library for internationalization (i18n) functionality.
-version: 0.4.0
+version: 0.5.0
 repository: https://github.com/dart-lang/i18n/tree/main/pkgs/intl4x
+platforms: ## TODO: Add native platforms once ICU4X is integrated.
+  web:
+
 
 environment:
   sdk: '>=3.0.0 <4.0.0'
diff --git a/pkgs/intl4x/test/ecma/datetime_format_test.dart b/pkgs/intl4x/test/ecma/datetime_format_test.dart
new file mode 100644
index 0000000..c1be29b
--- /dev/null
+++ b/pkgs/intl4x/test/ecma/datetime_format_test.dart
@@ -0,0 +1,156 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+library;
+
+import 'package:intl4x/datetime_format.dart';
+import 'package:intl4x/intl4x.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  testWithFormatting('Basic', () {
+    expect(
+        Intl(locale: const Locale(language: 'en', region: 'US'))
+            .datetimeFormat()
+            .format(DateTime.utc(2012, 12, 20, 3, 0, 0)),
+        '12/20/2012');
+  });
+
+  testWithFormatting('timezone', () {
+    final date = DateTime.utc(2021, 12, 17, 3, 0, 42);
+    final intl = Intl(locale: const Locale(language: 'en', region: 'US'));
+    final timeZone = 'America/Los_Angeles';
+    expect(
+      intl
+          .datetimeFormat(DateTimeFormatOptions(
+            timeZone: timeZone,
+            timeZoneName: TimeZoneName.short,
+          ))
+          .format(date),
+      '12/16/2021, PST',
+    );
+    expect(
+      intl
+          .datetimeFormat(DateTimeFormatOptions(
+            timeZone: timeZone,
+            timeZoneName: TimeZoneName.long,
+          ))
+          .format(date),
+      '12/16/2021, Pacific Standard Time',
+    );
+    expect(
+      intl
+          .datetimeFormat(DateTimeFormatOptions(
+            timeZone: timeZone,
+            timeZoneName: TimeZoneName.shortOffset,
+          ))
+          .format(date),
+      '12/16/2021, GMT-8',
+    );
+    expect(
+      intl
+          .datetimeFormat(DateTimeFormatOptions(
+            timeZone: timeZone,
+            timeZoneName: TimeZoneName.longOffset,
+          ))
+          .format(date),
+      '12/16/2021, GMT-08:00',
+    );
+    expect(
+      intl
+          .datetimeFormat(DateTimeFormatOptions(
+            timeZone: timeZone,
+            timeZoneName: TimeZoneName.shortGeneric,
+          ))
+          .format(date),
+      '12/16/2021, PT',
+    );
+    expect(
+      intl
+          .datetimeFormat(DateTimeFormatOptions(
+            timeZone: timeZone,
+            timeZoneName: TimeZoneName.longGeneric,
+          ))
+          .format(date),
+      '12/16/2021, Pacific Time',
+    );
+  });
+
+  testWithFormatting('day period', () {
+    final date = DateTime.utc(2021, 12, 17, 4, 0, 42);
+    expect(
+        Intl(locale: const Locale(language: 'en', region: 'GB'))
+            .datetimeFormat(const DateTimeFormatOptions(
+              hour: TimeStyle.numeric,
+              clockstyle: ClockStyle(
+                is12Hour: true,
+                startAtZero: false,
+              ),
+              dayPeriod: DayPeriod.short,
+              timeZone: 'UTC',
+            ))
+            .format(date),
+        '4 at night');
+
+    expect(
+        Intl(locale: const Locale(language: 'fr'))
+            .datetimeFormat(const DateTimeFormatOptions(
+              hour: TimeStyle.numeric,
+              clockstyle: ClockStyle(
+                is12Hour: true,
+                startAtZero: false,
+              ),
+              dayPeriod: DayPeriod.narrow,
+              timeZone: 'UTC',
+            ))
+            .format(date),
+        '4 mat.');
+
+    expect(
+        Intl(locale: const Locale(language: 'fr'))
+            .datetimeFormat(const DateTimeFormatOptions(
+              hour: TimeStyle.numeric,
+              clockstyle: ClockStyle(
+                is12Hour: true,
+                startAtZero: false,
+              ),
+              dayPeriod: DayPeriod.long,
+              timeZone: 'UTC',
+            ))
+            .format(date),
+        '4 du matin');
+  });
+
+  testWithFormatting('style', () {
+    final date = DateTime.utc(2021, 12, 17, 4, 0, 42);
+    expect(
+        Intl(locale: const Locale(language: 'en'))
+            .datetimeFormat(const DateTimeFormatOptions(
+              timeFormatStyle: TimeFormatStyle.short,
+              timeZone: 'UTC',
+            ))
+            .format(date),
+        '4:00 AM');
+    expect(
+        Intl(locale: const Locale(language: 'en'))
+            .datetimeFormat(const DateTimeFormatOptions(
+              dateFormatStyle: DateFormatStyle.short,
+              timeZone: 'UTC',
+            ))
+            .format(date),
+        '12/17/21');
+    expect(
+        Intl(locale: const Locale(language: 'en'))
+            .datetimeFormat(const DateTimeFormatOptions(
+              timeFormatStyle: TimeFormatStyle.medium,
+              dateFormatStyle: DateFormatStyle.short,
+              timeZone: 'UTC',
+            ))
+            .format(date),
+        '12/17/21, 4:00:42 AM');
+  });
+}