From da188452a6490a4fcda02698f02ed48a47617468 Mon Sep 17 00:00:00 2001 From: Albert Wolszon Date: Sat, 23 Nov 2024 19:55:24 +0100 Subject: [PATCH] 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.
Before the PR After the PR
```json { "bookingsPage_camo_dataLoaded": "CAMO data from {date} {time}.", "@bookingsPage_camo_dataLoaded": { "placeholders": { "date": { "type": "DateTime", "format": "yMMMd" }, "time": { "type": "DateTime", "format": "jm" } } }, } ``` ```json { "bookingsPage_camo_dataLoaded": "CAMO data from {date}.", "@bookingsPage_camo_dataLoaded": { "placeholders": { "date": { "type": "DateTime", "format": "yMMMd+jm" } } }, } ```
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. [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 --- AUTHORS | 1 + .../lib/src/localizations/gen_l10n.dart | 22 ++++- .../src/localizations/gen_l10n_templates.dart | 4 +- .../lib/src/localizations/gen_l10n_types.dart | 12 ++- .../generate_localizations_test.dart | 98 +++++++++++++++++++ .../test/integration.shard/gen_l10n_test.dart | 79 +++++++-------- .../test_data/gen_l10n_project.dart | 18 +++- 7 files changed, 188 insertions(+), 46 deletions(-) diff --git a/AUTHORS b/AUTHORS index 987103964b..b03820b6a5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -129,3 +129,4 @@ Flop Dimil Kalathiya Nate Wilson dy0gu +Albert Wolszon diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart index 0ef68cff42..c298bf9fed 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart @@ -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 formatParts = placeholder.dateFormatParts; + final String mainFormat = formatParts.first; + final List 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) diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart index 3f15740c46..5902159c1b 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart @@ -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)); diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart index 2bae97f0f8..e62b890409 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart @@ -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 validDateFormats = { '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 => ['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 get dateFormatParts => format?.split(_dateFormatPartsDelimiter) ?? []; + bool get hasValidDateFormat => dateFormatParts.every(validDateFormats.contains); static String? _stringAttribute( String resourceId, diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart index 55a33a294b..5d36205b53 100644 --- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart @@ -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({ + '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({ + '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({ + 'en': ''' +{ + "loggedIn": "Last logged in on {lastLoginDate}", + "@loggedIn": { + "placeholders": { + "lastLoginDate": { + "type": "DateTime", + "format": "foo+bar+baz" + } + } + } +}''' + }); + }, + throwsA(isA().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({ + 'en': ''' +{ + "loggedIn": "Last logged in on {lastLoginDate}", + "@loggedIn": { + "placeholders": { + "lastLoginDate": { + "type": "DateTime", + "format": "yMd+Hm+" + } + } + } +}''' + }); + }, + throwsA(isA().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( () { diff --git a/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart index 3567844e3d..eb45762335 100644 --- a/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart +++ b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart @@ -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' ); diff --git a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart index 3f184bb263..bee825e5f9 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart @@ -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))}', ]); }, ),