Skip to content

Commit

Permalink
Add intl4x list format (#677)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mosuem authored Jul 17, 2023
1 parent 522b34f commit 1dee979
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/health.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ on:
jobs:
health:
uses: dart-lang/ecosystem/.github/workflows/health.yaml@main
with:
coverage_web: true
4 changes: 4 additions & 0 deletions pkgs/intl4x/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.0

- Add list format.

## 0.1.0

- Initial version.
Expand Down
2 changes: 1 addition & 1 deletion pkgs/intl4x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)** | | | | | |


Expand Down
9 changes: 8 additions & 1 deletion pkgs/intl4x/lib/intl4x.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions pkgs/intl4x/lib/list_format.dart
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions pkgs/intl4x/lib/number_format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
32 changes: 32 additions & 0 deletions pkgs/intl4x/lib/src/list_format/list_format.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
18 changes: 18 additions & 0 deletions pkgs/intl4x/lib/src/list_format/list_format_4x.dart
Original file line number Diff line number Diff line change
@@ -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');
}
}
69 changes: 69 additions & 0 deletions pkgs/intl4x/lib/src/list_format/list_format_ecma.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
33 changes: 33 additions & 0 deletions pkgs/intl4x/lib/src/list_format/list_format_impl.dart
Original file line number Diff line number Diff line change
@@ -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,
);
}
42 changes: 42 additions & 0 deletions pkgs/intl4x/lib/src/list_format/list_format_options.dart
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions pkgs/intl4x/lib/src/list_format/list_format_stub.dart
Original file line number Diff line number Diff line change
@@ -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.');
2 changes: 1 addition & 1 deletion pkgs/intl4x/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
70 changes: 70 additions & 0 deletions pkgs/intl4x/test/ecma/list_format_test.dart
Original file line number Diff line number Diff line change
@@ -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');
});
});
}
29 changes: 29 additions & 0 deletions pkgs/intl4x/test/icu4x/list_format_test.dart
Original file line number Diff line number Diff line change
@@ -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>()));
});
}

0 comments on commit 1dee979

Please sign in to comment.