gen_l10n localizations date formatting (simple messages) (#47006)

* Implement date parsing for the gen_l10n tool
diff --git a/dev/tools/localization/gen_l10n.dart b/dev/tools/localization/gen_l10n.dart
index 972e981..3bab7be 100644
--- a/dev/tools/localization/gen_l10n.dart
+++ b/dev/tools/localization/gen_l10n.dart
@@ -136,7 +136,7 @@
 ''';
 
 const String simpleMethodTemplate = '''
-  String @methodName(@methodParameters) {
+  String @methodName(@methodParameters) {@dateFormatting
     return Intl.message(
       @message,
       locale: _localeName,
@@ -153,6 +153,96 @@
   }
 ''';
 
+// The set of date formats that can be automatically localized.
+//
+// The localizations generation tool makes use of the intl library's
+// DateFormat class to properly format dates based on the locale, the
+// desired format, as well as the passed in [DateTime]. For example, using
+// 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.
+//
+// See also:
+//
+// * <https://pub.dev/packages/intl>
+// * <https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html>
+// * <https://api.dartlang.org/stable/2.7.0/dart-core/DateTime-class.html>
+const Set<String> allowableDateFormats = <String>{
+  'd',
+  'E',
+  'EEEE',
+  'LLL',
+  'LLLL',
+  'M',
+  'Md',
+  'MEd',
+  'MMM',
+  'MMMd',
+  'MMMEd',
+  'MMMM',
+  'MMMMd',
+  'MMMMEEEEd',
+  'QQQ',
+  'QQQQ',
+  'y',
+  'yM',
+  'yMd',
+  'yMEd',
+  'yMMM',
+  'yMMMd',
+  'yMMMEd',
+  'yMMMM',
+  'yMMMMd',
+  'yMMMMEEEEd',
+  'yQQQ',
+  'yQQQQ',
+  'H',
+  'Hm',
+  'Hms',
+  'j',
+  'jm',
+  'jms',
+  'jmv',
+  'jmz',
+  'jv',
+  'jz',
+  'm',
+  'ms',
+  's',
+};
+
+bool _isDateParameter(dynamic placeholderValue) {
+  return placeholderValue is Map<String, dynamic> &&
+    placeholderValue['type'] == 'DateTime';
+}
+
+bool _dateParameterIsValid(Map<String, dynamic> placeholderValue, String placeholder) {
+  if (allowableDateFormats.contains(placeholderValue['format']))
+    return true;
+  throw L10nException(
+    'Date format ${placeholderValue['format']} for the $placeholder \n'
+    'placeholder does not have a corresponding DateFormat \n'
+    'constructor. Check the intl library\'s DateFormat class \n'
+    'constructors for allowed date formats.'
+  );
+}
+
+bool _containsFormatKey(Map<String, dynamic> placeholderValue, String placeholder) {
+  if (placeholderValue.containsKey('format'))
+    return true;
+  throw L10nException(
+    'The placeholder, $placeholder, has its "type" resource attribute set to '
+    'the "DateTime" type. To properly resolve for the right DateTime format, '
+    'the "format" attribute needs to be set to determine which DateFormat to '
+    'use. \n'
+    'Check the intl library\'s DateFormat class constructors for allowed '
+    'date formats.'
+  );
+}
+
 List<String> genMethodParameters(Map<String, dynamic> bundle, String key, String type) {
   final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
   if (attributesMap != null && attributesMap.containsKey('placeholders')) {
@@ -162,6 +252,30 @@
   return <String>[];
 }
 
+String generateDateFormattingLogic(Map<String, dynamic> bundle, String key) {
+  String result = '';
+  final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
+  if (attributesMap != null && attributesMap.containsKey('placeholders')) {
+    final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>;
+    for (String placeholder in placeholders.keys) {
+      final dynamic value = placeholders[placeholder];
+      if (
+        _isDateParameter(value) &&
+        _containsFormatKey(value, placeholder) &&
+        _dateParameterIsValid(value, placeholder)
+      ) {
+        result += '''
+
+    final DateFormat ${placeholder}DateFormat = DateFormat.${value['format']}(_localeName);
+    final String ${placeholder}String = ${placeholder}DateFormat.format($placeholder);
+''';
+      }
+    }
+  }
+
+  return result;
+}
+
 List<String> genIntlMethodArgs(Map<String, dynamic> bundle, String key) {
   final List<String> attributes = <String>['name: \'$key\''];
   final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
@@ -173,7 +287,20 @@
     if (attributesMap.containsKey('placeholders')) {
       final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>;
       if (placeholders.isNotEmpty) {
-        final String args = placeholders.keys.join(', ');
+        final List<String> argumentList = <String>[];
+        for (String placeholder in placeholders.keys) {
+          final dynamic value = placeholders[placeholder];
+          if (
+            _isDateParameter(value) &&
+            _containsFormatKey(value, placeholder) &&
+            _dateParameterIsValid(value, placeholder)
+          ) {
+            argumentList.add('${placeholder}String');
+          } else {
+            argumentList.add(placeholder);
+          }
+        }
+        final String args = argumentList.join(', ');
         attributes.add('args: <Object>[$args]');
       }
     }
@@ -186,8 +313,14 @@
     String message = bundle[key] as String;
     final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
     final Map<String, dynamic> placeholders = attributesMap['placeholders'] as Map<String, dynamic>;
-    for (String placeholder in placeholders.keys)
-      message = message.replaceAll('{$placeholder}', '\$$placeholder');
+    for (String placeholder in placeholders.keys) {
+      final dynamic value = placeholders[placeholder];
+      if (_isDateParameter(value)) {
+        message = message.replaceAll('{$placeholder}', '\$${placeholder}String');
+      } else {
+        message = message.replaceAll('{$placeholder}', '\$$placeholder');
+      }
+    }
     return generateString(message);
   }
 
@@ -202,6 +335,7 @@
     return simpleMethodTemplate
       .replaceAll('@methodName', key)
       .replaceAll('@methodParameters', genMethodParameters(bundle, key, 'Object').join(', '))
+      .replaceAll('@dateFormatting', generateDateFormattingLogic(bundle, key))
       .replaceAll('@message', '${genSimpleMethodMessage(bundle, key)}')
       .replaceAll('@intlMethodArgs', genIntlMethodArgs(bundle, key).join(',\n      '));
   }
diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart
index 2aca69d..1114b46 100644
--- a/dev/tools/test/localization/gen_l10n_test.dart
+++ b/dev/tools/test/localization/gen_l10n_test.dart
@@ -554,6 +554,237 @@
 ''');
     });
 
