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:
Albert Wolszon 2024-11-23 19:55:24 +01:00 committed by GitHub
parent 773b42f4fb
commit da188452a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 188 additions and 46 deletions

View File

@ -129,3 +129,4 @@ Flop <kukuzuo@gmail.com>
Dimil Kalathiya <kalathiyadimil@gmail.com>
Nate Wilson <nate.w5687@gmail.com>
dy0gu <support@dy0gu.com>
Albert Wolszon <albert@wolszon.me>

View File

@ -169,6 +169,16 @@ String generateDateFormattingLogic(Message message, LocaleInfo locale) {
final bool? isCustomDateFormat = placeholder.isCustomDateFormat;
if (!placeholder.hasValidDateFormat
&& (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(
'Date format "$placeholderFormat" for placeholder '
'${placeholder.name} does not have a corresponding DateFormat '
@ -178,9 +188,19 @@ String generateDateFormattingLogic(Message message, LocaleInfo locale) {
);
}
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
.replaceAll('@(placeholder)', placeholder.name)
.replaceAll('@(format)', placeholderFormat);
.replaceAll('@(format)', mainFormat)
.replaceAll('@(addedFormats)', addedFormatsString);
}
return dateFormatCustomTemplate
.replaceAll('@(placeholder)', placeholder.name)

View File

@ -123,10 +123,12 @@ const String numberFormatNamedTemplate = '''
''';
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));
''';
const String dateFormatAddFormatTemplate = '''.add_@(format)()''';
const String dateFormatCustomTemplate = '''
final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat(@(format), localeName);
final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder));

View File

@ -19,9 +19,9 @@ import 'message_parser.dart';
// DateFormat.yMMMMd("en_US").format(DateTime.utc(1996, 7, 10)) results
// in the string "July 10, 1996".
//
// Since the tool generates code that uses DateFormat's constructor, it is
// necessary to verify that the constructor exists, or the
// tool will generate code that may cause a compile-time error.
// Since the tool generates code that uses DateFormat's constructor and its
// add_* methods, it is necessary to verify that the constructor/method exists,
// or the tool will generate code that may cause a compile-time error.
//
// See also:
//
@ -72,6 +72,8 @@ const Set<String> validDateFormats = <String>{
's',
};
const String _dateFormatPartsDelimiter = '+';
// The set of number formats that can be automatically localized.
//
// 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 hasValidNumberFormat => _validNumberFormats.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(
String resourceId,

View File

