feat(i18n): add ICU parser
This commit is contained in:
parent
8a37aec127
commit
75f561a19e
@ -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';
|
||||
}),
|
||||
|
135
packages/wyatt_i18n/lib/src/core/utils/i18n_parser.dart
Normal file
135
packages/wyatt_i18n/lib/src/core/utils/i18n_parser.dart
Normal 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;
|
||||
}
|
||||
}
|
173
packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart
Normal file
173
packages/wyatt_i18n/lib/src/core/utils/icu_parser.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
52
packages/wyatt_i18n/lib/src/domain/entities/tokens.dart
Normal file
52
packages/wyatt_i18n/lib/src/domain/entities/tokens.dart
Normal 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;
|
||||
}
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user