Gen localization classes intead of a big map (#13653)

* Gen localization classes intead of a big map

* tighten up the newlines so that std output matches file output

* restore locale sorting
diff --git a/dev/tools/gen_localizations.dart b/dev/tools/gen_localizations.dart
index 5347685..a2edf2d 100644
--- a/dev/tools/gen_localizations.dart
+++ b/dev/tools/gen_localizations.dart
@@ -91,28 +91,139 @@
   return output.toString();
 }
 
-String generateLocalizationsMap() {
+String generateTranslationBundles() {
   final StringBuffer output = new StringBuffer();
 
-  output.writeln('''
-/// Maps from [Locale.languageCode] to a map that contains the localized strings
-/// for that locale.
-///
-/// This variable is used by [MaterialLocalizations].
-const Map<String, Map<String, String>> localizations = const <String, Map<String, String>> {''');
-
-  for (String locale in localeToResources.keys.toList()..sort()) {
-    output.writeln("  '$locale': const <String, String>{");
-
-    final Map<String, String> resources = localeToResources[locale];
-    for (String name in resources.keys) {
-      final String value = generateString(resources[name]);
-      output.writeln("    '$name': $value,");
-    }
-    output.writeln('  },');
+  final Map<String, List<String>> languageToLocales = <String, List<String>>{};
+  final Set<String> allResourceIdentifiers = new Set<String>();
+  for(String locale in localeToResources.keys.toList()..sort()) {
+    final List<String> codes = locale.split('_'); // [language, country]
+    assert(codes.length == 1 || codes.length == 2);
+    languageToLocales[codes[0]] ??= <String>[];
+    languageToLocales[codes[0]].add(locale);
+    allResourceIdentifiers.addAll(localeToResources[locale].keys);
   }
 
-  output.writeln('};');
+  // Generate the TranslationsBundle base class. It contains one getter
+  // per resource identifier found in any of the .arb files.
+  //
+  // class TranslationsBundle {
+  //   const TranslationsBundle(this.parent);
+  //   final TranslationsBundle parent;
+  //   String get scriptCategory => parent?.scriptCategory;
+  //   ...
+  // }
+  output.writeln('''
+// The TranslationBundle subclasses defined here encode all of the translations
+// found in the flutter_localizations/lib/src/l10n/*.arb files.
+//
+// The [MaterialLocalizations] class uses the (generated)
+// translationBundleForLocale() function to look up a const TranslationBundle
+// instance for a locale.
+
+import \'dart:ui\' show Locale;
+
+class TranslationBundle {
+  const TranslationBundle(this.parent);
+  final TranslationBundle parent;''');
+  for (String key in allResourceIdentifiers)
+    output.writeln('  String get $key => parent?.$key;');
+  output.writeln('''
+}''');
+
+  // Generate one private TranslationBundle subclass per supported
+  // language. Each of these classes overrides every resource identifier
+  // getter. For example:
+  //
+  // class _Bundle_en extends TranslationBundle {
+  //   const _Bundle_en() : super(null);
+  //   @override String get scriptCategory => r'English-like';
+  //   ...
+  // }
+  for(String language in languageToLocales.keys) {
+    final Map<String, String> resources = localeToResources[language];
+    output.writeln('''
+
+// ignore: camel_case_types
+class _Bundle_$language extends TranslationBundle {
+  const _Bundle_$language() : super(null);''');
+    for (String key in resources.keys) {
+      final String value = generateString(resources[key]);
+      output.writeln('''
+  @override String get $key => $value;''');
+    }
+   output.writeln('''
+}''');
+  }
+
+  // Generate one private TranslationBundle subclass for each locale
+  // with a country code. The parent of these subclasses is a const
+  // instance of a translation bundle for the same locale, but without
+  // a country code. These subclasses only override getters that
+  // return different value than the parent class, or a resource identifier
+  // that's not defined in the parent class. For example:
+  //
+  // class _Bundle_en_CA extends TranslationBundle {
+  //   const _Bundle_en_CA() : super(const _Bundle_en());
+  //   @override String get licensesPageTitle => r'Licences';
+  //   ...
+  // }
+  for(String language in languageToLocales.keys) {
+    final Map<String, String> languageResources = localeToResources[language];
+    for(String localeName in languageToLocales[language]) {
+      if (localeName == language)
+        continue;
+      final Map<String, String> localeResources = localeToResources[localeName];
+      output.writeln('''
+
+// ignore: camel_case_types
+class _Bundle_$localeName extends TranslationBundle {
+  const _Bundle_$localeName() : super(const _Bundle_$language());''');
+      for (String key in localeResources.keys) {
+        if (languageResources[key] == localeResources[key])
+          continue;
+        final String value = generateString(localeResources[key]);
+        output.writeln('''
+  @override String get $key => $value;''');
+      }
+     output.writeln('''
+}''');
+    }
+  }
+
+  // Generate the translationBundleForLocale function. Given a Locale
+  // it returns the corresponding const TranslationBundle.
+  output.writeln('''
+
+TranslationBundle translationBundleForLocale(Locale locale) {
+  switch(locale.languageCode) {''');
+  for(String language in languageToLocales.keys) {
+    if (languageToLocales[language].length == 1) {
+      output.writeln('''
+    case \'$language\':
+      return const _Bundle_${languageToLocales[language][0]}();''');
+    } else {
+      output.writeln('''
+    case \'$language\': {
+      switch(locale.toString()) {''');
+      for(String localeName in languageToLocales[language]) {
+        if (localeName == language)
+          continue;
+        output.writeln('''
+        case \'$localeName\':
+          return const _Bundle_$localeName();''');
+      }
+      output.writeln('''
+      }
+      return const _Bundle_$language();
+    }''');
+    }
+  }
+  output.writeln('''
+  }
+  return const TranslationBundle(null);
+}''');
+
   return output.toString();
 }
 
@@ -161,12 +272,12 @@
   final String regenerate = 'dart dev/tools/gen_localizations.dart --overwrite';
   final StringBuffer buffer = new StringBuffer();
   buffer.writeln(outputHeader.replaceFirst('@(regenerate)', regenerate));
-  buffer.writeln(generateLocalizationsMap());
+  buffer.write(generateTranslationBundles());
 
   if (options.writeToFile) {
     final File localizationsFile = new File(pathlib.join(directory.path, 'localizations.dart'));
-    localizationsFile.writeAsStringSync('$buffer');
+    localizationsFile.writeAsStringSync(buffer.toString());
   } else {
-    print(buffer);
+    stdout.write(buffer.toString());
   }
 }