diff --git a/.github/workflows/health.yaml b/.github/workflows/health.yaml index 6d772981..51b72275 100644 --- a/.github/workflows/health.yaml +++ b/.github/workflows/health.yaml @@ -6,3 +6,5 @@ on: 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 aac3189c..a28e8140 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 c948fbf3..17548615 100644 --- a/pkgs/intl4x/README.md +++ b/pkgs/intl4x/README.md @@ -14,7 +14,7 @@ A lightweight modular library for internationalization (i18n) functionality. ## 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 c2cfc45b..a5315e63 100644 --- a/pkgs/intl4x/lib/intl4x.dart +++ b/pkgs/intl4x/lib/intl4x.dart @@ -8,8 +8,10 @@ import 'src/collation/collation_impl.dart'; 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 @@ class Intl { 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 00000000..f67fa01b --- /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 971782ff..e977cfca 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 00000000..786d6e6b --- /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 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 00000000..e8f833b1 --- /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 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 00000000..3170ef8a --- /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 locale, Object options]); + external String format(List list); +} + +@JS('Intl.ListFormat.supportedLocalesOf') +external List supportedLocalesOfJS( + List 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 supportedLocalesOf( + String locale, + LocaleMatcher localeMatcher, + ) { + final o = newObject(); + setProperty(o, 'localeMatcher', localeMatcher.jsName); + return List.from(supportedLocalesOfJS([localeToJsFormat(locale)], o)); + } + + @override + String formatImpl(List list, ListFormatOptions options) { + return ListFormatJS([localeToJsFormat(locale)], options.toJsOptions()) + .format(list); + } +} + +extension on ListFormatOptions { + Object toJsOptions() { + final o = newObject(); + 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 00000000..1192ea45 --- /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 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 00000000..f8faa3ed --- /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 00000000..931ee5c5 --- /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 cdcd5271..4f38c35d 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 00000000..b37a76e5 --- /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 00000000..bea93dc8 --- /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())); + }); +}