Allow add_format() in flutter gen-l10n DateTime format (#156297)
This Pull Request extends the functionality of the `flutter gen-l10n` command (and its behavior during hot restart/reload) related to `DateFormat` type placeholders and their `format`. Until now, it was impossible to take advantage of `intl`'s `DateFormat.something().add_somethingElse()`. The `.add_x()` part was impossible to achieve. This PR adds the ability to take advantage of these methods over `DateFormat`, by adding the `add_` formats after the `+` character in the `format` in placeholder configuration. You can even have multiple added format parts if needed. All within a single placeholder. <table> <tr> <th>Before the PR</th> <th>After the PR</th> </tr> <tr> <td> ```json { "bookingsPage_camo_dataLoaded": "CAMO data from {date} {time}.", "@bookingsPage_camo_dataLoaded": { "placeholders": { "date": { "type": "DateTime", "format": "yMMMd" }, "time": { "type": "DateTime", "format": "jm" } } }, } ``` </td> <td> ```json { "bookingsPage_camo_dataLoaded": "CAMO data from {date}.", "@bookingsPage_camo_dataLoaded": { "placeholders": { "date": { "type": "DateTime", "format": "yMMMd+jm" } } }, } ``` </td> </tr> </table> Resolves #155817. ## Next steps After this PR is merged, an update to [i18n | Flutter > Messages with dates](https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#messages-with-dates) ([source](https://github.com/flutter/website/blob/main/src/content/ui/accessibility-and-internationalization/internationalization.md)) shall be made to include a mention of this new addition. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Andrew Kolos <andrewrkolos@gmail.com>
This commit is contained in:
parent
773b42f4fb
commit
da188452a6
1
AUTHORS
1
AUTHORS
@ -129,3 +129,4 @@ Flop <kukuzuo@gmail.com>
|
|||||||
Dimil Kalathiya <kalathiyadimil@gmail.com>
|
Dimil Kalathiya <kalathiyadimil@gmail.com>
|
||||||
Nate Wilson <nate.w5687@gmail.com>
|
Nate Wilson <nate.w5687@gmail.com>
|
||||||
dy0gu <support@dy0gu.com>
|
dy0gu <support@dy0gu.com>
|
||||||
|
Albert Wolszon <albert@wolszon.me>
|
||||||
|
@ -169,6 +169,16 @@ String generateDateFormattingLogic(Message message, LocaleInfo locale) {
|
|||||||
final bool? isCustomDateFormat = placeholder.isCustomDateFormat;
|
final bool? isCustomDateFormat = placeholder.isCustomDateFormat;
|
||||||
if (!placeholder.hasValidDateFormat
|
if (!placeholder.hasValidDateFormat
|
||||||
&& (isCustomDateFormat == null || !isCustomDateFormat)) {
|
&& (isCustomDateFormat == null || !isCustomDateFormat)) {
|
||||||
|
if (placeholder.dateFormatParts.length > 1) {
|
||||||
|
throw L10nException(
|
||||||
|
'Date format "$placeholderFormat" for placeholder '
|
||||||
|
'${placeholder.name} contains at least one invalid date format. '
|
||||||
|
'Ensure all date formats joined by a "+" character have '
|
||||||
|
'a corresponding DateFormat constructor.\n Check the intl '
|
||||||
|
"library's DateFormat class constructors for allowed date formats."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw L10nException(
|
throw L10nException(
|
||||||
'Date format "$placeholderFormat" for placeholder '
|
'Date format "$placeholderFormat" for placeholder '
|
||||||
'${placeholder.name} does not have a corresponding DateFormat '
|
'${placeholder.name} does not have a corresponding DateFormat '
|
||||||
@ -178,9 +188,19 @@ String generateDateFormattingLogic(Message message, LocaleInfo locale) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (placeholder.hasValidDateFormat) {
|
if (placeholder.hasValidDateFormat) {
|
||||||
|
// The first format is the main format, and the rest are added formats.
|
||||||
|
final List<String> formatParts = placeholder.dateFormatParts;
|
||||||
|
final String mainFormat = formatParts.first;
|
||||||
|
final List<String> addedFormats = formatParts.skip(1).toList();
|
||||||
|
|
||||||
|
final String addedFormatsString = addedFormats.map((String addFormat) {
|
||||||
|
return dateFormatAddFormatTemplate.replaceAll('@(format)', addFormat);
|
||||||
|
}).join();
|
||||||
|
|
||||||
return dateFormatTemplate
|
return dateFormatTemplate
|
||||||
.replaceAll('@(placeholder)', placeholder.name)
|
.replaceAll('@(placeholder)', placeholder.name)
|
||||||
.replaceAll('@(format)', placeholderFormat);
|
.replaceAll('@(format)', mainFormat)
|
||||||
|
.replaceAll('@(addedFormats)', addedFormatsString);
|
||||||
}
|
}
|
||||||
return dateFormatCustomTemplate
|
return dateFormatCustomTemplate
|
||||||
.replaceAll('@(placeholder)', placeholder.name)
|
.replaceAll('@(placeholder)', placeholder.name)
|
||||||
|
@ -123,10 +123,12 @@ const String numberFormatNamedTemplate = '''
|
|||||||
''';
|
''';
|
||||||
|
|
||||||
const String dateFormatTemplate = '''
|
const String dateFormatTemplate = '''
|
||||||
final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat.@(format)(localeName);
|
final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat.@(format)(localeName)@(addedFormats);
|
||||||
final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder));
|
final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder));
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
const String dateFormatAddFormatTemplate = '''.add_@(format)()''';
|
||||||
|
|
||||||
const String dateFormatCustomTemplate = '''
|
const String dateFormatCustomTemplate = '''
|
||||||
final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat(@(format), localeName);
|
final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat(@(format), localeName);
|
||||||
final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder));
|
final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder));
|
||||||
|
@ -19,9 +19,9 @@ import 'message_parser.dart';
|
|||||||
// DateFormat.yMMMMd("en_US").format(DateTime.utc(1996, 7, 10)) results
|
// DateFormat.yMMMMd("en_US").format(DateTime.utc(1996, 7, 10)) results
|
||||||
// in the string "July 10, 1996".
|
// in the string "July 10, 1996".
|
||||||
//
|
//
|
||||||
// Since the tool generates code that uses DateFormat's constructor, it is
|
// Since the tool generates code that uses DateFormat's constructor and its
|
||||||
// necessary to verify that the constructor exists, or the
|
// add_* methods, it is necessary to verify that the constructor/method exists,
|
||||||
// tool will generate code that may cause a compile-time error.
|
// or the tool will generate code that may cause a compile-time error.
|
||||||
//
|
//
|
||||||
// See also:
|
// See also:
|
||||||
//
|
//
|
||||||
@ -72,6 +72,8 @@ const Set<String> validDateFormats = <String>{
|
|||||||
's',
|
's',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const String _dateFormatPartsDelimiter = '+';
|
||||||
|
|
||||||
// The set of number formats that can be automatically localized.
|
// The set of number formats that can be automatically localized.
|
||||||
//
|
//
|
||||||
// The localizations generation tool makes use of the intl library's
|
// The localizations generation tool makes use of the intl library's
|
||||||
@ -252,7 +254,9 @@ class Placeholder {
|
|||||||
bool get requiresNumFormatting => <String>['int', 'num', 'double'].contains(type) && format != null;
|
bool get requiresNumFormatting => <String>['int', 'num', 'double'].contains(type) && format != null;
|
||||||
bool get hasValidNumberFormat => _validNumberFormats.contains(format);
|
bool get hasValidNumberFormat => _validNumberFormats.contains(format);
|
||||||
bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
|
bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
|
||||||
bool get hasValidDateFormat => validDateFormats.contains(format);
|
// 'format' can contain a number of date time formats separated by `dateFormatPartsDelimiter`.
|
||||||
|
List<String> get dateFormatParts => format?.split(_dateFormatPartsDelimiter) ?? <String>[];
|
||||||
|
bool get hasValidDateFormat => dateFormatParts.every(validDateFormats.contains);
|
||||||
|
|
||||||
static String? _stringAttribute(
|
static String? _stringAttribute(
|
||||||
String resourceId,
|
String resourceId,
|
||||||
|
@ -1686,6 +1686,104 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
expect(content, contains(r"DateFormat('asdf o\'clock', localeName)"));
|
expect(content, contains(r"DateFormat('asdf o\'clock', localeName)"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('handles adding two valid formats', () {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"loggedIn": "Last logged in on {lastLoginDate}",
|
||||||
|
"@loggedIn": {
|
||||||
|
"placeholders": {
|
||||||
|
"lastLoginDate": {
|
||||||
|
"type": "DateTime",
|
||||||
|
"format": "yMd+jms"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
final String content = getGeneratedFileContent(locale: 'en');
|
||||||
|
expect(content, contains(r'DateFormat.yMd(localeName).add_jms()'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('handles adding three valid formats', () {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"loggedIn": "Last logged in on {lastLoginDate}",
|
||||||
|
"@loggedIn": {
|
||||||
|
"placeholders": {
|
||||||
|
"lastLoginDate": {
|
||||||
|
"type": "DateTime",
|
||||||
|
"format": "yMMMMEEEEd+QQQQ+Hm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
final String content = getGeneratedFileContent(locale: 'en');
|
||||||
|
expect(content, contains(r'DateFormat.yMMMMEEEEd(localeName).add_QQQQ().add_Hm()'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('throws an exception when adding invalid formats', (){
|
||||||
|
expect(
|
||||||
|
() {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"loggedIn": "Last logged in on {lastLoginDate}",
|
||||||
|
"@loggedIn": {
|
||||||
|
"placeholders": {
|
||||||
|
"lastLoginDate": {
|
||||||
|
"type": "DateTime",
|
||||||
|
"format": "foo+bar+baz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
throwsA(isA<L10nException>().having(
|
||||||
|
(L10nException e) => e.message,
|
||||||
|
'message',
|
||||||
|
allOf(
|
||||||
|
contains('"foo+bar+baz"'),
|
||||||
|
contains('lastLoginDate'),
|
||||||
|
contains('contains at least one invalid date format.'),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('throws an exception when adding formats and trailing plus sign', () {
|
||||||
|
expect(
|
||||||
|
() {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"loggedIn": "Last logged in on {lastLoginDate}",
|
||||||
|
"@loggedIn": {
|
||||||
|
"placeholders": {
|
||||||
|
"lastLoginDate": {
|
||||||
|
"type": "DateTime",
|
||||||
|
"format": "yMd+Hm+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
throwsA(isA<L10nException>().having(
|
||||||
|
(L10nException e) => e.message,
|
||||||
|
'message',
|
||||||
|
allOf(
|
||||||
|
contains('"yMd+Hm+"'),
|
||||||
|
contains('lastLoginDate'),
|
||||||
|
contains('contains at least one invalid date format.'),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWithoutContext('throws an exception when no format attribute is passed in', () {
|
testWithoutContext('throws an exception when no format attribute is passed in', () {
|
||||||
expect(
|
expect(
|
||||||
() {
|
() {
|
||||||
|
@ -136,45 +136,46 @@ void main() {
|
|||||||
'#l10n 75 (she)\n'
|
'#l10n 75 (she)\n'
|
||||||
'#l10n 76 (6/26/2023)\n'
|
'#l10n 76 (6/26/2023)\n'
|
||||||
'#l10n 77 (5:23:00 AM)\n'
|
'#l10n 77 (5:23:00 AM)\n'
|
||||||
'#l10n 78 (--- es ---)\n'
|
'#l10n 78 (10/6/2024 11:29:48 PM and Tuesday, July 4, 2000 12:54:32 3rd quarter)\n'
|
||||||
'#l10n 79 (ES - Hello world)\n'
|
'#l10n 79 (--- es ---)\n'
|
||||||
'#l10n 80 (ES - Hello _NEWLINE_ World)\n'
|
'#l10n 80 (ES - Hello world)\n'
|
||||||
'#l10n 81 (ES - Hola \$ Mundo)\n'
|
'#l10n 81 (ES - Hello _NEWLINE_ World)\n'
|
||||||
'#l10n 82 (ES - Hello Mundo)\n'
|
'#l10n 82 (ES - Hola \$ Mundo)\n'
|
||||||
'#l10n 83 (ES - Hola Mundo)\n'
|
'#l10n 83 (ES - Hello Mundo)\n'
|
||||||
'#l10n 84 (ES - Hello World on viernes, 1 de enero de 1960)\n'
|
'#l10n 84 (ES - Hola Mundo)\n'
|
||||||
'#l10n 85 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
|
'#l10n 85 (ES - Hello World on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 86 (ES - Hello World from 1960 to 2020)\n'
|
'#l10n 86 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
|
||||||
'#l10n 87 (ES - Hello for 123)\n'
|
'#l10n 87 (ES - Hello World from 1960 to 2020)\n'
|
||||||
'#l10n 88 (ES - Hello)\n'
|
'#l10n 88 (ES - Hello for 123)\n'
|
||||||
'#l10n 89 (ES - Hello World)\n'
|
'#l10n 89 (ES - Hello)\n'
|
||||||
'#l10n 90 (ES - Hello two worlds)\n'
|
'#l10n 90 (ES - Hello World)\n'
|
||||||
'#l10n 91 (ES - Hello)\n'
|
'#l10n 91 (ES - Hello two worlds)\n'
|
||||||
'#l10n 92 (ES - Hello nuevo World)\n'
|
'#l10n 92 (ES - Hello)\n'
|
||||||
'#l10n 93 (ES - Hello two nuevo worlds)\n'
|
'#l10n 93 (ES - Hello nuevo World)\n'
|
||||||
'#l10n 94 (ES - Hello on viernes, 1 de enero de 1960)\n'
|
'#l10n 94 (ES - Hello two nuevo worlds)\n'
|
||||||
'#l10n 95 (ES - Hello World, on viernes, 1 de enero de 1960)\n'
|
'#l10n 95 (ES - Hello on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 96 (ES - Hello two worlds, on viernes, 1 de enero de 1960)\n'
|
'#l10n 96 (ES - Hello World, on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 97 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
|
'#l10n 97 (ES - Hello two worlds, on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 98 (ES - Hello World of 101 citizens)\n'
|
'#l10n 98 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
|
||||||
'#l10n 99 (ES - Hello two worlds with 102 total citizens)\n'
|
'#l10n 99 (ES - Hello World of 101 citizens)\n'
|
||||||
'#l10n 100 (ES - [Hola] -Mundo- #123#)\n'
|
'#l10n 100 (ES - Hello two worlds with 102 total citizens)\n'
|
||||||
'#l10n 101 (ES - \$!)\n'
|
'#l10n 101 (ES - [Hola] -Mundo- #123#)\n'
|
||||||
'#l10n 102 (ES - One \$)\n'
|
'#l10n 102 (ES - \$!)\n'
|
||||||
"#l10n 103 (ES - Flutter's amazing!)\n"
|
'#l10n 103 (ES - One \$)\n'
|
||||||
"#l10n 104 (ES - Flutter's amazing, times 2!)\n"
|
"#l10n 104 (ES - Flutter's amazing!)\n"
|
||||||
'#l10n 105 (ES - Flutter is "amazing"!)\n'
|
"#l10n 105 (ES - Flutter's amazing, times 2!)\n"
|
||||||
'#l10n 106 (ES - Flutter is "amazing", times 2!)\n'
|
'#l10n 106 (ES - Flutter is "amazing"!)\n'
|
||||||
'#l10n 107 (ES - 16 wheel truck)\n'
|
'#l10n 107 (ES - Flutter is "amazing", times 2!)\n'
|
||||||
"#l10n 108 (ES - Sedan's elegance)\n"
|
'#l10n 108 (ES - 16 wheel truck)\n'
|
||||||
'#l10n 109 (ES - Cabriolet has "acceleration")\n'
|
"#l10n 109 (ES - Sedan's elegance)\n"
|
||||||
'#l10n 110 (ES - Oh, she found ES - 1 itemES - !)\n'
|
'#l10n 110 (ES - Cabriolet has "acceleration")\n'
|
||||||
'#l10n 111 (ES - Indeed, ES - they like ES - Flutter!)\n'
|
'#l10n 111 (ES - Oh, she found ES - 1 itemES - !)\n'
|
||||||
'#l10n 112 (--- es_419 ---)\n'
|
'#l10n 112 (ES - Indeed, ES - they like ES - Flutter!)\n'
|
||||||
'#l10n 113 (ES 419 - Hello World)\n'
|
'#l10n 113 (--- es_419 ---)\n'
|
||||||
'#l10n 114 (ES 419 - Hello)\n'
|
'#l10n 114 (ES 419 - Hello World)\n'
|
||||||
'#l10n 115 (ES 419 - Hello World)\n'
|
'#l10n 115 (ES 419 - Hello)\n'
|
||||||
'#l10n 116 (ES 419 - Hello two worlds)\n'
|
'#l10n 116 (ES 419 - Hello World)\n'
|
||||||
|
'#l10n 117 (ES 419 - Hello two worlds)\n'
|
||||||
'#l10n END\n'
|
'#l10n END\n'
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -413,7 +413,21 @@ dependencies:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"datetime1": "{today, date, ::yMd}",
|
"datetime1": "{today, date, ::yMd}",
|
||||||
"datetime2": "{current, time, ::jms}"
|
"datetime2": "{current, time, ::jms}",
|
||||||
|
"datetimeAddedFormats": "{firstDate} and {secondDate}",
|
||||||
|
"@datetimeAddedFormats": {
|
||||||
|
"description": "A message with two dates, with added formats",
|
||||||
|
"placeholders": {
|
||||||
|
"firstDate": {
|
||||||
|
"type": "DateTime",
|
||||||
|
"format": "yMd+jms"
|
||||||
|
},
|
||||||
|
"secondDate": {
|
||||||
|
"type": "DateTime",
|
||||||
|
"format": "yMMMMEEEEd+Hms+QQQQ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
@ -693,6 +707,7 @@ class Home extends StatelessWidget {
|
|||||||
"${localizations.selectInPlural('female', 1)}",
|
"${localizations.selectInPlural('female', 1)}",
|
||||||
'${localizations.datetime1(DateTime(2023, 6, 26))}',
|
'${localizations.datetime1(DateTime(2023, 6, 26))}',
|
||||||
'${localizations.datetime2(DateTime(2023, 6, 26, 5, 23))}',
|
'${localizations.datetime2(DateTime(2023, 6, 26, 5, 23))}',
|
||||||
|
'${localizations.datetimeAddedFormats(DateTime(2024, 10, 6, 23, 29, 48), DateTime(2000, 7, 4, 12, 54, 32))}',
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -977,6 +992,7 @@ class Home extends StatelessWidget {
|
|||||||
"${localizations.selectInPlural(gender: 'female', count: 1)}",
|
"${localizations.selectInPlural(gender: 'female', count: 1)}",
|
||||||
'${localizations.datetime1(today: DateTime(2023, 6, 26))}',
|
'${localizations.datetime1(today: DateTime(2023, 6, 26))}',
|
||||||
'${localizations.datetime2(current: DateTime(2023, 6, 26, 5, 23))}',
|
'${localizations.datetime2(current: DateTime(2023, 6, 26, 5, 23))}',
|
||||||
|
'${localizations.datetimeAddedFormats(firstDate: DateTime(2024, 10, 6, 23, 29, 48), secondDate: DateTime(2000, 7, 4, 12, 54, 32))}',
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user