Add intl4x list format (#677)
* Add list format
* Add test
* Add readme and changelog
* Dart fix
* Add vm test
* Add license header to file
* Changes as per review
* Use web coverage
diff --git a/.github/workflows/health.yaml b/.github/workflows/health.yaml
index 6d77298..51b7227 100644
--- a/.github/workflows/health.yaml
+++ b/.github/workflows/health.yaml
@@ -6,3 +6,5 @@
jobs:
health:
uses: dart-lang/ecosystem/.github/workflows/health.yaml@main
+ with:
+ coverage_web: true
diff --git a/pkgs/intl4x/CHANGELOG.md b/pkgs/intl4x/CHANGELOG.md
index aac3189..a28e814 100644
--- a/pkgs/intl4x/CHANGELOG.md
+++ b/pkgs/intl4x/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.0
+
+- Add list format.
+
## 0.1.0
- Initial version.
diff --git a/pkgs/intl4x/README.md b/pkgs/intl4x/README.md
index c948fbf..1754861 100644
--- a/pkgs/intl4x/README.md
+++ b/pkgs/intl4x/README.md
@@ -14,7 +14,7 @@
## Status
| | Number format | List format | Date format | Collation | Display names |
|---|:---:|:---:|:---:|:---:|:---:|
-| **ECMA402 (web)** | :heavy_check_mark: | | | :heavy_check_mark: | |
+| **ECMA402 (web)** | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | |
| **ICU4X (web/native)** | | | | | |
diff --git a/pkgs/intl4x/lib/intl4x.dart b/pkgs/intl4x/lib/intl4x.dart
index c2cfc45..a5315e6 100644
--- a/pkgs/intl4x/lib/intl4x.dart
+++ b/pkgs/intl4x/lib/intl4x.dart
@@ -8,8 +8,10 @@
import 'src/data.dart';
import 'src/ecma/ecma_policy.dart';
import 'src/ecma/ecma_stub.dart' if (dart.library.js) 'src/ecma/ecma_web.dart';
+import 'src/list_format/list_format.dart';
+import 'src/list_format/list_format_impl.dart';
+import 'src/list_format/list_format_options.dart';
import 'src/locale.dart';
-import 'src/number_format/number_format.dart';
import 'src/number_format/number_format_impl.dart';
import 'src/options.dart';
@@ -49,6 +51,11 @@
NumberFormatImpl.build(currentLocale, localeMatcher, ecmaPolicy),
);
+ ListFormat listFormat([ListFormatOptions? options]) => ListFormat(
+ options ?? const ListFormatOptions(),
+ ListFormatImpl.build(currentLocale, localeMatcher, ecmaPolicy),
+ );
+
/// Construct an [Intl] instance providing the current [currentLocale] 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
new file mode 100644
index 0000000..f67fa01
--- /dev/null
+++ b/pkgs/intl4x/lib/list_format.dart
@@ -0,0 +1,6 @@
+// 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/list_format/list_format.dart';
+export 'src/list_format/list_format_options.dart';
diff --git a/pkgs/intl4x/lib/number_format.dart b/pkgs/intl4x/lib/number_format.dart
index 971782f..e977cfc 100644
--- a/pkgs/intl4x/lib/number_format.dart
+++ b/pkgs/intl4x/lib/number_format.dart
@@ -2,4 +2,5 @@
// 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/number_format/number_format.dart';
export 'src/number_format/number_format_options.dart';
diff --git a/pkgs/intl4x/lib/src/list_format/list_format.dart b/pkgs/intl4x/lib/src/list_format/list_format.dart
new file mode 100644
index 0000000..786d6e6
--- /dev/null
+++ b/pkgs/intl4x/lib/src/list_format/list_format.dart
@@ -0,0 +1,32 @@
+// 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';
+import '../test_checker.dart';
+import 'list_format_impl.dart';
+import 'list_format_options.dart';
+
+class ListFormat {
+ final ListFormatOptions _options;
+ final ListFormatImpl _listFormatImpl;
+
+ const ListFormat(this._options, this._listFormatImpl);
+
+ /// Locale-dependant concatenation of lists, for example in `en-US` locale:
+ /// ```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,
+ }) {
+ if (isInTest) {
+ return '${list.join(', ')}//${_listFormatImpl.locale}';
+ } else {
+ return _listFormatImpl.formatImpl(list, _options);
+ }
+ }
+}
diff --git a/pkgs/intl4x/lib/src/list_format/list_format_4x.dart b/pkgs/intl4x/lib/src/list_format/list_format_4x.dart
new file mode 100644
index 0000000..e8f833b
--- /dev/null
+++ b/pkgs/intl4x/lib/src/list_format/list_format_4x.dart
@@ -0,0 +1,18 @@
+// 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 'list_format_impl.dart';
+import 'list_format_options.dart';
+
+ListFormatImpl getListFormatter4X(Locale locale) => ListFormat4X(locale);
+
+class ListFormat4X extends ListFormatImpl {
+ ListFormat4X(super.locale);
+
+ @override
+ String formatImpl(List<String> list, ListFormatOptions options) {
+ throw UnimplementedError('Insert diplomat bindings here');
+ }
+}
diff --git a/pkgs/intl4x/lib/src/list_format/list_format_ecma.dart b/pkgs/intl4x/lib/src/list_format/list_format_ecma.dart
new file mode 100644
index 0000000..3170ef8
--- /dev/null
+++ b/pkgs/intl4x/lib/src/list_format/list_format_ecma.dart
@@ -0,0 +1,69 @@
+// 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 '../utils.dart';
+import 'list_format_impl.dart';
+import 'list_format_options.dart';
+
+ListFormatImpl? getListFormatterECMA(
+ Locale locale,
+ LocaleMatcher localeMatcher,
+) =>
+ _ListFormatECMA.tryToBuild(locale, localeMatcher);
+
+@JS('Intl.ListFormat')
+class ListFormatJS {
+ external factory ListFormatJS([List<String> locale, Object options]);
+ external String format(List<String> list);
+}
+
+@JS('Intl.ListFormat.supportedLocalesOf')
+external List<String> supportedLocalesOfJS(
+ List<String> listOfLocales, [
+ Object options,
+]);
+
+class _ListFormatECMA extends ListFormatImpl {
+ _ListFormatECMA(super.locales);
+
+ static ListFormatImpl? tryToBuild(
+ Locale locale,
+ LocaleMatcher localeMatcher,
+ ) {
+ final supportedLocales = supportedLocalesOf(locale, localeMatcher);
+ return supportedLocales.isNotEmpty
+ ? _ListFormatECMA(supportedLocales.first)
+ : null;
+ }
+
+ static List<String> supportedLocalesOf(
+ String locale,
+ LocaleMatcher localeMatcher,
+ ) {
+ final o = newObject<Object>();
+ setProperty(o, 'localeMatcher', localeMatcher.jsName);
+ return List.from(supportedLocalesOfJS([localeToJsFormat(locale)], o));
+ }
+
+ @override
+ String formatImpl(List<String> list, ListFormatOptions options) {
+ return ListFormatJS([localeToJsFormat(locale)], options.toJsOptions())
+ .format(list);
+ }
+}
+
+extension on ListFormatOptions {
+ Object toJsOptions() {
+ final o = newObject<Object>();
+ setProperty(o, 'localeMatcher', localeMatcher.jsName);
+ setProperty(o, 'type', type.name);
+ setProperty(o, 'style', style.name);
+ return o;
+ }
+}
diff --git a/pkgs/intl4x/lib/src/list_format/list_format_impl.dart b/pkgs/intl4x/lib/src/list_format/list_format_impl.dart
new file mode 100644
index 0000000..1192ea4
--- /dev/null
+++ b/pkgs/intl4x/lib/src/list_format/list_format_impl.dart
@@ -0,0 +1,33 @@
+// 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_policy.dart';
+import '../ecma/ecma_policy.dart';
+import '../locale.dart';
+import '../options.dart';
+import '../utils.dart';
+import 'list_format_4x.dart';
+import 'list_format_options.dart';
+import 'list_format_stub.dart' if (dart.library.js) 'list_format_ecma.dart';
+
+abstract class ListFormatImpl {
+ final Locale locale;
+
+ ListFormatImpl(this.locale);
+
+ String formatImpl(List<String> list, ListFormatOptions options);
+
+ factory ListFormatImpl.build(
+ Locale locales,
+ LocaleMatcher localeMatcher,
+ EcmaPolicy ecmaPolicy,
+ ) =>
+ buildFormatter(
+ locales,
+ localeMatcher,
+ ecmaPolicy,
+ getListFormatterECMA,
+ getListFormatter4X,
+ );
+}
diff --git a/pkgs/intl4x/lib/src/list_format/list_format_options.dart b/pkgs/intl4x/lib/src/list_format/list_format_options.dart
new file mode 100644
index 0000000..f8faa3e
--- /dev/null
+++ b/pkgs/intl4x/lib/src/list_format/list_format_options.dart
@@ -0,0 +1,42 @@
+// 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';
+
+class ListFormatOptions {
+ final Type type;
+ final ListStyle style;
+ final LocaleMatcher localeMatcher;
+
+ const ListFormatOptions({
+ this.type = Type.conjunction,
+ this.style = ListStyle.long,
+ this.localeMatcher = LocaleMatcher.bestfit,
+ });
+}
+
+/// Indicates the type of grouping.
+enum Type {
+ /// For "and"-based grouping of the list items: "A, B, and C".
+ conjunction,
+
+ /// For "or"-based grouping of the list items: "A, B, or C".
+ disjunction,
+
+ /// 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/list_format/list_format_stub.dart b/pkgs/intl4x/lib/src/list_format/list_format_stub.dart
new file mode 100644
index 0000000..931ee5c
--- /dev/null
+++ b/pkgs/intl4x/lib/src/list_format/list_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 'list_format_impl.dart';
+
+ListFormatImpl? getListFormatterECMA(
+ Locale locale,
+ LocaleMatcher localeMatcher,
+) =>
+ throw UnimplementedError('Cannot use ECMA outside of web environments.');
diff --git a/pkgs/intl4x/pubspec.yaml b/pkgs/intl4x/pubspec.yaml
index cdcd527..4f38c35 100644
--- a/pkgs/intl4x/pubspec.yaml
+++ b/pkgs/intl4x/pubspec.yaml
@@ -1,7 +1,7 @@
name: intl4x
description: >-
A lightweight modular library for internationalization (i18n) functionality.
-version: 0.1.0
+version: 0.2.0
repository: https://github.com/dart-lang/i18n/tree/main/pkgs/intl4x
environment:
diff --git a/pkgs/intl4x/test/ecma/list_format_test.dart b/pkgs/intl4x/test/ecma/list_format_test.dart
new file mode 100644
index 0000000..b37a76e
--- /dev/null
+++ b/pkgs/intl4x/test/ecma/list_format_test.dart
@@ -0,0 +1,70 @@
+// 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/ecma_policy.dart';
+import 'package:intl4x/intl4x.dart';
+import 'package:intl4x/src/list_format/list_format_options.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+ group('List style options', () {
+ final list = ['A', 'B', 'C'];
+ final intl = Intl(defaultLocale: 'en_US');
+ testWithFormatting('long', () {
+ final listFormat =
+ intl.listFormat(const ListFormatOptions(style: ListStyle.long));
+ expect(listFormat.format(list), 'A, B, and C');
+ });
+ testWithFormatting('short', () {
+ final listFormat =
+ intl.listFormat(const ListFormatOptions(style: ListStyle.short));
+ expect(listFormat.format(list), 'A, B, & C');
+ });
+ testWithFormatting('narrow', () {
+ final listFormat =
+ intl.listFormat(const ListFormatOptions(style: ListStyle.narrow));
+ expect(listFormat.format(list), 'A, B, C');
+ });
+ });
+
+ group('List type options', () {
+ final list = ['A', 'B', 'C'];
+ final intl = Intl(defaultLocale: 'en_US');
+ testWithFormatting('long', () {
+ final listFormat =
+ intl.listFormat(const ListFormatOptions(type: Type.conjunction));
+ expect(listFormat.format(list), 'A, B, and C');
+ });
+ testWithFormatting('short', () {
+ final listFormat =
+ intl.listFormat(const ListFormatOptions(type: Type.disjunction));
+ expect(listFormat.format(list), 'A, B, or C');
+ });
+ testWithFormatting('narrow', () {
+ final listFormat =
+ intl.listFormat(const ListFormatOptions(type: Type.unit));
+ expect(listFormat.format(list), 'A, B, C');
+ });
+ });
+
+ group('List style and type combinations', () {
+ final list = ['A', 'B', 'C'];
+ final intl = Intl(ecmaPolicy: const AlwaysEcma(), defaultLocale: 'en_US');
+ testWithFormatting('long', () {
+ final formatter = intl.listFormat(const ListFormatOptions(
+ style: ListStyle.narrow, type: Type.conjunction));
+ expect(formatter.format(list), 'A, B, C');
+ });
+ testWithFormatting('short', () {
+ final formatter = intl.listFormat(
+ const ListFormatOptions(style: ListStyle.short, type: Type.unit));
+ expect(formatter.format(list), 'A, B, C');
+ });
+ });
+}
diff --git a/pkgs/intl4x/test/icu4x/list_format_test.dart b/pkgs/intl4x/test/icu4x/list_format_test.dart
new file mode 100644
index 0000000..bea93dc
--- /dev/null
+++ b/pkgs/intl4x/test/icu4x/list_format_test.dart
@@ -0,0 +1,29 @@
+// 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('vm')
+library;
+
+import 'package:intl4x/intl4x.dart';
+import 'package:intl4x/list_format.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+ final list = ['A', 'B', 'C'];
+ test('Does not compare in tests', () {
+ final locale = 'de_DE';
+ final listFormatGerman = Intl(defaultLocale: locale)
+ .listFormat(const ListFormatOptions(style: ListStyle.long));
+ expect(listFormatGerman.format(list), '${list.join(', ')}//$locale');
+ });
+
+ testWithFormatting('long', () {
+ final intl = Intl(defaultLocale: 'en_US');
+ final listFormat =
+ intl.listFormat(const ListFormatOptions(style: ListStyle.long));
+ expect(() => listFormat.format(list), throwsA(isA<UnimplementedError>()));
+ });
+}