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, {