feat(i18n): add ICU parser

This commit is contained in:
Hugo Pointcheval 2023-02-27 17:42:22 +01:00
parent 8a37aec127
commit 75f561a19e
Signed by: hugo
GPG Key ID: 3AAC487E131E00BC
9 changed files with 413 additions and 26 deletions

View File

@ -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';
}),

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, String> {
const I18nParser({
required this.i18n,
this.arguments = const {},
}) : super();
final I18n i18n;
final Map<String, dynamic> arguments;
dynamic getArgument(String key) {
final arg = arguments[key];
if (arg == null) {
throw ArgumentsRequiredException(key, 'value');
}
return arg;
}
String pluralToString(Plural token) {
final List<Option?> 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<Option?> 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<Object, String> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
// 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<String> list) =>
list.map(string).cast<Parser>().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<dynamic> 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<Token>.from(
(result[2] is List ? result[2] as List : [result[2]])
.cast<Token>(),
),
),
);
Parser get plural =>
preface & pluralLiteral & comma & pluralClause.plus() & closeCurly;
Parser get intlPlural => plural.map(
(result) => Plural(
result[0] as String,
List<Option>.from(result[3] as Iterable<dynamic>),
),
);
Parser get selectLiteral => string('select');
Parser get genderClause =>
(genderKeyword & openCurly & interiorText & closeCurly).trim().map(
(result) => Option(
result[0] as String,
List<Token>.from(
(result[2] is List ? result[2] as List : [result[2]])
.cast<Token>(),
),
),
);
Parser get gender =>
preface & selectLiteral & comma & genderClause.plus() & closeCurly;
Parser get intlGender => gender.map(
(result) => Gender(
result[0] as String,
List<Option>.from(result[3] as Iterable<dynamic>),
),
);
Parser get selectClause =>
(id & openCurly & interiorText & closeCurly).trim().map(
(result) => Option(
result[0] as String,
List<Token>.from(
(result[2] is List ? result[2] as List : [result[2]])
.cast<Token>(),
),
),
);
Parser get generalSelect =>
preface & selectLiteral & comma & selectClause.plus() & closeCurly;
Parser get intlSelect => generalSelect.map(
(result) => Select(
result[0] as String,
List<Option>.from(result[3] as Iterable<dynamic>),
),
);
Parser get compound => (((parameter | nonIcuMessageText).plus() &
pluralOrGenderOrSelect &
(pluralOrGenderOrSelect | parameter | nonIcuMessageText).star()) |
(pluralOrGenderOrSelect &
(pluralOrGenderOrSelect | parameter | nonIcuMessageText).plus()))
.map((result) => result.expand((x) => x is List ? x : [x]).toList());
Parser get pluralOrGenderOrSelect => intlPlural | intlGender | intlSelect;
Parser get contents => pluralOrGenderOrSelect | parameter | messageText;
Parser get simpleText =>
(nonIcuMessageText | parameter | openCurly).plus().map(
(result) => result
.map((item) => item is String ? Literal(item) : item)
.toList(),
);
Parser get empty => epsilon().map((_) => Literal(''));
Parser get parameter => (openCurly & id & closeCurly)
.map((result) => Argument(result[1] as String));
List<Token>? parse(String message) {
final parsed = (compound | pluralOrGenderOrSelect | simpleText | empty)
.map(
(result) => List<Token>.from(result is List ? result : [result]),
)
.parse(message);
return parsed.isSuccess ? parsed.value : null;
}
}

View File

@ -1,20 +1,22 @@
// 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 <https://www.gnu.org/licenses/>.
export 'arb_parser.dart';
export 'i18n_parser.dart';
export 'icu_parser.dart';
export 'json_parser.dart';
export 'parser.dart';
export 'yaml_parser.dart';

View File

@ -16,4 +16,4 @@
export 'data_sources/assets_file_data_source_impl.dart';
export 'data_sources/network_data_source_impl.dart';
export 'repositories/i18_repository_impl.dart';
export 'repositories/i18n_repository_impl.dart';

View File

@ -15,15 +15,17 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_i18n/src/core/utils/i18n_parser.dart';
import 'package:wyatt_i18n/wyatt_i18n.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart' hide Option;
class I18RepositoryImpl extends I18nRepository {
const I18RepositoryImpl({
class I18nRepositoryImpl extends I18nRepository {
I18nRepositoryImpl({
required this.dataSource,
}) : super();
final I18nDataSource dataSource;
I18n _i18n = const I18n.empty();
Future<I18n> _parse(
String content,
@ -81,7 +83,7 @@ class I18RepositoryImpl extends I18nRepository {
() async {
final content = await dataSource.load(locale: locale);
return _parse(
return _i18n = await _parse(
content,
parser ?? dataSource.format.parser,
locale: locale,
@ -102,7 +104,7 @@ class I18RepositoryImpl extends I18nRepository {
/// Strict is always true when loading from a uri. Because
/// the locale is not given and can't be inferred.
return _parse(
return _i18n = await _parse(
content,
parser ?? dataSource.format.parser,
strict: true,
@ -116,7 +118,15 @@ class I18RepositoryImpl extends I18nRepository {
String key, [
Map<String, dynamic> arguments = const {},
]) {
// TODO: implement get
throw UnimplementedError();
final I18nParser parser = I18nParser(i18n: _i18n, arguments: arguments);
if (_i18n.containsKey(key)) {
return Result.tryCatch<String, AppException, AppException>(
() => parser.parse(_i18n[key] as String, key: key),
(error) => error,
);
} else {
throw KeyNotFoundException(key, arguments);
}
}
}

View File

@ -24,6 +24,9 @@ class I18n extends Entity {
required this.data,
}) : super();
/// Creates an empty i18n file.
const I18n.empty() : this(locale: '', unparsedData: '', data: const {});
/// The locale of the i18n file.
final String locale;
@ -33,13 +36,7 @@ class I18n extends Entity {
/// The data of the i18n file.
final Map<String, dynamic> data;
String operator [](String key) {
final value = data[key];
bool containsKey(String key) => data.containsKey(key);
if (value is String) {
return value;
} else {
throw Exception('Invalid i18n key: $key');
}
}
dynamic operator [](String key) => data[key];
}

View File

@ -0,0 +1,52 @@
// 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 <https://www.gnu.org/licenses/>.
enum TokenType { literal, argument, plural, gender, select }
class Token {
Token(this.type, this.value);
TokenType type;
String value;
}
class Option {
Option(this.name, this.value);
String name;
List<Token> value;
}
class Literal extends Token {
Literal(String value) : super(TokenType.literal, value);
}
class Argument extends Token {
Argument(String value) : super(TokenType.argument, value);
}
class Gender extends Token {
Gender(String value, this.options) : super(TokenType.gender, value);
List<Option> options;
}
class Plural extends Token {
Plural(String value, this.options) : super(TokenType.plural, value);
List<Option> options;
}
class Select extends Token {
Select(String value, this.options) : super(TokenType.select, value);
List<Option> options;
}

View File

@ -7,7 +7,9 @@ environment:
sdk: ">=2.19.0 <3.0.0"
dependencies:
collection: ^1.17.0
flutter: {sdk: flutter}
intl: ^0.18.0
path: ^1.8.0
petitparser: ^5.1.0
wyatt_architecture: