[gen-l10n] Remove need for ignoring two lints in generated code (#78778)

* Remove need for unused import for placeholder braces

* Remove need for unused intl import for when plurals aren't used in the generated code
diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
index a65dc2e..3d26363 100644
--- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
+++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
@@ -200,7 +200,7 @@
     '=2': 'two',
     'few': 'few',
     'many': 'many',
-    'other': 'other'
+    'other': 'other',
   };
 
   final List<String> pluralLogicArgs = <String>[];
@@ -211,9 +211,19 @@
       String argValue = generateString(match.group(2));
       for (final Placeholder placeholder in message.placeholders) {
         if (placeholder != countPlaceholder && placeholder.requiresFormatting) {
-          argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}');
+          argValue = argValue.replaceAll(
+            '#${placeholder.name}#',
+            _needsCurlyBracketStringInterpolation(argValue, placeholder.name)
+              ? '\${${placeholder.name}String}'
+              : '\$${placeholder.name}String'
+          );
         } else {
-          argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}');
+          argValue = argValue.replaceAll(
+            '#${placeholder.name}#',
+            _needsCurlyBracketStringInterpolation(argValue, placeholder.name)
+              ? '\${${placeholder.name}}'
+              : '\$${placeholder.name}'
+          );
         }
       }
       pluralLogicArgs.add('      ${pluralIds[pluralKey]}: $argValue');
@@ -238,14 +248,61 @@
     .replaceAll('@(none)\n', '');
 }
 
+bool _needsCurlyBracketStringInterpolation(String messageString, String placeholder) {
+  final int placeholderIndex = messageString.indexOf(placeholder);
+  // This means that this message does not contain placeholders/parameters,
+  // since one was not found in the message.
+  if (placeholderIndex == -1) {
+    return false;
+  }
+
+  final bool isPlaceholderEndOfSubstring = placeholderIndex + placeholder.length + 2 == messageString.length;
+
+  if (placeholderIndex > 2 && !isPlaceholderEndOfSubstring) {
+    // Normal case
+    // Examples:
+    // "'The number of {hours} elapsed is: 44'" // no curly brackets.
+    // "'哈{hours}哈'" // no curly brackets.
+    // "'m#hours#m'" // curly brackets.
+    // "'I have to work _#hours#_' sometimes." // curly brackets.
+    final RegExp commonCaseRE = RegExp('[^a-zA-Z_][#{]$placeholder[#}][^a-zA-Z_]');
+    return !commonCaseRE.hasMatch(messageString);
+  } else if (placeholderIndex == 2) {
+    // Example:
+    // "'{hours} elapsed.'" // no curly brackets
+    // '#placeholder# ' // no curly brackets
+    // '#placeholder#m' // curly brackets
+    final RegExp startOfString = RegExp('[#{]$placeholder[#}][^a-zA-Z_]');
+    return !startOfString.hasMatch(messageString);
+  } else {
+    // Example:
+    // "'hours elapsed: {hours}'"
+    // "'Time elapsed: {hours}'" // no curly brackets
+    // ' #placeholder#' // no curly brackets
+    // 'm#placeholder#' // curly brackets
+    final RegExp endOfString = RegExp('[^a-zA-Z_][#{]$placeholder[#}]');
+    return !endOfString.hasMatch(messageString);
+  }
+}
+
 String generateMethod(Message message, AppResourceBundle bundle) {
   String generateMessage() {
     String messageValue = generateString(bundle.translationFor(message));
     for (final Placeholder placeholder in message.placeholders) {
       if (placeholder.requiresFormatting) {
-        messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}String}');
+        messageValue = messageValue.replaceAll(
+          '{${placeholder.name}}',
+          _needsCurlyBracketStringInterpolation(messageValue, placeholder.name)
+            ? '\${${placeholder.name}String}'
+            : '\$${placeholder.name}String'
+        );
       } else {
-        messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}}');
+        messageValue = messageValue.replaceAll(
+          '{${placeholder.name}}',
+          _needsCurlyBracketStringInterpolation(messageValue, placeholder.name)
+            ? '\${${placeholder.name}}'
+            : '\$${placeholder.name}'
+        );
       }
     }
 
@@ -964,7 +1021,8 @@
       .replaceAll('@(fileName)', fileName)
       .replaceAll('@(class)', '$className${locale.camelCase()}')
       .replaceAll('@(localeName)', locale.toString())
-      .replaceAll('@(methods)', methods.join('\n\n'));
+      .replaceAll('@(methods)', methods.join('\n\n'))
+      .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '');
   }
 
   String _generateSubclass(
@@ -1103,9 +1161,12 @@
       .replaceAll('@(supportedLocales)', supportedLocalesCode.join(',\n    '))
       .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', '))
       .replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
-      .replaceAll('@(delegateClass)', delegateClass);
+      .replaceAll('@(delegateClass)', delegateClass)
+      .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '');
   }
 
+  bool _containsPluralMessage() => _allMessages.any((Message message) => message.isPlural);
+
   void writeOutputFiles(Logger logger, { bool isFromYaml = false }) {
     // First, generate the string contents of all necessary files.
     _generateCode();
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 5169ef9..01f2c75 100644
--- a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart
+++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart
@@ -160,11 +160,9 @@
 
 const String classFileTemplate = '''
 @(header)
-// ignore: unused_import
-import 'package:intl/intl.dart' as intl;
-import '@(fileName)';
 
-// ignore_for_file: unnecessary_brace_in_string_interps
+@(requiresIntlImport)
+import '@(fileName)';
 
 /// The translations for @(language) (`@(localeName)`).
 class @(class) extends @(baseClass) {
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 f0e9453..15515c5 100644
--- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
+++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
@@ -56,6 +56,9 @@
 {
   "title": "标题"
 }''';
+const String intlImportDartCode = '''
+import 'package:intl/intl.dart' as intl;
+''';
 
 void _standardFlutterDirectoryL10nSetup(FileSystem fs) {
   final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
@@ -1728,6 +1731,281 @@
       });
     });
 
+    test('intl package import should be omitted in subclass files when no plurals are included', () {
+      fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+        ..childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString)
+        ..childFile('app_es.arb').writeAsStringSync(singleEsMessageArbFileString);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          inputPathString: defaultL10nPathString,
+          outputPathString: defaultL10nPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.loadResources();
+        generator.writeOutputFiles(BufferLogger.test());
+      } on Exception catch (e) {
+        fail('Generating output files should not fail: $e');
+      }
+
+      final String localizationsFile = fs.file(
+        fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+      ).readAsStringSync();
+      expect(localizationsFile, isNot(contains(intlImportDartCode)));
+    });
+
+    test('intl package import should be kept in subclass files when plurals are included', () {
+      const String pluralMessageArb = '''
+{
+  "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}}",
+  "@helloWorlds": {
+    "description": "A plural message",
+    "placeholders": {
+      "count": {}
+    }
+  }
+}
+''';
+
+      const String pluralMessageEsArb = '''
+{
+  "helloWorlds": "{count,plural, =0{ES - Hello} =1{ES - Hello World} =2{ES - Hello two worlds} few{ES - Hello {count} worlds} many{ES - Hello all {count} worlds} other{ES - Hello other {count} worlds}}"
+}
+''';
+
+      fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+        ..childFile(defaultTemplateArbFileName).writeAsStringSync(pluralMessageArb)
+        ..childFile('app_es.arb').writeAsStringSync(pluralMessageEsArb);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          inputPathString: defaultL10nPathString,
+          outputPathString: defaultL10nPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.loadResources();
+        generator.writeOutputFiles(BufferLogger.test());
+      } on Exception catch (e) {
+        fail('Generating output files should not fail: $e');
+      }
+
+      final String localizationsFile = fs.file(
+        fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+      ).readAsStringSync();
+      expect(localizationsFile, contains(intlImportDartCode));
+    });
+
+    test('check for string interpolation rules', () {
+      const String enArbCheckList = '''
+{
+  "one": "The number of {one} elapsed is: 44",
+  "@one": {
+    "description": "test one",
+    "placeholders": {
+      "one": {
+        "type": "String"
+      }
+    }
+  },
+  "two": "哈{two}哈",
+  "@two": {
+    "description": "test two",
+    "placeholders": {
+      "two": {
+        "type": "String"
+      }
+    }
+  },
+  "three": "m{three}m",
+  "@three": {
+    "description": "test three",
+    "placeholders": {
+      "three": {
+        "type": "String"
+      }
+    }
+  },
+  "four": "I have to work _{four}_ sometimes.",
+  "@four": {
+    "description": "test four",
+    "placeholders": {
+      "four": {
+        "type": "String"
+      }
+    }
+  },
+  "five": "{five} elapsed.",
+  "@five": {
+    "description": "test five",
+    "placeholders": {
+      "five": {
+        "type": "String"
+      }
+    }
+  },
+  "six": "{six}m",
+  "@six": {
+    "description": "test six",
+    "placeholders": {
+      "six": {
+        "type": "String"
+      }
+    }
+  },
+  "seven": "hours elapsed: {seven}",
+  "@seven": {
+    "description": "test seven",
+    "placeholders": {
+      "seven": {
+        "type": "String"
+      }
+    }
+  },
+  "eight": " {eight}",
+  "@eight": {
+    "description": "test eight",
+    "placeholders": {
+      "eight": {
+        "type": "String"
+      }
+    }
+  },
+  "nine": "m{nine}",
+  "@nine": {
+    "description": "test nine",
+    "placeholders": {
+      "nine": {
+        "type": "String"
+      }
+    }
+  }
+}
+''';
+
+      // It's fine that the arb is identical -- Just checking
+      // generated code for use of '${variable}' vs '$variable'
+      const String esArbCheckList = '''
+{
+  "one": "The number of {one} elapsed is: 44",
+  "two": "哈{two}哈",
+  "three": "m{three}m",
+  "four": "I have to work _{four}_ sometimes.",
+  "five": "{five} elapsed.",
+  "six": "{six}m",
+  "seven": "hours elapsed: {seven}",
+  "eight": " {eight}",
+  "nine": "m{nine}"
+}
+''';
+
+      fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+        ..childFile(defaultTemplateArbFileName).writeAsStringSync(enArbCheckList)
+        ..childFile('app_es.arb').writeAsStringSync(esArbCheckList);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          inputPathString: defaultL10nPathString,
+          outputPathString: defaultL10nPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.loadResources();
+        generator.writeOutputFiles(BufferLogger.test());
+      } on Exception catch (e) {
+        if (e is L10nException) {
+          print(e.message);
+        }
+        fail('Generating output files should not fail: $e');
+      }
+
+      final String localizationsFile = fs.file(
+        fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+      ).readAsStringSync();
+
+      expect(localizationsFile, contains(r'$one'));
+      expect(localizationsFile, contains(r'$two'));
+      expect(localizationsFile, contains(r'${three}'));
+      expect(localizationsFile, contains(r'${four}'));
+      expect(localizationsFile, contains(r'$five'));
+      expect(localizationsFile, contains(r'${six}m'));
+      expect(localizationsFile, contains(r'$seven'));
+      expect(localizationsFile, contains(r'$eight'));
+      expect(localizationsFile, contains(r'${nine}'));
+    });
+
+    test('check for string interpolation rules - plurals', () {
+      const String enArbCheckList = '''
+{
+  "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}",
+  "@first": {
+    "description": "First set of plural messages to test.",
+    "placeholders": {
+      "count": {}
+    }
+  },
+  "second": "{count,plural, =0{test {count}} other{ {count}}",
+  "@second": {
+    "description": "Second set of plural messages to test.",
+    "placeholders": {
+      "count": {}
+    }
+  }
+}
+''';
+
+      // It's fine that the arb is identical -- Just checking
+      // generated code for use of '${variable}' vs '$variable'
+      const String esArbCheckList = '''
+{
+  "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}",
+  "second": "{count,plural, =0{test {count}} other{ {count}}"
+}
+''';
+
+      fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+        ..childFile(defaultTemplateArbFileName).writeAsStringSync(enArbCheckList)
+        ..childFile('app_es.arb').writeAsStringSync(esArbCheckList);
+
+      final LocalizationsGenerator generator = LocalizationsGenerator(fs);
+      try {
+        generator.initialize(
+          inputPathString: defaultL10nPathString,
+          outputPathString: defaultL10nPathString,
+          templateArbFileName: defaultTemplateArbFileName,
+          outputFileString: defaultOutputFileString,
+          classNameString: defaultClassNameString,
+        );
+        generator.loadResources();
+        generator.writeOutputFiles(BufferLogger.test());
+      } on Exception catch (e) {
+        if (e is L10nException) {
+          print(e.message);
+        }
+        fail('Generating output files should not fail: $e');
+      }
+
+      final String localizationsFile = fs.file(
+        fs.path.join(syntheticL10nPackagePath, 'output-localization-file_es.dart'),
+      ).readAsStringSync();
+
+      expect(localizationsFile, contains(r'test $count test'));
+      expect(localizationsFile, contains(r'哈$count哈'));
+      expect(localizationsFile, contains(r'm${count}m'));
+      expect(localizationsFile, contains(r'_${count}_'));
+      expect(localizationsFile, contains(r'$count test'));
+      expect(localizationsFile, contains(r'${count}m'));
+      expect(localizationsFile, contains(r'test $count'));
+      expect(localizationsFile, contains(r' $count'));
+    });
+
     test(
       'should throw with descriptive error message when failing to parse the '
       'arb file',