Generate message lookup in gen_l10n (#50733)

diff --git a/dev/tools/localization/bin/gen_l10n.dart b/dev/tools/localization/bin/gen_l10n.dart
index 1c61a95..9bfcd79 100644
--- a/dev/tools/localization/bin/gen_l10n.dart
+++ b/dev/tools/localization/bin/gen_l10n.dart
@@ -63,9 +63,8 @@
     exit(0);
   }
 
-  final String flutterRoot = Platform.environment['FLUTTER_ROOT'];
-  final String flutterBin = Platform.isWindows ? 'flutter.bat' : 'flutter';
-  final String flutterPath = flutterRoot == null ? flutterBin : path.join(flutterRoot, 'bin', flutterBin);
+  await precacheLanguageAndRegionTags();
+
   final String arbPathString = results['arb-dir'] as String;
   final String outputFileString = results['output-localization-file'] as String;
   final String templateArbFileName = results['template-arb-file'] as String;
@@ -74,6 +73,7 @@
 
   const local.LocalFileSystem fs = local.LocalFileSystem();
   final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs);
+
   try {
     localizationsGenerator
       ..initialize(
@@ -83,9 +83,8 @@
         classNameString: classNameString,
         preferredSupportedLocaleString: preferredSupportedLocaleString,
       )
-      ..parseArbFiles()
-      ..generateClassMethods()
-      ..generateOutputFile();
+      ..loadResources()
+      ..writeOutputFile();
   } on FileSystemException catch (e) {
     exitWithError(e.message);
   } on FormatException catch (e) {
@@ -93,24 +92,4 @@
   } on L10nException catch (e) {
     exitWithError(e.message);
   }
-
-  final ProcessResult pubGetResult = await Process.run(flutterPath, <String>['pub', 'get']);
-  if (pubGetResult.exitCode != 0) {
-    stderr.write(pubGetResult.stderr);
-    exit(1);
-  }
-
-  final ProcessResult generateFromArbResult = await Process.run(flutterPath, <String>[
-    'pub',
-    'run',
-    'intl_translation:generate_from_arb',
-    '--output-dir=${localizationsGenerator.l10nDirectory.path}',
-    '--no-use-deferred-loading',
-    localizationsGenerator.outputFile.path,
-    ...localizationsGenerator.arbPathStrings,
-  ]);
-  if (generateFromArbResult.exitCode != 0) {
-    stderr.write(generateFromArbResult.stderr);
-    exit(1);
-  }
 }
diff --git a/dev/tools/localization/gen_l10n.dart b/dev/tools/localization/gen_l10n.dart
index 751ba61..2102947 100644
--- a/dev/tools/localization/gen_l10n.dart
+++ b/dev/tools/localization/gen_l10n.dart
@@ -13,255 +13,78 @@
 import 'gen_l10n_types.dart';
 import 'localizations_utils.dart';
 
-// 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',
-};
-
-// The set of number formats that can be automatically localized.
-//
-// The localizations generation tool makes use of the intl library's
-// NumberFormat class to properly format numbers based on the locale, the
-// desired format, as well as the passed in number. For example, using
-// DateFormat.compactLong("en_US").format(1200000) results
-// in the string "1.2 million".
-//
-// Since the tool generates code that uses NumberFormat'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/NumberFormat-class.html>
-const Set<String> allowableNumberFormats = <String>{
-  'compact',
-  'compactCurrency',
-  'compactSimpleCurrency',
-  'compactLong',
-  'currency',
-  'decimalPattern',
-  'decimalPercentPattern',
-  'percentPattern',
-  'scientificPattern',
-  'simpleCurrency',
-};
-
-// The names of the NumberFormat factory constructors which have named
-// parameters rather than positional parameters.
-//
-// This helps the tool correctly generate number formmatting code correctly.
-//
-// Example of code that uses named parameters:
-// final NumberFormat format = NumberFormat.compact(
-//   locale: _localeName,
-// );
-//
-// Example of code that uses positional parameters:
-// final NumberFormat format = NumberFormat.scientificPattern(_localeName);
-const Set<String> numberFormatsWithNamedParameters = <String>{
-  'compact',
-  'compactCurrency',
-  'compactSimpleCurrency',
-  'compactLong',
-  'currency',
-  'decimalPercentPattern',
-  'simpleCurrency',
-};
-
-List<String> generateIntlMethodArgs(Message message) {
-  final List<String> methodArgs = <String>["name: '${message.resourceId}'"];
-  if (message.description != null)
-    methodArgs.add('desc: ${generateString(message.description)}');
-  if (message.placeholders.isNotEmpty) {
-    final String args = message.placeholders.map<String>((Placeholder placeholder) {
-      return placeholder.name;
-    }).join(', ');
-    methodArgs.add('args: <Object>[$args]');
-  }
-  return methodArgs;
-}
-
-List<String> generateInnerMethodArgs(Message message) {
+List<String> generateMethodParameters(Message message) {
+  assert(message.placeholders.isNotEmpty);
+  final Placeholder countPlaceholder = message.isPlural ? message.getCountPlaceholder() : null;
   return message.placeholders.map((Placeholder placeholder) {
-    final String arg = placeholder.name;
-    return placeholder.requiresFormatting ? '${arg}String' : arg;
+    final String type = placeholder == countPlaceholder ? 'int' : placeholder.type;
+    return '$type ${placeholder.name}';
   }).toList();
 }
 
 String generateDateFormattingLogic(Message message) {
-  if (message.placeholders.isEmpty)
-    return '';
+  if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting)
+    return '@(none)';
 
-  final StringBuffer result = StringBuffer();
-  for (final Placeholder placeholder in message.placeholders) {
-    if (!placeholder.isDate)
-      continue;
-    if (placeholder.format == null) {
-      throw L10nException(
-        'The placeholder, ${placeholder.name}, has its "type" resource attribute set to '
-        'the "${placeholder.type}" type. To properly resolve for the right '
-        '${placeholder.type} 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.'
-      );
-    }
-    if (!allowableDateFormats.contains(placeholder.format)) {
-      throw L10nException(
-        'Date format "${placeholder.format}" for placeholder '
-        '${placeholder.name} does not have a corresponding DateFormat '
-        "constructor\n. Check the intl library's DateFormat class "
-        'constructors for allowed date formats.'
-      );
-    }
-    result.write('''
+  final Iterable<String> formatStatements = message.placeholders
+    .where((Placeholder placeholder) => placeholder.isDate)
+    .map((Placeholder placeholder) {
+      if (placeholder.format == null) {
+        throw L10nException(
+          'The placeholder, ${placeholder.name}, has its "type" resource attribute set to '
+          'the "${placeholder.type}" type. To properly resolve for the right '
+          '${placeholder.type} 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.'
+        );
+      }
+      if (!placeholder.hasValidDateFormat) {
+        throw L10nException(
+          'Date format "${placeholder.format}" for placeholder '
+          '${placeholder.name} does not have a corresponding DateFormat '
+          'constructor\n. Check the intl library\'s DateFormat class '
+          'constructors for allowed date formats.'
+        );
+      }
+      return dateFormatTemplate
+        .replaceAll('@(placeholder)', placeholder.name)
+        .replaceAll('@(format)', placeholder.format);
+    });
 
-    final DateFormat ${placeholder.name}DateFormat = DateFormat.${placeholder.format}(_localeName);
-    final String ${placeholder.name}String = ${placeholder.name}DateFormat.format(${placeholder.name});
-''');
-  }
-  return result.toString();
+  return formatStatements.isEmpty ? '@(none)' : formatStatements.join('');
 }
 
 String generateNumberFormattingLogic(Message message) {
-  if (message.placeholders.isEmpty)
-    return '';
+  if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
+    return '@(none)';
+  }
 