+    test('correctly generates simple message with dates', () {
+      const String singleDateMessageArbFileString = '''{
+  "springBegins": "Spring begins on {springStartDate}",
+  "@springBegins": {
+      "description": "The first day of spring",
+      "placeholders": {
+          "springStartDate": {
+              "type": "DateTime",
+              "format": "yMMMMEEEEd"
+          }
+      }
+  }
+}''';
+      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
+        ..createSync(recursive: true);
+      l10nDirectory.childFile(defaultTemplateArbFileName)
+        .writeAsStringSync(singleDateMessageArbFileString);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          l10nDirectoryPath: defaultArbPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.parseArbFiles();
+        generator.generateClassMethods();
+      } on Exception catch (e) {
+        fail('Parsing template arb file should succeed: \n$e');
+      }
+
+      expect(generator.classMethods, isNotEmpty);
+      expect(
+        generator.classMethods.first,
+        '''  String springBegins(Object springStartDate) {
+    final DateFormat springStartDateDateFormat = DateFormat.yMMMMEEEEd(_localeName);
+    final String springStartDateString = springStartDateDateFormat.format(springStartDate);
+
+    return Intl.message(
+      r'Spring begins on \$springStartDateString',
+      locale: _localeName,
+      name: 'springBegins',
+      desc: r'The first day of spring',
+      args: <Object>[springStartDateString]
+    );
+  }
+''');
+    });
+
+    test('throws an exception when improperly formatted date is passed in', () {
+      const String singleDateMessageArbFileString = '''{
+  "springBegins": "Spring begins on {springStartDate}",
+  "@springBegins": {
+      "description": "The first day of spring",
+      "placeholders": {
+          "springStartDate": {
+              "type": "DateTime",
+              "format": "asdf"
+          }
+      }
+  }
+}''';
+      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
+        ..createSync(recursive: true);
+      l10nDirectory.childFile(defaultTemplateArbFileName)
+        .writeAsStringSync(singleDateMessageArbFileString);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          l10nDirectoryPath: defaultArbPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.parseArbFiles();
+        generator.generateClassMethods();
+      } on L10nException catch (e) {
+        expect(e.message, contains('asdf'));
+        expect(e.message, contains('springStartDate'));
+        expect(e.message, contains('does not have a corresponding DateFormat'));
+        return;
+      }
+
+      fail('Improper date formatting should throw an exception');
+    });
+
+    test('throws an exception when no format attribute is passed in', () {
+      const String singleDateMessageArbFileString = '''{
+  "springBegins": "Spring begins on {springStartDate}",
+  "@springBegins": {
+      "description": "The first day of spring",
+      "placeholders": {
+          "springStartDate": {
+              "type": "DateTime"
+          }
+      }
+  }
+}''';
+      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
+        ..createSync(recursive: true);
+      l10nDirectory.childFile(defaultTemplateArbFileName)
+        .writeAsStringSync(singleDateMessageArbFileString);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          l10nDirectoryPath: defaultArbPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.parseArbFiles();
+        generator.generateClassMethods();
+      } on L10nException catch (e) {
+        expect(e.message, contains('the "format" attribute needs to be set'));
+        return;
+      }
+
+      fail('Improper date formatting should throw an exception');
+    });
+
+    test('correctly generates simple message with date along with other placeholders', () {
+      const String singleDateMessageArbFileString = '''{
+  "springGreetings": "Since it's {springStartDate}, it's finally spring! {helloWorld}!",
+  "@springGreetings": {
+      "description": "A realization that it's finally the spring season, followed by a greeting.",
+      "placeholders": {
+          "springStartDate": {
+              "type": "DateTime",
+              "format": "yMMMMEEEEd"
+          },
+          "helloWorld": {}
+      }
+  }
+}''';
+      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
+        ..createSync(recursive: true);
+      l10nDirectory.childFile(defaultTemplateArbFileName)
+        .writeAsStringSync(singleDateMessageArbFileString);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          l10nDirectoryPath: defaultArbPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.parseArbFiles();
+        generator.generateClassMethods();
+      } on Exception catch (e) {
+        fail('Parsing template arb file should succeed: \n$e');
+      }
+
+      expect(generator.classMethods, isNotEmpty);
+      expect(
+        generator.classMethods.first,
+        '''  String springGreetings(Object springStartDate, Object helloWorld) {
+    final DateFormat springStartDateDateFormat = DateFormat.yMMMMEEEEd(_localeName);
+    final String springStartDateString = springStartDateDateFormat.format(springStartDate);
+
+    return Intl.message(
+      r\'Since it\' "\'" r\'s \$springStartDateString, it\' "\'" r\'s finally spring! \$helloWorld!\',
+      locale: _localeName,
+      name: 'springGreetings',
+      desc: r\'A realization that it\' "\'" r\'s finally the spring season, followed by a greeting.\',
+      args: <Object>[springStartDateString, helloWorld]
+    );
+  }
+''');
+    });
+
+    test('correctly generates simple message with multiple dates', () {
+      const String singleDateMessageArbFileString = '''{
+  "springRange": "Spring begins on {springStartDate} and ends on {springEndDate}",
+  "@springRange": {
+      "description": "The range of dates for spring in the year",
+      "placeholders": {
+          "springStartDate": {
+              "type": "DateTime",
+              "format": "yMMMMEEEEd"
+          },
+          "springEndDate": {
+              "type": "DateTime",
+              "format": "yMMMMEEEEd"
+          }
+      }
+  }
+}''';
+      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
+        ..createSync(recursive: true);
+      l10nDirectory.childFile(defaultTemplateArbFileName)
+        .writeAsStringSync(singleDateMessageArbFileString);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          l10nDirectoryPath: defaultArbPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.parseArbFiles();
+        generator.generateClassMethods();
+      } on Exception catch (e) {
+        fail('Parsing template arb file should succeed: \n$e');
+      }
+
+      expect(generator.classMethods, isNotEmpty);
+      expect(
+        generator.classMethods.first,
+        '''  String springRange(Object springStartDate, Object springEndDate) {
+    final DateFormat springStartDateDateFormat = DateFormat.yMMMMEEEEd(_localeName);
+    final String springStartDateString = springStartDateDateFormat.format(springStartDate);
+
+    final DateFormat springEndDateDateFormat = DateFormat.yMMMMEEEEd(_localeName);
+    final String springEndDateString = springEndDateDateFormat.format(springEndDate);
+
+    return Intl.message(
+      r\'Spring begins on \$springStartDateString and ends on \$springEndDateString\',
+      locale: _localeName,
+      name: 'springRange',
+      desc: r\'The range of dates for spring in the year\',
+      args: <Object>[springStartDateString, springEndDateString]
+    );
+  }
+''');
+    });
+
     test('correctly generates a plural message:', () {
       const String singlePluralMessageArbFileString = '''{
   "helloWorlds": "{count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}",