Skip to content

Commit

Permalink
Add support for display names from ICU4X (#800)
Browse files Browse the repository at this point in the history
* Add support for display names from ICU4X

* Update icu4x

* Revert update of submodule

* Update to ICU4X changes

* Add tests

* Fix js

* Fix workflow

* fix checkout path

* Switch to using locales

* Skip non-working

* Add changelog entry

* Tweak messages

* Add check in readme
  • Loading branch information
mosuem authored Mar 8, 2024
1 parent c6c6c25 commit 9918522
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 22 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/intl4x.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ jobs:
runs-on: ubuntu-latest

env:
ICU4X_BUILD_MODE: fetch
ICU4X_BUILD_MODE: checkout
LOCAL_ICU4X_CHECKOUT: ${{ github.workspace }}/submodules/icu4x

defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions pkgs/intl4x/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Add ICU4X support for number formatting.
- Add resource identifier annotations.
- Add ICU4X support for plural rules.
- Add ICU4X support for display names.

## 0.8.1

Expand Down
2 changes: 1 addition & 1 deletion pkgs/intl4x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ via our [issue tracker](https://github.com/dart-lang/i18n/issues)).
| | 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: | | |
| **ICU4X (web/native)** | :heavy_check_mark: | | | :heavy_check_mark: | :heavy_check_mark: | |

## Implementation and Goals

Expand Down
31 changes: 26 additions & 5 deletions pkgs/intl4x/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ Unknown build mode for icu4x. Set the `ICU4X_BUILD_MODE` environment variable wi
path: AssetAbsolutePath(Uri.file(libPath)),
)
],
dependencies: Dependencies([Uri.file('build.dart')]),
dependencies: Dependencies([
...buildMode.dependencies,
Uri.file('build.dart'),
]),
).writeToFile(outDir: config.outDir);
}

Expand All @@ -64,6 +67,8 @@ void unzipFirstFile({required File input, required File output}) {
}

sealed class BuildMode {
List<Uri> get dependencies;

Future<void> build();
}

Expand Down Expand Up @@ -98,6 +103,9 @@ final class FetchMode implements BuildMode {
return 'ubuntu';
}
}

@override
List<Uri> get dependencies => [];
}