-  final StringBuffer result = StringBuffer();
-  for (final Placeholder placeholder in message.placeholders) {
-    if (!placeholder.isNumber)
-      continue;
-    if (!allowableNumberFormats.contains(placeholder.format)) {
-      throw L10nException(
-        'Number format ${placeholder.format} for the ${placeholder.name} '
-        'placeholder does not have a corresponding NumberFormat constructor.\n'
-        "Check the intl library's NumberFormat class constructors for allowed "
-        'number formats.'
+  final Iterable<String> formatStatements = message.placeholders
+    .where((Placeholder placeholder) => placeholder.isNumber)
+    .map((Placeholder placeholder) {
+      if (!placeholder.hasValidNumberFormat) {
+        throw L10nException(
+          'Number format ${placeholder.format} for the ${placeholder.name} '
+          'placeholder does not have a corresponding NumberFormat constructor.\n'
+          'Check the intl library\'s NumberFormat class constructors for allowed '
+          'number formats.'
+        );
+      }
+      final Iterable<String> parameters =
+        placeholder.optionalParameters.map<String>((OptionalParameter parameter) {
+          return '${parameter.name}: ${parameter.value}';
+        },
       );
-    }
-    if (numberFormatsWithNamedParameters.contains(placeholder.format)) {
-      final StringBuffer optionalParametersString = StringBuffer();
-      for (final OptionalParameter parameter in placeholder.optionalParameters)
-        optionalParametersString.write('\n      ${parameter.name}: ${parameter.value},');
-      result.write('''
+      return numberFormatTemplate
+        .replaceAll('@(placeholder)', placeholder.name)
+        .replaceAll('@(format)', placeholder.format)
+        .replaceAll('@(parameters)', parameters.join(',    \n'));
+    });
 
-    final NumberFormat ${placeholder.name}NumberFormat = NumberFormat.${placeholder.format}(
-      locale: _localeName,${optionalParametersString.toString()}
-    );
-    final String ${placeholder.name}String = ${placeholder.name}NumberFormat.format(${placeholder.name});
-''');
-
-    } else {
-          result.write('''
-
-    final NumberFormat ${placeholder.name}NumberFormat = NumberFormat.${placeholder.format}(_localeName);
-    final String ${placeholder.name}String = ${placeholder.name}NumberFormat.format(${placeholder.name});
-''');
-    }
-  }
-  return result.toString();
+  return formatStatements.isEmpty ? '@(none)' : formatStatements.join('');
 }
 
-String genSimpleMethod(Message message) {
-  String genSimpleMethodMessage() {
-    String messageValue = message.value;
-    for (final Placeholder placeholder in message.placeholders) {
-        messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}}');
-    }
-    final String generatedMessage = generateString(messageValue); // "r'...'"
-    return generatedMessage.startsWith('r') ? generatedMessage.substring(1) : generatedMessage;
-  }
-
-  List<String> genMethodParameters([String type]) {
-    return message.placeholders.map((Placeholder placeholder) {
-      return '${type ?? placeholder.type} ${placeholder.name}';
-    }).toList();
-  }
-
-  if (message.placeholdersRequireFormatting) {
-    return formatMethodTemplate
-      .replaceAll('@(methodName)', message.resourceId)
-      .replaceAll('@(methodParameters)', genMethodParameters().join(', '))
-      .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
-      .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
-      .replaceAll('@(message)', genSimpleMethodMessage())
-      .replaceAll('@(innerMethodParameters)', genMethodParameters('Object').join(', '))
-      .replaceAll('@(innerMethodArgs)', generateInnerMethodArgs(message).join(', '))
-      .replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n        '));
-  }
-
-  if (message.placeholders.isNotEmpty) {
-    return simpleMethodTemplate
-      .replaceAll('@(methodName)', message.resourceId)
-      .replaceAll('@(methodParameters)', genMethodParameters().join(', '))
-      .replaceAll('@(message)', genSimpleMethodMessage())
-      .replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n      '));
-  }
-
-  return getterMethodTemplate
-    .replaceAll('@(methodName)', message.resourceId)
-    .replaceAll('@(message)', genSimpleMethodMessage())
-    .replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n      '));
-}
-
-
 String generatePluralMethod(Message message) {
   if (message.placeholders.isEmpty) {
     throw L10nException(
@@ -277,15 +100,6 @@
   for (final Placeholder placeholder in message.placeholders)
     easyMessage = easyMessage.replaceAll('{${placeholder.name}}', '#${placeholder.name}#');
 
-  const Map<String, String> pluralIds = <String, String>{
-    '=0': 'zero',
-    '=1': 'one',
-    '=2': 'two',
-    'few': 'few',
-    'many': 'many',
-    'other': 'other'
-  };
-
   final Placeholder countPlaceholder = message.getCountPlaceholder();
   if (countPlaceholder == null) {
     throw L10nException(
@@ -295,67 +109,154 @@
     );
   }
 
-  final List<String> intlMethodArgs = <String>[
-    countPlaceholder.name,
-    'locale: _localeName',
-    ...generateIntlMethodArgs(message),
-  ];
+  const Map<String, String> pluralIds = <String, String>{
+    '=0': 'zero',
+    '=1': 'one',
+    '=2': 'two',
+    'few': 'few',
+    'many': 'many',
+    'other': 'other'
+  };
 
+  final List<String> pluralLogicArgs = <String>[];
   for (final String pluralKey in pluralIds.keys) {
-    final RegExp expRE = RegExp('($pluralKey){([^}]+)}');
+    final RegExp expRE = RegExp('($pluralKey)\\s*{([^}]+)}');
     final RegExpMatch match = expRE.firstMatch(easyMessage);
     if (match != null && match.groupCount == 2) {
       String argValue = match.group(2);
       for (final Placeholder placeholder in message.placeholders) {
-        if (placeholder.requiresFormatting) {
+        if (placeholder != countPlaceholder && placeholder.requiresFormatting) {
           argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}');
         } else {
           argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}');
         }
       }
-      intlMethodArgs.add("${pluralIds[pluralKey]}: '$argValue'");
+      pluralLogicArgs.add("      ${pluralIds[pluralKey]}: '$argValue'");
     }
   }
 
-  List<String> generatePluralMethodParameters([String type]) {
-    return message.placeholders.map((Placeholder placeholder) {
-      final String placeholderType = placeholder == countPlaceholder ? 'int' : (type ?? placeholder.type);
-      return '$placeholderType ${placeholder.name}';
-    }).toList();
+  final List<String> parameters = message.placeholders.map((Placeholder placeholder) {
+    final String placeholderType = placeholder == countPlaceholder ? 'int' : placeholder.type;
+    return '$placeholderType ${placeholder.name}';
+  }).toList();
+
+  final String comment = message.description ?? 'No description provided in @${message.resourceId}';
+
+  return pluralMethodTemplate
+    .replaceAll('@(comment)', comment)
+    .replaceAll('@(name)', message.resourceId)
+    .replaceAll('@(parameters)', parameters.join(', '))
+    .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
+    .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
+    .replaceAll('@(count)', countPlaceholder.name)
+    .replaceAll('@(pluralLogicArgs)', pluralLogicArgs.join(',\n'))
+    .replaceAll('@(none)\n', '');
+}
+
+String generateMethod(Message message, AppResourceBundle bundle) {
+  String generateMessage() {
+    String messageValue = bundle.translationFor(message);
+    for (final Placeholder placeholder in message.placeholders) {
+      if (placeholder.requiresFormatting) {
+        messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}String}');
+      } else {
+        messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}}');
+      }
+    }
+    return "'$messageValue'";
+  }
+
+  if (message.isPlural) {
+    return generatePluralMethod(message);
   }
 
   if (message.placeholdersRequireFormatting) {
-    return pluralFormatMethodTemplate
-      .replaceAll('@(methodName)', message.resourceId)
-      .replaceAll('@(methodParameters)', generatePluralMethodParameters().join(', '))
+    return formatMethodTemplate
+      .replaceAll('@(name)', message.resourceId)
+      .replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
       .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
       .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
-      .replaceAll('@(innerMethodParameters)', generatePluralMethodParameters('Object').join(', '))
-      .replaceAll('@(innerMethodArgs)', generateInnerMethodArgs(message).join(', '))
-      .replaceAll('@(intlMethodArgs)', intlMethodArgs.join(',\n      '));
+      .replaceAll('@(message)', generateMessage())
+      .replaceAll('@(none)\n', '');
   }
 