@ -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)"));
});
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', () {
expect(
() {

View File

@ -136,45 +136,46 @@ void main() {
'#l10n 75 (she)\n'
'#l10n 76 (6/26/2023)\n'
'#l10n 77 (5:23:00 AM)\n'
'#l10n 78 (--- es ---)\n'
'#l10n 79 (ES - Hello world)\n'
'#l10n 80 (ES - Hello _NEWLINE_ World)\n'
'#l10n 81 (ES - Hola \$ Mundo)\n'
'#l10n 82 (ES - Hello Mundo)\n'
'#l10n 83 (ES - Hola Mundo)\n'
'#l10n 84 (ES - Hello World on viernes, 1 de enero de 1960)\n'
'#l10n 85 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
'#l10n 86 (ES - Hello World from 1960 to 2020)\n'
'#l10n 87 (ES - Hello for 123)\n'
'#l10n 88 (ES - Hello)\n'
'#l10n 89 (ES - Hello World)\n'
'#l10n 90 (ES - Hello two worlds)\n'
'#l10n 91 (ES - Hello)\n'
'#l10n 92 (ES - Hello nuevo World)\n'
'#l10n 93 (ES - Hello two nuevo worlds)\n'
'#l10n 94 (ES - Hello on viernes, 1 de enero de 1960)\n'
'#l10n 95 (ES - Hello World, on viernes, 1 de enero de 1960)\n'
'#l10n 96 (ES - Hello two worlds, on viernes, 1 de enero de 1960)\n'
'#l10n 97 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
'#l10n 98 (ES - Hello World of 101 citizens)\n'
'#l10n 99 (ES - Hello two worlds with 102 total citizens)\n'
'#l10n 100 (ES - [Hola] -Mundo- #123#)\n'
'#l10n 101 (ES - \$!)\n'
'#l10n 102 (ES - One \$)\n'
"#l10n 103 (ES - Flutter's amazing!)\n"
"#l10n 104 (ES - Flutter's amazing, times 2!)\n"
'#l10n 105 (ES - Flutter is "amazing"!)\n'
'#l10n 106 (ES - Flutter is "amazing", times 2!)\n'
'#l10n 107 (ES - 16 wheel truck)\n'
"#l10n 108 (ES - Sedan's elegance)\n"
'#l10n 109 (ES - Cabriolet has "acceleration")\n'
'#l10n 110 (ES - Oh, she found ES - 1 itemES - !)\n'
'#l10n 111 (ES - Indeed, ES - they like ES - Flutter!)\n'
'#l10n 112 (--- es_419 ---)\n'
'#l10n 113 (ES 419 - Hello World)\n'
'#l10n 114 (ES 419 - Hello)\n'
'#l10n 115 (ES 419 - Hello World)\n'
'#l10n 116 (ES 419 - Hello two worlds)\n'
'#l10n 78 (10/6/2024 11:29:48 PM and Tuesday, July 4, 2000 12:54:32 3rd quarter)\n'
'#l10n 79 (--- es ---)\n'
'#l10n 80 (ES - Hello world)\n'
'#l10n 81 (ES - Hello _NEWLINE_ World)\n'
'#l10n 82 (ES - Hola \$ Mundo)\n'
'#l10n 83 (ES - Hello Mundo)\n'
'#l10n 84 (ES - Hola Mundo)\n'
'#l10n 85 (ES - Hello World on viernes, 1 de enero de 1960)\n'
'#l10n 86 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
'#l10n 87 (ES - Hello World from 1960 to 2020)\n'
'#l10n 88 (ES - Hello for 123)\n'
'#l10n 89 (ES - Hello)\n'
'#l10n 90 (ES - Hello World)\n'
'#l10n 91 (ES - Hello two worlds)\n'
'#l10n 92 (ES - Hello)\n'
'#l10n 93 (ES - Hello nuevo World)\n'
'#l10n 94 (ES - Hello two nuevo worlds)\n'
'#l10n 95 (ES - Hello 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 two worlds, on viernes, 1 de enero de 1960)\n'
'#l10n 98 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
'#l10n 99 (ES - Hello World of 101 citizens)\n'
'#l10n 100 (ES - Hello two worlds with 102 total citizens)\n'
'#l10n 101 (ES - [Hola] -Mundo- #123#)\n'
'#l10n 102 (ES - \$!)\n'
'#l10n 103 (ES - One \$)\n'
"#l10n 104 (ES - Flutter's amazing!)\n"
"#l10n 105 (ES - Flutter's amazing, times 2!)\n"
'#l10n 106 (ES - Flutter is "amazing"!)\n'
'#l10n 107 (ES - Flutter is "amazing", times 2!)\n'
'#l10n 108 (ES - 16 wheel truck)\n'
"#l10n 109 (ES - Sedan's elegance)\n"
'#l10n 110 (ES - Cabriolet has "acceleration")\n'
'#l10n 111 (ES - Oh, she found ES - 1 itemES - !)\n'
'#l10n 112 (ES - Indeed, ES - they like ES - Flutter!)\n'
'#l10n 113 (--- es_419 ---)\n'
'#l10n 114 (ES 419 - Hello World)\n'
'#l10n 115 (ES 419 - Hello)\n'
'#l10n 116 (ES 419 - Hello World)\n'
'#l10n 117 (ES 419 - Hello two worlds)\n'
'#l10n END\n'
);

View File

@ -413,7 +413,21 @@ dependencies:
}
},
"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.datetime1(DateTime(2023, 6, 26))}',
'${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.datetime1(today: DateTime(2023, 6, 26))}',
'${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))}',
]);
},
),