From c045b8cd8b3fd514e7e1cd0e8b2a003f00c7f398 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Mon, 27 Feb 2023 17:42:22 +0100 Subject: [PATCH] feat(i18n): add ICU parser --- packages/wyatt_i18n/example/lib/main.dart | 30 ++- .../lib/src/core/utils/i18n_parser.dart | 135 ++++++++++++++ .../lib/src/core/utils/icu_parser.dart | 173 ++++++++++++++++++ .../wyatt_i18n/lib/src/core/utils/utils.dart | 8 +- packages/wyatt_i18n/lib/src/data/data.dart | 2 +- ...ry_impl.dart => i18n_repository_impl.dart} | 24 ++- .../lib/src/domain/entities/i18n.dart | 13 +- .../lib/src/domain/entities/tokens.dart | 52 ++++++ packages/wyatt_i18n/pubspec.yaml | 2 + 9 files changed, 413 insertions(+), 26 deletions(-) create mode 100644 packages/wyatt_i18n/lib/src/core/utils/i18n_parser.dart create mode 100644 packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart rename packages/wyatt_i18n/lib/src/data/repositories/{i18_repository_impl.dart => i18n_repository_impl.dart} (84%) create mode 100644 packages/wyatt_i18n/lib/src/domain/entities/tokens.dart diff --git a/packages/wyatt_i18n/example/lib/main.dart b/packages/wyatt_i18n/example/lib/main.dart index ed462ce9..82ac6a5b 100644 --- a/packages/wyatt_i18n/example/lib/main.dart +++ b/packages/wyatt_i18n/example/lib/main.dart @@ -43,14 +43,30 @@ class App extends StatelessWidget { // test const I18nDataSource dataSource = AssetsFileDataSourceImpl(); - const I18nRepository repository = - I18RepositoryImpl(dataSource: dataSource); - - final test = (await repository.load('en')).ok; + final I18nRepository repository = + I18nRepositoryImpl(dataSource: dataSource); - print(test?.locale); - print(test?.data); - print(test?.data['btnAddFile']); + final test1 = (await repository.load(locale: 'en')).ok; + // final test2 = (await repository.loadFrom( + // Uri.parse('assets/i18n.en.yaml'), + // parser: const YamlParser(), + // )) + // .ok; + + // print(test1?.locale); + // print(test1?.data); + // print(test1?.data['btnAddFile']); + + final text = repository.get('youHavePushed', { + 'count': 8, + }); + + print(text.ok); + + + // print(test2?.locale); + // print(test2?.data); + // print(test2?.data['btnAddFile']); return 'test'; }), diff --git a/packages/wyatt_i18n/lib/src/core/utils/i18n_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/i18n_parser.dart new file mode 100644 index 00000000..6cfb1273 --- /dev/null +++ b/packages/wyatt_i18n/lib/src/core/utils/i18n_parser.dart @@ -0,0 +1,135 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:collection/collection.dart'; +import 'package:intl/intl.dart'; +import 'package:wyatt_i18n/src/core/utils/icu_parser.dart'; +import 'package:wyatt_i18n/src/domain/entities/tokens.dart'; +import 'package:wyatt_i18n/wyatt_i18n.dart'; + +class I18nParser extends Parser { + const I18nParser({ + required this.i18n, + this.arguments = const {}, + }) : super(); + + final I18n i18n; + final Map arguments; + + dynamic getArgument(String key) { + final arg = arguments[key]; + if (arg == null) { + throw ArgumentsRequiredException(key, 'value'); + } + + return arg; + } + + String pluralToString(Plural token) { + final List options = token.options; + final zero = + options.firstWhereOrNull((o) => o!.name == 'zero' || o.name == '=0'); + final one = + options.firstWhereOrNull((o) => o!.name == 'one' || o.name == '=1'); + final two = + options.firstWhereOrNull((o) => o!.name == 'two' || o.name == '=2'); + final few = options.firstWhereOrNull((o) => o!.name == 'few'); + final many = options.firstWhereOrNull((o) => o!.name == 'many'); + final other = options.firstWhereOrNull((o) => o!.name == 'other'); + final zeroStr = zero?.value.map(parsedElementToString).join() ?? ''; + final oneStr = one?.value.map(parsedElementToString).join() ?? ''; + final twoStr = two?.value.map(parsedElementToString).join() ?? ''; + final fewStr = few?.value.map(parsedElementToString).join() ?? ''; + final manyStr = many?.value.map(parsedElementToString).join() ?? ''; + final otherStr = other?.value.map(parsedElementToString).join() ?? ''; + + return Intl.plural( + getArgument(token.value) as num, + zero: zeroStr, + two: twoStr, + one: oneStr, + few: fewStr, + other: otherStr, + many: manyStr, + ); + } + + String genderToString(Gender token) { + final List options = token.options; + final other = options.firstWhereOrNull((o) => o!.name == 'other'); + final male = options.firstWhereOrNull((o) => o!.name == 'male'); + final female = options.firstWhereOrNull((o) => o!.name == 'female'); + final otherStr = other?.value.map(parsedElementToString).join() ?? ''; + final maleStr = male?.value.map(parsedElementToString).join() ?? ''; + final femaleStr = female?.value.map(parsedElementToString).join() ?? ''; + + return Intl.gender( + getArgument(token.value) as String, + other: otherStr, + female: femaleStr, + male: maleStr, + ); + } + + String selectToString(Select token) { + final Map cases = { + for (var e in token.options.map( + (o) => MapEntry( + o.name, + o.value.map(parsedElementToString).join(), + ), + )) + e.key: e.value, + }; + + return Intl.select( + getArgument(token.value) as String, + cases, + ); + } + + String parsedElementToString(Token token) { + switch (token.type) { + case TokenType.literal: + return token.value; + case TokenType.plural: + return pluralToString(token as Plural); + case TokenType.gender: + return genderToString(token as Gender); + case TokenType.argument: + return getArgument(token.value).toString(); + case TokenType.select: + return selectToString(token as Select); + } + } + + @override + String parse(String input, {String? key}) { + String? result; + try { + result = IcuParser().parse(input)?.map(parsedElementToString).join(); + if (result == null) { + throw MalformedValueException(key ?? '', input); + } + } on ArgumentsRequiredException catch (_) { + rethrow; + } catch (e) { + throw MalformedValueException(key ?? '', input); + } + + return result; + } +} diff --git a/packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart b/packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart new file mode 100644 index 00000000..8405642a --- /dev/null +++ b/packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart @@ -0,0 +1,173 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ignore_for_file: avoid_dynamic_calls, inference_failure_on_untyped_parameter + +import 'package:petitparser/petitparser.dart' hide Token; +import 'package:wyatt_i18n/src/domain/entities/tokens.dart'; + +class IcuParser { + IcuParser() { + // There is a cycle here, so we need the explicit + // set to avoid infinite recursion. + interiorText.set(contents.plus() | empty); + } + Parser get openCurly => char('{'); + + Parser get closeCurly => char('}'); + + Parser get quotedCurly => (string("'{'") | string("'}'")).map((x) => x[1]); + + Parser get icuEscapedText => quotedCurly | twoSingleQuotes; + + Parser get curly => openCurly | closeCurly; + + Parser get notAllowedInIcuText => curly | char('<'); + + Parser get icuText => notAllowedInIcuText.neg(); + + Parser get notAllowedInNormalText => char('{'); + + Parser get normalText => notAllowedInNormalText.neg(); + + Parser get messageText => + (icuEscapedText | icuText).plus().flatten().map(Literal.new); + + Parser get nonIcuMessageText => normalText.plus().flatten().map(Literal.new); + + Parser get twoSingleQuotes => string("''").map((x) => "'"); + + Parser get number => digit().plus().flatten().trim().map(int.parse); + + Parser get id => (letter() & (word() | char('_')).star()).flatten().trim(); + + Parser get comma => char(',').trim(); + + /// Given a list of possible keywords, return a rule that accepts any of them. + /// e.g., given ["male", "female", "other"], accept any of them. + Parser asKeywords(List list) => + list.map(string).cast().reduce((a, b) => a | b).flatten().trim(); + + Parser get pluralKeyword => asKeywords( + ['=0', '=1', '=2', 'zero', 'one', 'two', 'few', 'many', 'other'], + ); + + Parser get genderKeyword => asKeywords(['female', 'male', 'other']); + + SettableParser interiorText = undefined(); + + Parser get preface => (openCurly & id & comma).map((values) => values[1]); + + Parser get pluralLiteral => string('plural'); + + Parser get pluralClause => + (pluralKeyword & openCurly & interiorText & closeCurly).trim().map( + (result) => Option( + result[0] as String, + List.from( + (result[2] is List ? result[2] as List : [result[2]]) + .cast(), + ), + ), + ); + + Parser get plural => + preface & pluralLiteral & comma & pluralClause.plus() & closeCurly; + + Parser get intlPlural => plural.map( + (result) => Plural( + result[0] as String, + List