-  return pluralMethodTemplate
-    .replaceAll('@(methodName)', message.resourceId)
-    .replaceAll('@(methodParameters)', generatePluralMethodParameters().join(', '))
-    .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
-    .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
-    .replaceAll('@(intlMethodArgs)', intlMethodArgs.join(',\n      '));
+  if (message.placeholders.isNotEmpty) {
+    return methodTemplate
+      .replaceAll('@(name)', message.resourceId)
+      .replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
+      .replaceAll('@(message)', generateMessage());
+  }
+
+  return getterTemplate
+    .replaceAll('@(name)', message.resourceId)
+    .replaceAll('@(message)', generateMessage());
 }
 
-/// The localizations generation class used to generate the localizations
-/// classes, as well as all pertinent Dart files required to internationalize a
-/// Flutter application.
+String generateClass(String className, AppResourceBundle bundle, Iterable<Message> messages) {
+  final LocaleInfo locale = bundle.locale;
+
+  String baseClassName = className;
+  if (locale.countryCode != null) {
+    baseClassName = '$className${LocaleInfo.fromString(locale.languageCode).camelCase()}';
+  }
+
+  final Iterable<String> methods = messages
+    .where((Message message) => bundle.translationFor(message) != null)
+    .map((Message message) => generateMethod(message, bundle));
+
+  return classTemplate
+    .replaceAll('@(language)', describeLocale(locale.toString()))
+    .replaceAll('@(baseClass)', baseClassName)
+    .replaceAll('@(class)', '$className${locale.camelCase()}')
+    .replaceAll('@(localeName)', locale.toString())
+    .replaceAll('@(methods)', methods.join('\n\n'));
+}
+
+String generateBaseClassMethod(Message message) {
+  final String comment = message.description ?? 'No description provided in @${message.resourceId}';
+  if (message.placeholders.isNotEmpty) {
+    return baseClassMethodTemplate
+      .replaceAll('@(comment)', comment)
+      .replaceAll('@(name)', message.resourceId)
+      .replaceAll('@(parameters)', generateMethodParameters(message).join(', '));
+  }
+  return baseClassGetterTemplate
+    .replaceAll('@(comment)', comment)
+    .replaceAll('@(name)', message.resourceId);
+}
+
+String generateLookupBody(AppResourceBundleCollection allBundles, String className) {
+  final Iterable<String> switchClauses = allBundles.languages.map((String language) {
+    final Iterable<LocaleInfo> locales = allBundles.localesForLanguage(language);
+    if (locales.length == 1) {
+      return switchClauseTemplate
+        .replaceAll('@(case)', language)
+        .replaceAll('@(class)', '$className${locales.first.camelCase()}');
+    }
+
+    final Iterable<LocaleInfo> localesWithCountryCodes = locales.where((LocaleInfo locale) => locale.countryCode != null);
+    return countryCodeSwitchTemplate
+      .replaceAll('@(languageCode)', language)
+      .replaceAll('@(class)', '$className${LocaleInfo.fromString(language).camelCase()}')
+      .replaceAll('@(switchClauses)', localesWithCountryCodes.map((LocaleInfo locale) {
+          return switchClauseTemplate
+            .replaceAll('@(case)', locale.countryCode)
+            .replaceAll('@(class)', '$className${locale.camelCase()}');
+        }).join('\n        '));
+  });
+  return switchClauses.join('\n    ');
+}
+
 class LocalizationsGenerator {
   /// Creates an instance of the localizations generator class.
   ///
   /// It takes in a [FileSystem] representation that the class will act upon.
   LocalizationsGenerator(this._fs);
 
-  static RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$');
-  static RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$');
-
   final file.FileSystem _fs;
+  Iterable<Message> _allMessages;
+  AppResourceBundleCollection _allBundles;
+
 
   /// The reference to the project's l10n directory.
   ///
@@ -404,7 +305,9 @@
   List<LocaleInfo> _preferredSupportedLocales;
 
   /// The list of all arb path strings in [l10nDirectory].
-  final List<String> arbPathStrings = <String>[];
+  List<String> get arbPathStrings {
+    return _allBundles.bundles.map((AppResourceBundle bundle) => bundle.file.path).toList();
+  }
 
   /// The supported language codes as found in the arb files located in
   /// [l10nDirectory].
@@ -414,10 +317,6 @@
   /// [l10nDirectory].
   final Set<LocaleInfo> supportedLocales = <LocaleInfo>{};
 
-  /// The class methods that will be generated in the localizations class
-  /// based on messages found in the template arb file.
-  final List<String> classMethods = <String>[];
-
   /// Initializes [l10nDirectory], [templateArbFile], [outputFile] and [className].
   ///
   /// Throws an [L10nException] when a provided configuration is not allowed
@@ -531,7 +430,9 @@
   /// will take priority over the other locales.
   @visibleForTesting
   void setPreferredSupportedLocales(String inputLocales) {
-    if (inputLocales != null) {
+    if (inputLocales == null || inputLocales.trim().isEmpty) {
+      _preferredSupportedLocales = const <LocaleInfo>[];
+    } else {
       final List<dynamic> preferredLocalesStringList = json.decode(inputLocales) as List<dynamic>;
       _preferredSupportedLocales = preferredLocalesStringList.map((dynamic localeString) {
         if (localeString.runtimeType != String) {
@@ -542,70 +443,6 @@
     }
   }
 
-  /// Scans [l10nDirectory] for arb files and parses them for language and locale
-  /// information.
-  void parseArbFiles() {
-    final List<File> fileSystemEntityList = l10nDirectory
-      .listSync()
-      .whereType<File>()
-      .toList();
-    final List<LocaleInfo> localeInfoList = <LocaleInfo>[];
-
-    for (final File file in fileSystemEntityList) {
-      final String filePath = file.path;
-      if (arbFilenameRE.hasMatch(filePath)) {
-        final Map<String, dynamic> arbContents = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
-        String localeString = arbContents['@@locale'] as String;
-        if (localeString == null) {
-          final RegExpMatch arbFileMatch = arbFilenameLocaleRE.firstMatch(filePath);
-          if (arbFileMatch == null) {
-            throw L10nException(
-              "The following .arb file's locale could not be determined: \n"
-              '$filePath \n'
-              "Make sure that the locale is specified in the '@@locale' "
-              'property or as part of the filename (e.g. file_en.arb)'
-            );
-          }
-
-          localeString = arbFilenameLocaleRE.firstMatch(filePath)[1];
-        }
-
-        arbPathStrings.add(filePath);
-        final LocaleInfo localeInfo = LocaleInfo.fromString(localeString);
-        if (localeInfoList.contains(localeInfo))
-          throw L10nException(
-            'Multiple arb files with the same locale detected. \n'
-            'Ensure that there is exactly one arb file for each locale.'
-          );
-        localeInfoList.add(localeInfo);
-      }
-    }
-
-    arbPathStrings.sort();
-    localeInfoList.sort();
-    supportedLanguageCodes.addAll(localeInfoList.map((LocaleInfo localeInfo) {
-      return "'${localeInfo.languageCode}'";
-    }));
-
-    if (preferredSupportedLocales != null) {
-      for (final LocaleInfo preferredLocale in preferredSupportedLocales) {
-        if (!localeInfoList.contains(preferredLocale)) {
-          throw L10nException(
-            "The preferred supported locale, '$preferredLocale', cannot be "
-            'added. Please make sure that there is a corresponding arb file '
-            'with translations for the locale, or remove the locale from the '
-            'preferred supported locale list if there is no intent to support '
-            'it.'
-          );
-        }
-
-        localeInfoList.removeWhere((LocaleInfo localeInfo) => localeInfo == preferredLocale);
-      }
-      localeInfoList.insertAll(0, preferredSupportedLocales);
-    }
-    supportedLocales.addAll(localeInfoList);
-  }
-
   static bool _isValidGetterAndMethodName(String name) {
     // Public Dart method name must not start with an underscore
     if (name[0] == '_')
@@ -622,83 +459,78 @@
     return true;
   }
 
-  static String _genSupportedLocaleProperty(Set<LocaleInfo> supportedLocales) {
-    const String prefix = 'static const List<Locale> supportedLocales = <Locale>[\n    Locale(';
-    const String suffix = '),\n  ];';
-
-    String resultingProperty = prefix;
-    for (final LocaleInfo locale in supportedLocales) {
-      final String languageCode = locale.languageCode;
-      final String countryCode = locale.countryCode;
-
-      resultingProperty += "'$languageCode'";
-      if (countryCode != null)
-        resultingProperty += ", '$countryCode'";
-      resultingProperty += '),\n    Locale(';
-    }
-    resultingProperty = resultingProperty.substring(0, resultingProperty.length - '),\n    Locale('.length);
-    resultingProperty += suffix;
-
-    return resultingProperty;
-  }
-
-  /// Generates the methods for the localizations class.
-  ///
-  /// The method parses [templateArbFile] and uses its resource ids as the
-  /// Dart method and getter names. It then uses each resource id's
-  /// corresponding resource value to figure out how to define these getters.
-  ///
-  /// For example, a message with plurals will be handled differently from
-  /// a simple, singular message.
-  ///
-  /// Throws an [L10nException] when a provided configuration is not allowed
-  /// by [LocalizationsGenerator].
-  ///
-  /// Throws a [FileSystemException] when a file operation necessary for setting
-  /// up the [LocalizationsGenerator] cannot be completed.
-  ///
-  /// Throws a [FormatException] when parsing the arb file is unsuccessful.
-  void generateClassMethods() {
-    Map<String, dynamic> bundle;
-    try {
-      bundle = json.decode(templateArbFile.readAsStringSync()) as Map<String, dynamic>;
-    } on FileSystemException catch (e) {
-      throw FileSystemException('Unable to read input arb file: $e');
-    } on FormatException catch (e) {
-      throw FormatException('Unable to parse arb file: $e');
-    }
-
-    final List<String> sortedArbKeys = bundle.keys.toList()..sort();
-    for (final String key in sortedArbKeys) {
-      if (key.startsWith('@'))
-        continue;
-      if (!_isValidGetterAndMethodName(key)) {
+  // Load _allMessages from templateArbFile and _allBundles from all of the ARB
+  // files in l10nDirectory. Also initialized: supportedLocales.
+  void loadResources() {
+    final AppResourceBundle templateBundle = AppResourceBundle(templateArbFile);
+    _allMessages = templateBundle.resourceIds.map((String id) => Message(templateBundle.resources, id));
+    for (final String resourceId in templateBundle.resourceIds)
+      if (!_isValidGetterAndMethodName(resourceId)) {
         throw L10nException(
-          'Invalid key format: $key \n It has to be in camel case, cannot start '
-          'with a number or underscore, and cannot contain non-alphanumeric characters.'
+          'Invalid ARB resource name "$resourceId" in $templateArbFile.\n'
+          'Resources names must be valid Dart method names: they have to be '
+          'camel case, cannot start with a number or underscore, and cannot '
+          'contain non-alphanumeric characters.'
         );
       }
 
-      final Message message = Message(bundle, key);
-      if (message.isPlural)
-        classMethods.add(generatePluralMethod(message));
-      else
-        classMethods.add(genSimpleMethod(message));
+    _allBundles = AppResourceBundleCollection(l10nDirectory);
+
+    final List<LocaleInfo> allLocales = List<LocaleInfo>.from(_allBundles.locales);
+    for (final LocaleInfo preferredLocale in preferredSupportedLocales) {
+      final int index = allLocales.indexOf(preferredLocale);
+      if (index == -1) {
+        throw L10nException(
+          "The preferred supported locale, '$preferredLocale', cannot be "
+          'added. Please make sure that there is a corresponding ARB file '
+          'with translations for the locale, or remove the locale from the '
+          'preferred supported locale list.'
+        );
+      }
+      allLocales.removeAt(index);
+      allLocales.insertAll(0, preferredSupportedLocales);
     }
+    supportedLocales.addAll(allLocales);
   }
 
-  /// Generates a file that contains the localizations class and the
-  /// LocalizationsDelegate class.
-  void generateOutputFile() {
+  // Generate the AppLocalizations class, its LocalizationsDelegate subclass.
+  String generateCode() {
     final String directory = path.basename(l10nDirectory.path);
     final String outputFileName = path.basename(outputFile.path);
-    outputFile.writeAsStringSync(
-      defaultFileTemplate
-        .replaceAll('@(className)', className)
-        .replaceAll('@(classMethods)', classMethods.join('\n'))
-        .replaceAll('@(importFile)', '$directory/$outputFileName')
-        .replaceAll('@(supportedLocales)', _genSupportedLocaleProperty(supportedLocales))
-        .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.toList().join(', '))
+
+    final Iterable<String> supportedLocalesCode = supportedLocales.map((LocaleInfo locale) {
+      final String country = locale.countryCode;
+      final String countryArg = country == null ? '' : ', $country';
+      return 'Locale(\'${locale.languageCode}$countryArg\')';
+    });
+
+    final Set<String> supportedLanguageCodes = Set<String>.from(
+      _allBundles.locales.map<String>((LocaleInfo locale) => '\'${locale.languageCode}\'')
     );
+
+    final StringBuffer allMessagesClasses = StringBuffer();
+    final List<LocaleInfo> allLocales = _allBundles.locales.toList()..sort();
+    for (final LocaleInfo locale in allLocales) {
+      allMessagesClasses.writeln();
+      allMessagesClasses.writeln(
+        generateClass(className, _allBundles.bundleFor(locale), _allMessages)
+      );
+    }
+
+    final String lookupBody = generateLookupBody(_allBundles, className);
+
+    return fileTemplate
+      .replaceAll('@(class)', className)
+      .replaceAll('@(methods)', _allMessages.map(generateBaseClassMethod).join('\n'))
+      .replaceAll('@(importFile)', '$directory/$outputFileName')
+      .replaceAll('@(supportedLocales)', supportedLocalesCode.join(',\n    '))
+      .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', '))
+      .replaceAll('@(allMessagesClasses)', allMessagesClasses.toString().trim())
+      .replaceAll('@(lookupName)', '_lookup$className')
+      .replaceAll('@(lookupBody)', lookupBody);
+  }
+
+  void writeOutputFile() {
+    outputFile.writeAsStringSync(generateCode());
   }
 }
diff --git a/dev/tools/localization/gen_l10n_templates.dart b/dev/tools/localization/gen_l10n_templates.dart
index 4caa5ca..328be9a 100644
--- a/dev/tools/localization/gen_l10n_templates.dart
+++ b/dev/tools/localization/gen_l10n_templates.dart
@@ -2,80 +2,29 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-const String getterMethodTemplate = '''
-  String get @(methodName) {
-    return Intl.message(
-      @(message),
-      locale: _localeName,
-      @(intlMethodArgs)
-    );
-  }
-''';
-
-const String simpleMethodTemplate = '''
-  String @(methodName)(@(methodParameters)) {
-    return Intl.message(
-      @(message),
-      locale: _localeName,
-      @(intlMethodArgs)
-    );
-  }
-''';
-
-const String formatMethodTemplate = '''
-  String @(methodName)(@(methodParameters)) {@(dateFormatting)@(numberFormatting)
-    String @(methodName)(@(innerMethodParameters)) {
-      return Intl.message(
-        @(message),
-        locale: _localeName,
-        @(intlMethodArgs)
-      );
-    }
-    return @(methodName)(@(innerMethodArgs));
-  }
-''';
-
-const String pluralMethodTemplate = '''
-  String @(methodName)(@(methodParameters)) {@(dateFormatting)@(numberFormatting)
-    return Intl.plural(
-      @(intlMethodArgs)
-    );
-  }
-''';
-
-const String pluralFormatMethodTemplate = '''
-  String @(methodName)(@(methodParameters)) {@(dateFormatting)@(numberFormatting)
-    String @(methodName)(@(innerMethodParameters)) {
-      return Intl.plural(
-        @(intlMethodArgs)
-      );
-    }
-    return @(methodName)(@(innerMethodArgs));
-  }
-''';
-
-const String defaultFileTemplate = '''
+const String fileTemplate = '''
 import 'dart:async';
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
-import 'package:intl/intl.dart';
+import 'package:intl/intl.dart' as intl;
 
-import 'messages_all.dart';
+// ignore_for_file: unnecessary_brace_in_string_interps
 
-/// Callers can lookup localized strings with an instance of @(className) returned
-/// by `@(className).of(context)`.
+/// Callers can lookup localized strings with an instance of @(class) returned
+/// by `@(class).of(context)`.
 ///
-/// Applications need to include `@(className).delegate()` in their app's
-/// localizationDelegates list, and the locales they support in the app's
+/// Applications need to include `@(class).delegate()` in their app\'s
+/// localizationDelegates list, and the locales they support in the app\'s
 /// supportedLocales list. For example:
 ///
 /// ```
 /// import '@(importFile)';
 ///
 /// return MaterialApp(
-///   localizationsDelegates: @(className).localizationsDelegates,
-///   supportedLocales: @(className).supportedLocales,
+///   localizationsDelegates: @(class).localizationsDelegates,
+///   supportedLocales: @(class).supportedLocales,
 ///   home: MyApplicationHome(),
 /// );
 /// ```
@@ -113,26 +62,18 @@
 /// Select and expand the newly-created Localizations item then, for each
 /// locale your application supports, add a new item and select the locale
 /// you wish to add from the pop-up menu in the Value field. This list should
-/// be consistent with the languages listed in the @(className).supportedLocales
+/// be consistent with the languages listed in the @(class).supportedLocales
 /// property.
-
-// ignore_for_file: unnecessary_brace_in_string_interps
-
-class @(className) {
-  @(className)(Locale locale) : _localeName = Intl.canonicalizedLocale(locale.toString());
+abstract class @(class) {
+  @(class)(String locale) : assert(locale != null), _localeName = intl.Intl.canonicalizedLocale(locale.toString());
 
   final String _localeName;
 
-  static Future<@(className)> load(Locale locale) {
-    return initializeMessages(locale.toString())
-      .then<@(className)>((_) => @(className)(locale));
+  static @(class) of(BuildContext context) {
+    return Localizations.of<@(class)>(context, @(class));
   }
 
-  static @(className) of(BuildContext context) {
-    return Localizations.of<@(className)>(context, @(className));
-  }
-
-  static const LocalizationsDelegate<@(className)> delegate = _@(className)Delegate();
+  static const LocalizationsDelegate<@(class)> delegate = _@(class)Delegate();
 
   /// A list of this localizations delegate along with the default localizations
   /// delegates.
@@ -152,21 +93,104 @@
   ];
 
   /// A list of this localizations delegate's supported locales.
-  @(supportedLocales)
+  static const List<Locale> supportedLocales = <Locale>[
+    @(supportedLocales)
+  ];
 
-@(classMethods)
-}
+@(methods)}
 
-class _@(className)Delegate extends LocalizationsDelegate<@(className)> {
-  const _@(className)Delegate();
+class _@(class)Delegate extends LocalizationsDelegate<@(class)> {
+  const _@(class)Delegate();
 
   @override
-  Future<@(className)> load(Locale locale) => @(className).load(locale);
+  Future<@(class)> load(Locale locale) {
+    return SynchronousFuture<@(class)>(@(lookupName)(locale));
+  }
 
   @override
   bool isSupported(Locale locale) => <String>[@(supportedLanguageCodes)].contains(locale.languageCode);
 
   @override
-  bool shouldReload(_@(className)Delegate old) => false;
+  bool shouldReload(_@(class)Delegate old) => false;
+}
+
+@(allMessagesClasses)
+
+@(class) @(lookupName)(Locale locale) {
+  switch(locale.languageCode) {
+    @(lookupBody)
+  }
+  assert(false, '@(class).delegate failed to load unsupported locale "\$locale"');
+  return null;
 }
 ''';
+
+const String numberFormatTemplate = '''
+    final intl.NumberFormat @(placeholder)NumberFormat = intl.NumberFormat.@(format)(
+      locale: _localeName,
+      @(parameters)
+    );
+    final String @(placeholder)String = @(placeholder)NumberFormat.format(@(placeholder));
+''';
+
+const String dateFormatTemplate = '''
+    final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat.@(format)(_localeName);
+    final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder));
+''';
+
+const String getterTemplate = '''
+  @override
+  String get @(name) => @(message);''';
+
+const String methodTemplate = '''
+  @override
+  String @(name)(@(parameters)) {
+    return @(message);
+  }''';
+
+const String formatMethodTemplate = '''
+  @override
+  String @(name)(@(parameters)) {
+@(dateFormatting)
+@(numberFormatting)
+    return @(message);
+  }''';
+
+const String pluralMethodTemplate = '''
+  @override
+  String @(name)(@(parameters)) {
+@(dateFormatting)
+@(numberFormatting)
+    return intl.Intl.pluralLogic(
+      @(count),
+      locale: _localeName,
+@(pluralLogicArgs),
+    );
+  }''';
+
+const String classTemplate = '''
+/// The translations for @(language) (`@(localeName)`).
+class @(class) extends @(baseClass) {
+  @(class)([String locale = '@(localeName)']) : super(locale);
+
+@(methods)
+}''';
+
+const String baseClassGetterTemplate = '''
+  // @(comment)
+  String get @(name);
+''';
+
+const String baseClassMethodTemplate = '''
+  // @(comment)
+  String @(name)(@(parameters));
+''';
+
+const String switchClauseTemplate = '''case '@(case)': return @(class)();''';
+
+const String countryCodeSwitchTemplate = '''case '@(languageCode)': {
+      switch (locale.countryCode) {
+        @(switchClauses)
+      }
+      return @(class)();
+    }''';
diff --git a/dev/tools/localization/gen_l10n_types.dart b/dev/tools/localization/gen_l10n_types.dart
index f00535a..b760dfc 100644
--- a/dev/tools/localization/gen_l10n_types.dart
+++ b/dev/tools/localization/gen_l10n_types.dart
@@ -2,12 +2,149 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert';
+import 'dart:io';
+
+import 'localizations_utils.dart';
+
+// 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> _validDateFormats = <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',
+};
+
+// The set of number formats that can be automatically localized.
+//
+// The localizations generation tool makes use of the intl library's
+// NumberFormat class to properly format numbers based on the locale, the
+// desired format, as well as the passed in number. For example, using
+// DateFormat.compactLong("en_US").format(1200000) results
+// in the string "1.2 million".
+//
+// Since the tool generates code that uses NumberFormat'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/NumberFormat-class.html>
+const Set<String> _validNumberFormats = <String>{
+  'compact',
+  'compactCurrency',
+  'compactSimpleCurrency',
+  'compactLong',
+  'currency',
+  'decimalPattern',
+  'decimalPercentPattern',
+  'percentPattern',
+  'scientificPattern',
+  'simpleCurrency',
+};
+
+// The names of the NumberFormat factory constructors which have named
+// parameters rather than positional parameters.
+//
+// This helps the tool correctly generate number formmatting code correctly.
+//
+// Example of code that uses named parameters:
+// final NumberFormat format = NumberFormat.compact(
+//   locale: _localeName,
+// );
+//
+// Example of code that uses positional parameters:
+// final NumberFormat format = NumberFormat.scientificPattern(_localeName);
+const Set<String> _numberFormatsWithNamedParameters = <String>{
+  'compact',
+  'compactCurrency',
+  'compactSimpleCurrency',
+  'compactLong',
+  'currency',
+  'decimalPercentPattern',
+  'simpleCurrency',
+};
+
 class L10nException implements Exception {
   L10nException(this.message);
 
   final String message;
 }
 
+// One optional named parameter to be used by a NumberFormat.
+//
+// Some of the NumberFormat factory constructors have optional named parameters.
+// For example NumberFormat.compactCurrency has a decimalDigits parameter that
+// specifies the number of decimal places to use when formatting.
+//
+// Optional parameters for NumberFormat placeholders are specified as a
+// JSON map value for optionalParameters in a resource's "@" ARB file entry:
+//
+// "@myResourceId": {
+//   "placeholders": {
+//     "myNumberPlaceholder": {
+//       "type": "double",
+//       "format": "compactCurrency",
+//       "optionalParameters": {
+//         "decimalDigits": 2
+//       }
+//     }
+//   }
+// }
 class OptionalParameter {
   const OptionalParameter(this.name, this.value) : assert(name != null), assert(value != null);
 
@@ -15,6 +152,39 @@
   final Object value;
 }
 
+// One message parameter: one placeholder from an @foo entry in the template ARB file.
+//
+// Placeholders are specified as a JSON map with one entry for each placeholder.
+// One placeholder must be specified for each message "{parameter}".
+// Each placeholder entry is also a JSON map. If the map is empty, the placeholder
+// is assumed to be an Object value whose toString() value will be displayed.
+// For example:
+//
+// "greeting": "{hello} {world}",
+// "@greeting": {
+//   "description": "A message with a two parameters",
+//   "placeholders": {
+//     "hello": {},
+//     "world": {}
+//   }
+// }
+//
+// Each placeholder can optionally specify a valid Dart type. If the type
+// is NumberFormat or DateFormat then a format which matches one of the
+// type's factory constructors can also be specified. In this example the
+// date placeholder is to be formated with DateFormat.yMMMMd:
+//
+// "helloWorldOn": "Hello World on {date}",
+// "@helloWorldOn": {
+//   "description": "A message with a date parameter",
+//   "placeholders": {
+//     "date": {
+//       "type": "DateTime",
+//       "format": "yMMMMd"
+//     }
+//   }
+// }
+//
 class Placeholder {
   Placeholder(this.resourceId, this.name, Map<String, dynamic> attributes)
     : assert(resourceId != null),
@@ -33,7 +203,10 @@
 
   bool get requiresFormatting => <String>['DateTime', 'double', 'int', 'num'].contains(type);
   bool get isNumber => <String>['double', 'int', 'num'].contains(type);
+  bool get hasValidNumberFormat => _validNumberFormats.contains(format);
+  bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
   bool get isDate => 'DateTime' == type;
+  bool get hasValidDateFormat => _validDateFormats.contains(format);
 
   static String _stringAttribute(
     String resourceId,
@@ -75,6 +248,21 @@
   }
 }
 
+// One translation: one pair of foo,@foo entries from the template ARB file.
+//
+// The template ARB file must contain an entry called @myResourceId for each
+// message named myResourceId. The @ entry describes message parameters
+// called "placeholders" and can include an optional description.
+// Here's a simple example message with no parameters:
+//
+// "helloWorld": "Hello World",
+// "@helloWorld": {
+//   "description": "The conventional newborn programmer greeting"
+// }
+//
+// The value of this Message is "Hello World". The Message's value is the
+// localized string to be shown for the template ARB file's locale.
+// The docs for the Placeholder explain how placeholder entries are defined.
 class Message {
   Message(Map<String, dynamic> bundle, this.resourceId)
     : assert(bundle != null),
@@ -169,3 +357,90 @@
     }).toList();
   }
 }
+
+// Represents the contents of one ARB file.
+class AppResourceBundle {
+  factory AppResourceBundle(File file) {
+    assert(file != null);
+    // Assuming that the caller has verified that the file exists and is readable.
+
+    final Map<String, dynamic> resources = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
+    String localeString = resources['@@locale'] as String;
+    if (localeString == null) {
+      final RegExp filenameRE = RegExp(r'^[^_]*_(\w+)\.arb$');
+      final RegExpMatch match = filenameRE.firstMatch(file.path);
+      localeString = match == null ? null : match[1];
+    }
+    if (localeString == null) {
+      throw L10nException(
+        "The following .arb file's locale could not be determined: \n"
+        '${file.path} \n'
+        "Make sure that the locale is specified in the file's '@@locale' "
+        'property or as part of the filename (e.g. file_en.arb)'
+       );
+     }
+
+    final Iterable<String> ids = resources.keys.where((String key) => !key.startsWith('@'));
+    return AppResourceBundle._(file, LocaleInfo.fromString(localeString), resources, ids);
+  }
+
+  const AppResourceBundle._(this.file, this.locale, this.resources, this.resourceIds);
+
+  final File file;
+  final LocaleInfo locale;
+  final Map<String, dynamic> resources;
+  final Iterable<String> resourceIds;
+
+  String translationFor(Message message) => resources[message.resourceId] as String;
+
+  @override
+  String toString() {
+    return 'AppResourceBundle($locale, ${file.path})';
+  }
+}
+
+// Represents all of the ARB files in [directory] as [AppResourceBundle]s.
+class AppResourceBundleCollection {
+  factory AppResourceBundleCollection(Directory directory) {
+    assert(directory != null);
+    // Assuming that the caller has verified that the directory is readable.
+
+    final RegExp filenameRE = RegExp(r'(\w+)\.arb$');
+    final Map<LocaleInfo, AppResourceBundle> localeToBundle = <LocaleInfo, AppResourceBundle>{};
+    final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{};
+    final List<File> files = directory.listSync().whereType<File>().toList()..sort(sortFilesByPath);
+    for (final File file in files) {
+      if (filenameRE.hasMatch(file.path)) {
+        final AppResourceBundle bundle = AppResourceBundle(file);
+        if (localeToBundle[bundle.locale] != null) {
+          throw L10nException(
+            "Multiple arb files with the same '${bundle.locale}' locale detected. \n"
+            'Ensure that there is exactly one arb file for each locale.'
+          );
+        }
+        localeToBundle[bundle.locale] = bundle;
+        languageToLocales[bundle.locale.languageCode] ??= <LocaleInfo>[];
+        languageToLocales[bundle.locale.languageCode].add(bundle.locale);
+      }
+    }
+    return AppResourceBundleCollection._(directory, localeToBundle, languageToLocales);
+  }
+
+  const AppResourceBundleCollection._(this._directory, this._localeToBundle, this._languageToLocales);
+
+  final Directory _directory;
+  final Map<LocaleInfo, AppResourceBundle> _localeToBundle;
+  final Map<String, List<LocaleInfo>> _languageToLocales;
+
+  Iterable<LocaleInfo> get locales => _localeToBundle.keys;
+  Iterable<AppResourceBundle> get bundles => _localeToBundle.values;
+  AppResourceBundle bundleFor(LocaleInfo locale) => _localeToBundle[locale];
+
+  Iterable<String> get languages => _languageToLocales.keys;
+  Iterable<LocaleInfo> localesForLanguage(String language) => _languageToLocales[language] ?? <LocaleInfo>[];
+
+  @override
+  String toString() {
+    return 'AppResourceBundleCollection(${_directory.path}, ${locales.length} locales)';
+  }
+}
diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart
index 7847b79..e090bc0 100644
--- a/dev/tools/test/localization/gen_l10n_test.dart
+++ b/dev/tools/test/localization/gen_l10n_test.dart
@@ -25,7 +25,6 @@
     "description": "Title for the application"
   }
 }''';
-
 const String esArbFileName = 'app_es.arb';
 const String singleEsMessageArbFileString = '''
 {
@@ -48,10 +47,11 @@
 void main() {
   MemoryFileSystem fs;
 
-  setUp(() {
+  setUp(() async {
     fs = MemoryFileSystem(
       style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix
     );
+    await precacheLanguageAndRegionTags();
   });
 
   group('Setters', () {
@@ -233,7 +233,7 @@
     });
   });
 
-  group('parseArbFiles', () {
+  group('loadResources', () {
     test('correctly initializes supportedLocales and supportedLanguageCodes properties', () {
       _standardFlutterDirectoryL10nSetup(fs);
 
@@ -246,7 +246,7 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
         fail('Setting language and locales should not fail: \n$e');
       }
@@ -275,7 +275,7 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
         fail('Setting language and locales should not fail: \n$e');
       }
@@ -306,7 +306,7 @@
           classNameString: defaultClassNameString,
           preferredSupportedLocaleString: preferredSupportedLocaleString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
         fail('Setting language and locales should not fail: \n$e');
       }
@@ -340,7 +340,7 @@
             classNameString: defaultClassNameString,
             preferredSupportedLocaleString: preferredSupportedLocaleString,
           );
-          generator.parseArbFiles();
+          generator.loadResources();
         } on L10nException catch (e) {
           expect(
             e.message,
@@ -381,7 +381,7 @@
             classNameString: defaultClassNameString,
             preferredSupportedLocaleString: preferredSupportedLocaleString,
           );
-          generator.parseArbFiles();
+          generator.loadResources();
         } on L10nException catch (e) {
           expect(
             e.message,
@@ -417,7 +417,7 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
         fail('Setting language and locales should not fail: \n$e');
       }
@@ -468,7 +468,7 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
         fail('Setting language and locales should not fail: \n$e');
       }
@@ -512,7 +512,7 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
         fail('Setting language and locales should not fail: \n$e');
       }
@@ -538,7 +538,7 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
         expect(e.message, contains('locale could not be determined'));
         return;
@@ -572,9 +572,9 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
+        generator.loadResources();
       } on L10nException catch (e) {
-        expect(e.message, contains('Multiple arb files with the same locale detected'));
+        expect(e.message, contains("Multiple arb files with the same 'en' locale detected"));
         return;
       }
 
@@ -585,11 +585,12 @@
     });
   });
 
-  group('generateClassMethods', () {
+  group('generateCode', () {
     group('DateTime tests', () {
       test('throws an exception when improperly formatted date is passed in', () {
         const String singleDateMessageArbFileString = '''
 {
+  "@@locale": "en",
   "springBegins": "Spring begins on {springStartDate}",
   "@springBegins": {
       "description": "The first day of spring",
@@ -614,8 +615,8 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
           expect(e.message, contains('asdf'));
           expect(e.message, contains('springStartDate'));
@@ -652,8 +653,8 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
           expect(e.message, contains('the "format" attribute needs to be set'));
           return;
@@ -662,199 +663,6 @@
         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,
-          r'''
-  String springGreetings(DateTime springStartDate, Object helloWorld) {
-    final DateFormat springStartDateDateFormat = DateFormat.yMMMMEEEEd(_localeName);
-    final String springStartDateString = springStartDateDateFormat.format(springStartDate);
-
-    String springGreetings(Object springStartDate, Object helloWorld) {
-      return Intl.message(
-        "Since it's ${springStartDate}, it's finally spring! ${helloWorld}!",
-        locale: _localeName,
-        name: 'springGreetings',
-        desc: "A realization that it's finally the spring season, followed by a greeting.",
-        args: <Object>[springStartDate, helloWorld]
-      );
-    }
-    return springGreetings(springStartDateString, helloWorld);
-  }
-''');
-      });
-    });
-
-    group('Number tests', () {
-      test('correctly adds optional named parameters to numbers', () {
-        const Set<String> numberFormatsWithNamedParameters = <String>{
-          'compact',
-          'compactCurrency',
-          'compactSimpleCurrency',
-          'compactLong',
-          'currency',
-          'decimalPercentPattern',
-          'simpleCurrency',
-        };
-
-        for (final String numberFormat in numberFormatsWithNamedParameters) {
-          final String singleNumberMessage = '''
-{
-  "courseCompletion": "You have completed {progress} of the course.",
-  "@courseCompletion": {
-    "description": "The amount of progress the student has made in their class.",
-    "placeholders": {
-      "progress": {
-        "type": "double",
-        "format": "$numberFormat",
-        "optionalParameters": {
-          "decimalDigits": 2
-        }
-      }
-    }
-  }
-}''';
-          final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
-            ..createSync(recursive: true);
-          l10nDirectory.childFile(defaultTemplateArbFileName)
-            .writeAsStringSync(singleNumberMessage);
-
-          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 courseCompletion(double progress) {
-    final NumberFormat progressNumberFormat = NumberFormat.$numberFormat(
-      locale: _localeName,
-      decimalDigits: 2,
-    );
-    final String progressString = progressNumberFormat.format(progress);
-
-    String courseCompletion(Object progress) {
-      return Intl.message(
-        'You have completed \${progress} of the course.',
-        locale: _localeName,
-        name: 'courseCompletion',
-        desc: 'The amount of progress the student has made in their class.',
-        args: <Object>[progress]
-      );
-    }
-    return courseCompletion(progressString);
-  }
-''');}
-      });
-
-      test('correctly adds optional positional parameters to numbers', () {
-        const Set<String> numberFormatsWithPositionalParameters = <String>{
-          'decimalPattern',
-          'percentPattern',
-          'scientificPattern',
-        };
-
-        for (final String numberFormat in numberFormatsWithPositionalParameters) {
-          final String singleNumberMessage = '''
-{
-  "courseCompletion": "You have completed {progress} of the course.",
-  "@courseCompletion": {
-    "description": "The amount of progress the student has made in their class.",
-    "placeholders": {
-      "progress": {
-        "type": "double",
-        "format": "$numberFormat"
-      }
-    }
-  }
-}''';
-          final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
-            ..createSync(recursive: true);
-          l10nDirectory.childFile(defaultTemplateArbFileName)
-            .writeAsStringSync(singleNumberMessage);
-
-          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 courseCompletion(double progress) {
-    final NumberFormat progressNumberFormat = NumberFormat.$numberFormat(_localeName);
-    final String progressString = progressNumberFormat.format(progress);
-
-    String courseCompletion(Object progress) {
-      return Intl.message(
-        'You have completed \${progress} of the course.',
-        locale: _localeName,
-        name: 'courseCompletion',
-        desc: 'The amount of progress the student has made in their class.',
-        args: <Object>[progress]
-      );
-    }
-    return courseCompletion(progressString);
-  }
-''');
-        }
-      });
-
       test('throws an exception when improperly formatted number is passed in', () {
         const String singleDateMessageArbFileString = '''
 {
@@ -882,8 +690,8 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
           expect(e.message, contains('asdf'));
           expect(e.message, contains('progress'));
@@ -918,8 +726,8 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
           expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format'));
           return;
@@ -950,8 +758,8 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
           expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format'));
           return;
@@ -978,8 +786,8 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
           expect(e.message, contains('Resource attribute "@helloWorlds" was not found'));
           return;
@@ -1009,8 +817,8 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
           expect(e.message, contains('is not properly formatted'));
           expect(e.message, contains('Ensure that it is a map with string valued keys'));
@@ -1041,8 +849,8 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
-        generator.generateClassMethods();
+        generator.loadResources();
+        generator.generateCode();
       } on FormatException catch (e) {
         expect(e.message, contains('Unexpected character'));
         return;
@@ -1072,8 +880,8 @@
           outputFileString: defaultOutputFileString,
           classNameString: defaultClassNameString,
         );
-        generator.parseArbFiles();
-        generator.generateClassMethods();
+        generator.loadResources();
+        generator.generateCode();
       } on L10nException catch (e) {
         expect(e.message, contains('Resource attribute "@title" was not found'));
         return;
@@ -1089,11 +897,11 @@
       test('cannot contain non-alphanumeric symbols', () {
         const String nonAlphaNumericArbFile = '''
 {
-    "title!!": "Stocks",
-    "@title!!": {
-      "description": "Title for the Stocks application"
-    }
-  }''';
+  "title!!": "Stocks",
+  "@title!!": {
+    "description": "Title for the Stocks application"
+  }
+}''';
         final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
           ..createSync(recursive: true);
         l10nDirectory.childFile(defaultTemplateArbFileName)
@@ -1107,10 +915,10 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
-          expect(e.message, contains('Invalid key format'));
+          expect(e.message, contains('Invalid ARB resource name'));
           return;
         }
 
@@ -1120,11 +928,11 @@
       test('must start with lowercase character', () {
         const String nonAlphaNumericArbFile = '''
 {
-    "Title": "Stocks",
-    "@Title": {
-      "description": "Title for the Stocks application"
-    }
-  }''';
+  "Title": "Stocks",
+  "@Title": {
+    "description": "Title for the Stocks application"
+  }
+}''';
         final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
           ..createSync(recursive: true);
         l10nDirectory.childFile(defaultTemplateArbFileName)
@@ -1138,10 +946,10 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
-          expect(e.message, contains('Invalid key format'));
+          expect(e.message, contains('Invalid ARB resource name'));
           return;
         }
 
@@ -1151,11 +959,11 @@
       test('cannot start with a number', () {
         const String nonAlphaNumericArbFile = '''
 {
-    "123title": "Stocks",
-    "@123title": {
-      "description": "Title for the Stocks application"
-    }
-  }''';
+  "123title": "Stocks",
+  "@123title": {
+    "description": "Title for the Stocks application"
+  }
+}''';
         final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
           ..createSync(recursive: true);
         l10nDirectory.childFile(defaultTemplateArbFileName)
@@ -1169,10 +977,10 @@
             outputFileString: defaultOutputFileString,
             classNameString: defaultClassNameString,
           );
-          generator.parseArbFiles();
-          generator.generateClassMethods();
+          generator.loadResources();
+          generator.generateCode();
         } on L10nException catch (e) {
-          expect(e.message, contains('Invalid key format'));
+          expect(e.message, contains('Invalid ARB resource name'));
           return;
         }