final class LocalMode implements BuildMode {
Expand All @@ -111,26 +119,37 @@ final class LocalMode implements BuildMode {
Future<void> build() async {
await File(_localBinaryPath).copy(libPath);
}

@override
List<Uri> get dependencies => [Uri.file(_localBinaryPath)];
}

final class CheckoutMode implements BuildMode {
final BuildConfig config;
final String libPath;
CheckoutMode(this.config, this.libPath);

String? get workingDirectory => Platform.environment['LOCAL_ICU4X_CHECKOUT'];

@override
Future<void> build() async {
final workingDirectory = Platform.environment['LOCAL_ICU4X_CHECKOUT'];
if (workingDirectory == null) {
throw ArgumentError('Specify the ICU4X checkout folder'
'with the LOCAL_ICU4X_CHECKOUT variable');
}
final lib = await buildLib(
config,
workingDirectory,
workingDirectory!,
);
await File(lib).copy(libPath);
}

@override
List<Uri> get dependencies => Directory(workingDirectory!)
.listSync()
.whereType<File>()
.map((e) => Uri.file(e.path))
.toList();
}

Future<String> buildLib(
Expand Down Expand Up @@ -165,13 +184,15 @@ Future<String> buildLib(
'buffer_provider',
'logging',
'simple_logger',
'experimental_components',
];
final noStdFeatures = [
'default_components',
'compiled_data',
'buffer_provider',
'libc-alloc',
'panic-handler'
'panic-handler',
'experimental_components',
];
final tempDir = Directory.systemTemp.createTempSync();
final linkModeType =
Expand All @@ -181,7 +202,7 @@ Future<String> buildLib(
final arguments = [
if (isNoStd) '+nightly',
'rustc',
'-p={crateName}',
'-p=$crateName',
'--crate-type=$linkModeType',
'--release',
'--config=profile.release.panic="abort"',
Expand Down
6 changes: 6 additions & 0 deletions pkgs/intl4x/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
tags:
icu4xUnimplemented:
on_platform:
vm:
skip: "Not implemented in ICU4X yet. Use -P force to force tests."
presets: { force: { skip: false } }
62 changes: 49 additions & 13 deletions pkgs/intl4x/lib/src/display_names/display_names_4x.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +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 '../../display_names.dart';
import '../bindings/lib.g.dart' as icu;
import '../data.dart';
import '../data_4x.dart';
import '../locale/locale.dart';
import '../options.dart';
import '../locale/locale_4x.dart';
import 'display_names_impl.dart';
import 'display_names_options.dart';

DisplayNamesImpl getDisplayNames4X(
Locale locale,
Expand All @@ -16,35 +18,69 @@ DisplayNamesImpl getDisplayNames4X(
DisplayNames4X(locale, data, options);

class DisplayNames4X extends DisplayNamesImpl {
DisplayNames4X(super.locale, Data data, super.options);
final icu.LocaleDisplayNamesFormatter _formatter;
final icu.RegionDisplayNames _regionFormatter;

DisplayNames4X(super.locale, Data data, super.options)
: _formatter = icu.LocaleDisplayNamesFormatter(
data.to4X(),
locale.to4X(),
options.to4X(),
),
_regionFormatter = icu.RegionDisplayNames(
data.to4X(),
locale.to4X(),
);

@override
String ofCalendar(Calendar calendar) {
throw UnimplementedError('Insert diplomat bindings here');
throw UnsupportedError('Not supported by ICU4X yet.');
}

@override
String ofCurrency(String currencyCode) {
throw UnimplementedError('Insert diplomat bindings here');
throw UnsupportedError('Not supported by ICU4X yet.');
}

@override
String ofDateTime(DateTimeField field) {
throw UnimplementedError('Insert diplomat bindings here');
throw UnsupportedError('Not supported by ICU4X yet.');
}

@override
String ofLanguage(Locale locale) {
throw UnimplementedError('Insert diplomat bindings here');
}
String ofLanguage(Locale locale) => _formatter.of(locale.to4X());

@override
String ofRegion(String regionCode) {
throw UnimplementedError('Insert diplomat bindings here');
}
String ofRegion(String regionCode) => _regionFormatter.of(regionCode);

@override
String ofScript(String scriptCode) {
throw UnimplementedError('Insert diplomat bindings here');
throw UnsupportedError('Not supported by ICU4X yet.');
}
}

extension on DisplayNamesOptions {
icu.DisplayNamesOptions to4X() {
final icuStyle = switch (style) {
Style.narrow => icu.DisplayNamesStyle.narrow,
Style.short => icu.DisplayNamesStyle.short,
Style.long => icu.DisplayNamesStyle.long,
};

final icuFallback = switch (fallback) {
Fallback.code => icu.DisplayNamesFallback.code,
Fallback.none => icu.DisplayNamesFallback.none,
};

final icuLanguageDisplay = switch (languageDisplay) {
LanguageDisplay.dialect => icu.LanguageDisplay.dialect,
LanguageDisplay.standard => icu.LanguageDisplay.standard,
};

return icu.DisplayNamesOptions(
style: icuStyle,
fallback: icuFallback,
languageDisplay: icuLanguageDisplay,
);
}
}
3 changes: 2 additions & 1 deletion pkgs/intl4x/lib/src/display_names/display_names_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import '../ecma/ecma_policy.dart';
import '../locale/locale.dart';
import '../options.dart';
import '../utils.dart';
import 'display_names_4x.dart';
import 'display_names_4x.dart'
if (dart.library.js) 'display_names_stub_4x.dart';
import 'display_names_options.dart';
import 'display_names_stub.dart' if (dart.library.js) 'display_names_ecma.dart';

Expand Down
15 changes: 15 additions & 0 deletions pkgs/intl4x/lib/src/display_names/display_names_stub_4x.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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 '../../display_names.dart';
import '../data.dart';
import '../locale/locale.dart';
import 'display_names_impl.dart';

DisplayNamesImpl getDisplayNames4X(
Locale locale,
Data data,
DisplayNamesOptions options,
) =>
throw UnimplementedError('Cannot use ICU4X in web environments.');
141 changes: 141 additions & 0 deletions pkgs/intl4x/test/display_names_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// 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/display_names.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'))
.displayNames()
.ofLanguage(const Locale(language: 'de', region: 'DE')),
'German (Germany)');
});

testWithFormatting('language', () {
String languageOf(Locale locale, Locale language) => Intl(locale: locale)
.displayNames(const DisplayNamesOptions(style: Style.long))
.ofLanguage(language);

const en = Locale(language: 'en');
const fr = Locale(language: 'fr');
const de = Locale(language: 'de');
const zh = Locale(language: 'zh', script: 'Hant');
expect(languageOf(en, fr), 'French');
expect(languageOf(en, de), 'German');
expect(languageOf(en, const Locale(language: 'fr', region: 'CA')),
'Canadian French');
//TODO(mosuem): Skip as ECMA seems to have a bug here.
// expect(languageOf(en, zh), 'Traditional Chinese');
expect(languageOf(en, const Locale(language: 'en', region: 'US')),
'American English');
//TODO(mosuem): Skip as ECMA seems to have a bug here.
// expect(languageOf(en, const Locale(language: 'zh', region: 'TW')),
// 'Chinese (Taiwan)');

expect(languageOf(zh, fr), '法文');
expect(languageOf(zh, const Locale(language: 'zh')), '中文');
expect(languageOf(zh, de), '德文');
});

testWithFormatting(
'language with languageDisplay',
() {
String languageWith(LanguageDisplay display) =>
Intl(locale: const Locale(language: 'en'))
.displayNames(DisplayNamesOptions(languageDisplay: display))
.ofLanguage(const Locale(language: 'en', region: 'GB'));

expect(languageWith(LanguageDisplay.dialect), 'British English');
expect(
languageWith(LanguageDisplay.standard), 'English (United Kingdom)');
},
tags: ['icu4xUnimplemented'],
);

testWithFormatting(
'calendar',
() {
final displayNames =
Intl(locale: const Locale(language: 'en')).displayNames();

expect(displayNames.ofCalendar(Calendar.roc), 'Minguo Calendar');
expect(displayNames.ofCalendar(Calendar.gregory), 'Gregorian Calendar');
expect(displayNames.ofCalendar(Calendar.chinese), 'Chinese Calendar');
},
tags: ['icu4xUnimplemented'],
);

testWithFormatting(
'dateTimeField',
() {
final displayNames =
Intl(locale: const Locale(language: 'pt')).displayNames();
expect(displayNames.ofDateTime(DateTimeField.era), 'era');
expect(displayNames.ofDateTime(DateTimeField.year), 'ano');
expect(displayNames.ofDateTime(DateTimeField.month), 'mês');
expect(displayNames.ofDateTime(DateTimeField.quarter), 'trimestre');
expect(displayNames.ofDateTime(DateTimeField.weekOfYear), 'semana');
expect(displayNames.ofDateTime(DateTimeField.weekday), 'dia da semana');
expect(displayNames.ofDateTime(DateTimeField.dayPeriod), 'AM/PM');
expect(displayNames.ofDateTime(DateTimeField.day), 'dia');
expect(displayNames.ofDateTime(DateTimeField.hour), 'hora');
expect(displayNames.ofDateTime(DateTimeField.minute), 'minuto');
expect(displayNames.ofDateTime(DateTimeField.second), 'segundo');
},
tags: ['icu4xUnimplemented'],
);

testWithFormatting(
'currency',
() {
expect(
Intl(locale: const Locale(language: 'pt'))
.displayNames()
.ofCurrency('USD'),
'Dólar americano',
);
},
tags: ['icu4xUnimplemented'],
);

testWithFormatting(
'script',
() {
expect(
Intl(locale: const Locale(language: 'fr'))
.displayNames()
.ofScript('Egyp'),
'hiéroglyphes égyptiens',
);
},
tags: ['icu4xUnimplemented'],
);

testWithFormatting('region', () {
String regionNames(Locale locale, String region) =>
Intl(locale: locale).displayNames().ofRegion(region);

const en = Locale(language: 'en');
expect(regionNames(en, '419'), 'Latin America');
expect(regionNames(en, 'BZ'), 'Belize');
expect(regionNames(en, 'US'), 'United States');
expect(regionNames(en, 'BA'), 'Bosnia & Herzegovina');
expect(regionNames(en, 'MM'), 'Myanmar (Burma)');

const zh = Locale(language: 'zh', script: 'Hant');
expect(regionNames(zh, '419'), '拉丁美洲');
expect(regionNames(zh, 'BZ'), '貝里斯');
expect(regionNames(zh, 'US'), '美國');
expect(regionNames(zh, 'BA'), '波士尼亞與赫塞哥維納');
expect(regionNames(zh, 'MM'), '緬甸');

const es = Locale(language: 'es', region: '419');
expect(regionNames(es, 'DE'), 'Alemania');
});
}
2 changes: 1 addition & 1 deletion submodules/icu4x
Submodule icu4x updated 1213 files

0 comments on commit 9918522

Please sign in to comment.