Add `DateTime` formatting for ICU4X (#788)

* Add `DateTime` formatting for ICU4X

* Adapt to testing

* Adapting tests

* Add skips to test

* Fix `islamic` calendar match

* Update ICU4X

* Address comments

* Refactor

* Refactor the other method too

* Add checks to readme

* Prepare for publish

* Topics to list

* Update SDK dep

* Fix pana issues

* Update example
diff --git a/pkgs/intl4x/CHANGELOG.md b/pkgs/intl4x/CHANGELOG.md
index 61741f0..923b6ba 100644
--- a/pkgs/intl4x/CHANGELOG.md
+++ b/pkgs/intl4x/CHANGELOG.md
@@ -1,10 +1,11 @@
-## 0.8.2-wip
+## 0.8.2
 
 - Add ICU4X support for number formatting.
 - Add resource identifier annotations.
 - Add ICU4X support for plural rules.
 - Add ICU4X support for display names.
 - Add ICU4X support for list formatting.
+- Add ICU4X support for datetime formatting.
 
 ## 0.8.1
 
diff --git a/pkgs/intl4x/README.md b/pkgs/intl4x/README.md
index 9af3f19..f0f5ecf 100644
--- a/pkgs/intl4x/README.md
+++ b/pkgs/intl4x/README.md
@@ -18,7 +18,7 @@
 |   | Number format  | List format  | Date format  | Collation  | Display names | Plural Rules |
 |---|:---:|:---:|:---:|:---:|:---:|:---:|
 | **ECMA402 (web)** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
-| **ICU4X (web/native)**  | :heavy_check_mark: | :heavy_check_mark: |   | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| **ICU4X (web/native)**  | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
 
 ## Implementation and Goals
 
diff --git a/pkgs/intl4x/example/pubspec.yaml b/pkgs/intl4x/example/pubspec.yaml
index 5fb4fc0..5380f1d 100644
--- a/pkgs/intl4x/example/pubspec.yaml
+++ b/pkgs/intl4x/example/pubspec.yaml
@@ -9,10 +9,8 @@
 dependencies:
   intl4x: 
     path: ../
-  js: ^0.6.7
+  js: ^0.7.1
 
 dev_dependencies:
-  build_runner: ^2.4.0
-  build_web_compilers: ^3.0.0
   dart_flutter_team_lints: ^1.0.0
   lints: ^2.0.0
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_4x.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_4x.dart
index f9e3f62..a42b7fc 100644
--- a/pkgs/intl4x/lib/src/datetime_format/datetime_format_4x.dart
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_4x.dart
@@ -2,10 +2,13 @@
 // 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 '../../datetime_format.dart';
+import '../bindings/lib.g.dart' as icu;
 import '../data.dart';
+import '../data_4x.dart';
 import '../locale/locale.dart';
+import '../locale/locale_4x.dart';
 import 'datetime_format_impl.dart';
-import 'datetime_format_options.dart';
 
 DateTimeFormatImpl getDateTimeFormatter4X(
   Locale locale,
@@ -15,10 +18,143 @@
     DateTimeFormat4X(locale, data, options);
 
 class DateTimeFormat4X extends DateTimeFormatImpl {
-  DateTimeFormat4X(super.locale, Data data, super.options);
+  final icu.DateTimeFormatter? _dateTimeFormatter;
+  final icu.DateFormatter? _dateFormatter;
+  final icu.TimeFormatter? _timeFormatter;
+  final icu.ZonedDateTimeFormatter? _zonedDateTimeFormatter;
+  final icu.DataProvider _data;
+
+  DateTimeFormat4X(super.locale, Data data, super.options)
+      : _data = data.to4X(),
+        _dateTimeFormatter = _setDateTimeFormatter(options, data, locale),
+        _timeFormatter = options.timeFormatStyle != null
+            ? icu.TimeFormatter.withLength(
+                data.to4X(),
+                locale.to4X(),
+                options.dateFormatStyle?.timeTo4xOptions() ??
+                    icu.TimeLength.short,
+              )
+            : null,
+        _dateFormatter = _setDateFormatter(options, data, locale),
+        _zonedDateTimeFormatter = options.timeZone != null
+            ? icu.ZonedDateTimeFormatter.withLengths(
+                data.to4X(),
+                locale.to4X(),
+                options.dateFormatStyle?.dateTo4xOptions() ??
+                    icu.DateLength.short, //TODO: Check defaults
+                options.timeFormatStyle?.timeTo4xOptions() ??
+                    icu.TimeLength.short, //TODO: Check defaults
+              )
+            : null;
+
+  static icu.DateTimeFormatter? _setDateTimeFormatter(
+    DateTimeFormatOptions options,
+    Data data,
+    Locale locale,
+  ) {
+    final dateFormatStyle = options.dateFormatStyle;
+    final timeFormatStyle = options.timeFormatStyle;
+
+    if (dateFormatStyle == null || timeFormatStyle == null) {
+      return null;
+    }
+
+    return icu.DateTimeFormatter.withLengths(
+      data.to4X(),
+      locale.to4X(),
+      dateFormatStyle.dateTo4xOptions(),
+      timeFormatStyle.timeTo4xOptions(),
+    );
+  }
+
+  static icu.DateFormatter? _setDateFormatter(
+    DateTimeFormatOptions options,
+    Data data,
+    Locale locale,
+  ) {
+    final dateFormatStyle = options.dateFormatStyle;
+    final timeFormatStyle = options.timeFormatStyle;
+
+    if (dateFormatStyle == null && timeFormatStyle != null) {
+      return null;
+    }
+
+    return icu.DateFormatter.withLength(
+      data.to4X(),
+      locale.to4X(),
+      dateFormatStyle?.dateTo4xOptions() ?? icu.DateLength.short,
+    );
+  }
 
   @override
   String formatImpl(DateTime datetime) {
-    throw UnimplementedError('Insert diplomat bindings here');
+    final calendarKind = options.calendar?.to4x() ?? icu.AnyCalendarKind.iso;
+    final isoDateTime = icu.DateTime.fromIsoInCalendar(
+      datetime.year,
+      datetime.month,
+      datetime.day,
+      datetime.hour,
+      datetime.minute,
+      datetime.second,
+      datetime.microsecond * 1000,
+      icu.Calendar.forKind(_data, calendarKind),
+    );
+    if (_zonedDateTimeFormatter != null) {
+      final ianaToBcp47Mapper = icu.IanaToBcp47Mapper(_data);
+      final timeZone = icu.CustomTimeZone.empty()
+        ..trySetIanaTimeZoneId(ianaToBcp47Mapper, options.timeZone!);
+      return _zonedDateTimeFormatter.formatDatetimeWithCustomTimeZone(
+        isoDateTime,
+        timeZone,
+      );
+    } else if (_dateTimeFormatter != null) {
+      return _dateTimeFormatter.formatDatetime(isoDateTime);
+    } else if (_dateFormatter != null) {
+      return _dateFormatter.formatDatetime(isoDateTime);
+    } else if (_timeFormatter != null) {
+      return _timeFormatter.formatDatetime(isoDateTime);
+    } else {
+      throw UnimplementedError(
+          'Custom skeletons are not yet supported in ICU4X. '
+          'Either date or time formatting has to be enabled.');
+    }
   }
 }
+
+extension on TimeFormatStyle {
+  icu.TimeLength timeTo4xOptions() => switch (this) {
+        TimeFormatStyle.full => icu.TimeLength.full,
+        TimeFormatStyle.long => icu.TimeLength.long,
+        TimeFormatStyle.medium => icu.TimeLength.medium,
+        TimeFormatStyle.short => icu.TimeLength.short,
+      };
+  icu.DateLength dateTo4xOptions() => switch (this) {
+        TimeFormatStyle.full => icu.DateLength.full,
+        TimeFormatStyle.long => icu.DateLength.long,
+        TimeFormatStyle.medium => icu.DateLength.medium,
+        TimeFormatStyle.short => icu.DateLength.short,
+      };
+}
+
+extension on Calendar {
+  icu.AnyCalendarKind to4x() => switch (this) {
+        Calendar.buddhist => icu.AnyCalendarKind.buddhist,
+        Calendar.chinese => icu.AnyCalendarKind.chinese,
+        Calendar.coptic => icu.AnyCalendarKind.coptic,
+        Calendar.dangi => icu.AnyCalendarKind.dangi,
+        Calendar.ethioaa => icu.AnyCalendarKind.ethiopianAmeteAlem,
+        Calendar.ethiopic => icu.AnyCalendarKind.ethiopian,
+        Calendar.gregory => icu.AnyCalendarKind.gregorian,
+        Calendar.hebrew => icu.AnyCalendarKind.hebrew,
+        Calendar.indian => icu.AnyCalendarKind.indian,
+        Calendar.islamic => icu.AnyCalendarKind.islamicObservational,
+        Calendar.islamicUmalqura => icu.AnyCalendarKind.islamicUmmAlQura,
+        Calendar.islamicTbla => icu.AnyCalendarKind.islamicTabular,
+        Calendar.islamicCivil => icu.AnyCalendarKind.islamicCivil,
+        Calendar.islamicRgsa => icu.AnyCalendarKind.islamicObservational,
+        Calendar.iso8601 => icu.AnyCalendarKind.iso,
+        Calendar.japanese => icu.AnyCalendarKind.japanese,
+        Calendar.persian => icu.AnyCalendarKind.persian,
+        Calendar.roc => icu.AnyCalendarKind.roc,
+      };
+}
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart
index 0a4f295..a84a386 100644
--- a/pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_impl.dart
@@ -9,10 +9,11 @@
 import '../locale/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';
+import 'datetime_format_stub_4x.dart'
+    if (dart.library.io) 'datetime_format_4x.dart';
 
 /// This is an intermediate to defer to the actual implementations of
 /// datetime formatting.
diff --git a/pkgs/intl4x/lib/src/datetime_format/datetime_format_stub_4x.dart b/pkgs/intl4x/lib/src/datetime_format/datetime_format_stub_4x.dart
new file mode 100644
index 0000000..5f33090
--- /dev/null
+++ b/pkgs/intl4x/lib/src/datetime_format/datetime_format_stub_4x.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2024, 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 '../data.dart';
+import '../locale/locale.dart';
+import 'datetime_format_impl.dart';
+import 'datetime_format_options.dart';
+
+DateTimeFormatImpl getDateTimeFormatter4X(
+  Locale locale,
+  Data data,
+  DateTimeFormatOptions options,
+) =>
+    throw UnimplementedError('Cannot use ICU4X in web environments.');
diff --git a/pkgs/intl4x/pubspec.yaml b/pkgs/intl4x/pubspec.yaml
index a378faa..42d6869 100644
--- a/pkgs/intl4x/pubspec.yaml
+++ b/pkgs/intl4x/pubspec.yaml
@@ -1,27 +1,32 @@
 name: intl4x
 description: >-
   A lightweight modular library for internationalization (i18n) functionality.
-version: 0.8.2-wip
+version: 0.8.2
 repository: https://github.com/dart-lang/i18n/tree/main/pkgs/intl4x
-platforms: ## TODO: Add native platforms once ICU4X is integrated.
+platforms:
   web:
+  android:
+  ios:
+  linux:
+  macos:
+  windows:
+topics:
+  - i18n
 
 environment:
-  sdk: ">=3.0.0 <4.0.0"
+  sdk: ">=3.3.0 <4.0.0"
 
 dependencies:
   ffi: ^2.1.0
-  js: ^0.6.5
+  js: ^0.7.1
   meta: ^1.12.0
 
 dev_dependencies:
   archive: ^3.4.10
   args: ^2.4.2
-  build_runner: ^2.1.4
-  build_web_compilers: ^3.2.1
   collection: ^1.18.0
-  dart_flutter_team_lints: ^1.0.0
-  lints: ^2.0.0
-  native_assets_cli: ^0.3.2
+  dart_flutter_team_lints: ^2.1.1
+  lints: ^3.0.0
+  native_assets_cli: ^0.4.2
   path: ^1.9.0
-  test: ^1.22.1
\ No newline at end of file
+  test: ^1.22.1
diff --git a/pkgs/intl4x/test/datetime_format_test.dart b/pkgs/intl4x/test/datetime_format_test.dart
new file mode 100644
index 0000000..b08e9d2
--- /dev/null
+++ b/pkgs/intl4x/test/datetime_format_test.dart
@@ -0,0 +1,169 @@
+// 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: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');
+    },
+    tags: ['icu4xUnimplemented'],
+  );
+
+  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',
+      );
+    },
+    tags: ['icu4xUnimplemented'],
+  );
+
+  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');
+    },
+    tags: ['icu4xUnimplemented'],
+  );
+
+  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');
+    },
+    tags: ['icu4xUnimplemented'],
+  );
+}
diff --git a/pkgs/intl4x/test/ecma/datetime_format_test.dart b/pkgs/intl4x/test/ecma/datetime_format_test.dart
deleted file mode 100644
index c1be29b..0000000
--- a/pkgs/intl4x/test/ecma/datetime_format_test.dart
+++ /dev/null
@@ -1,156 +0,0 @@
-// 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');
-  });
-}
diff --git a/pkgs/intl4x/test/utils.dart b/pkgs/intl4x/test/utils.dart
index eaccc11..c91252b 100644
--- a/pkgs/intl4x/test/utils.dart
+++ b/pkgs/intl4x/test/utils.dart
@@ -4,8 +4,10 @@
 
 import 'dart:async';
 
+import 'package:meta/meta.dart';
 import 'package:test/test.dart';
 
+@isTest
 void testWithFormatting<T>(
   dynamic description,
   T Function() body, {