gen_localizations.dart 26.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7 8 9
// This program generates a getMaterialTranslation() and a
// getCupertinoTranslation() function that look up the translations provided by
// the arb files. The returned value is a generated instance of a
// GlobalMaterialLocalizations or a GlobalCupertinoLocalizations that
// corresponds to a single locale.
10
//
11
// The *.arb files are in packages/flutter_localizations/lib/src/l10n.
12 13 14 15 16
//
// The arb (JSON) format files must contain a single map indexed by locale.
// Each map value is itself a map with resource identifier keys and localized
// resource string values.
//
17 18 19 20
// The arb filenames are expected to have the form "material_(\w+)\.arb" or
// "cupertino_(\w+)\.arb" where the group following "_" identifies the language
// code and the country code, e.g. "material_en.arb" or "material_en_GB.arb".
// In most cases both codes are just two characters.
21 22 23 24
//
// This app is typically run by hand when a module's .arb files have been
// updated.
//
25 26 27 28 29 30 31
// ## Usage
//
// Run this program from the root of the git repository.
//
// The following outputs the generated Dart code to the console as a dry run:
//
// ```
32
// dart dev/tools/localization/bin/gen_localizations.dart
33 34
// ```
//
35 36 37 38 39 40 41
// If you have removed localizations from the canonical localizations, then
// add the '--remove-undefined' flag to also remove them from the other files.
//
// ```
// dart dev/tools/localization/bin/gen_localizations.dart --remove-undefined
// ```
//
42
// If the data looks good, use the `-w` or `--overwrite` option to overwrite the
43 44
// packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart
// and packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart file:
45 46
//
// ```
47
// dart dev/tools/localization/bin/gen_localizations.dart --overwrite
48
// ```
49 50 51

import 'dart:io';

52
import 'package:path/path.dart' as path;
53

54 55 56 57
import '../gen_cupertino_localizations.dart';
import '../gen_material_localizations.dart';
import '../localizations_utils.dart';
import '../localizations_validator.dart';
58 59
import 'encode_kn_arb_files.dart';

60 61
/// This is the core of this script; it generates the code used for translations.
String generateArbBasedLocalizationSubclasses({
62 63 64 65 66 67 68 69 70 71 72
  required Map<LocaleInfo, Map<String, String>> localeToResources,
  required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes,
  required String generatedClassPrefix,
  required String baseClass,
  required HeaderGenerator generateHeader,
  required ConstructorGenerator generateConstructor,
  required String factoryName,
  required String factoryDeclaration,
  required String factoryArguments,
  required String supportedLanguagesConstant,
  required String supportedLanguagesDocMacro,
73 74 75
}) {
  assert(generatedClassPrefix.isNotEmpty);
  assert(baseClass.isNotEmpty);
76
  assert(factoryName.isNotEmpty);
77 78
  assert(factoryDeclaration.isNotEmpty);
  assert(factoryArguments.isNotEmpty);
79 80
  assert(supportedLanguagesConstant.isNotEmpty);
  assert(supportedLanguagesDocMacro.isNotEmpty);
81

82
  final StringBuffer output = StringBuffer();
83
  output.writeln(generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'));
84

85
  final StringBuffer supportedLocales = StringBuffer();
86

87 88 89 90
  final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{};
  final Map<String, Set<String>> languageToScriptCodes = <String, Set<String>>{};
  // Used to calculate if there are any corresponding countries for a given language and script.
  final Map<LocaleInfo, Set<String>> languageAndScriptToCountryCodes = <LocaleInfo, Set<String>>{};
91
  final Set<String> allResourceIdentifiers = <String>{};
92
  for (final LocaleInfo locale in localeToResources.keys.toList()..sort()) {
93
    if (locale.scriptCode != null) {
94
      languageToScriptCodes[locale.languageCode] ??= <String>{};
95
      languageToScriptCodes[locale.languageCode]!.add(locale.scriptCode!);
96 97
    }
    if (locale.countryCode != null && locale.scriptCode != null) {
98
      final LocaleInfo key = LocaleInfo.fromString('${locale.languageCode}_${locale.scriptCode}');
99
      languageAndScriptToCountryCodes[key] ??= <String>{};
100
      languageAndScriptToCountryCodes[key]!.add(locale.countryCode!);
101 102
    }
    languageToLocales[locale.languageCode] ??= <LocaleInfo>[];
103 104
    languageToLocales[locale.languageCode]!.add(locale);
    allResourceIdentifiers.addAll(localeToResources[locale]!.keys.toList()..sort());
105 106
  }

107
  // We generate one class per supported language (e.g.
108 109
  // `MaterialLocalizationEn`). These implement everything that is needed by the
  // superclass (e.g. GlobalMaterialLocalizations).
110

111 112 113 114 115
  // We also generate one subclass for each locale with a script code (e.g.
  // `MaterialLocalizationZhHant`). Their superclasses are the aforementioned
  // language classes for the same locale but without a script code (e.g.
  // `MaterialLocalizationZh`).

116 117 118
  // We also generate one subclass for each locale with a country code (e.g.
  // `MaterialLocalizationEnGb`). Their superclasses are the aforementioned
  // language classes for the same locale but without a country code (e.g.
119 120 121 122
  // `MaterialLocalizationEn`).

  // If scriptCodes for a language are defined, we expect a scriptCode to be
  // defined for locales that contain a countryCode. The superclass becomes
123
  // the script subclass (e.g. `MaterialLocalizationZhHant`) and the generated
124 125 126 127 128 129
  // subclass will also contain the script code (e.g. `MaterialLocalizationZhHantTW`).

  // When scriptCodes are not defined for languages that use scriptCodes to distinguish
  // between significantly differing scripts, we assume the scriptCodes in the
  // [LocaleInfo.fromString] factory and add it to the [LocaleInfo]. We then generate
  // the script classes based on the first locale that we assume to use the script.
130 131 132

  final List<String> allKeys = allResourceIdentifiers.toList()..sort();
  final List<String> languageCodes = languageToLocales.keys.toList()..sort();
133
  final LocaleInfo canonicalLocale = LocaleInfo.fromString('en');
134
  for (final String languageName in languageCodes) {
135
    final LocaleInfo languageLocale = LocaleInfo.fromString(languageName);
136

137 138 139
    output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
    output.writeln(generateConstructor(languageLocale));

140
    final Map<String, String> languageResources = localeToResources[languageLocale]!;
141
    for (final String key in allKeys) {
142
      final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
143
      output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale));
144
    }
145 146
    output.writeln('}');
    int countryCodeCount = 0;
147 148
    int scriptCodeCount = 0;
    if (languageToScriptCodes.containsKey(languageName)) {
149
      scriptCodeCount = languageToScriptCodes[languageName]!.length;
150 151
      // Language has scriptCodes, so we need to properly fallback countries to corresponding
      // script default values before language default values.
152
      for (final String scriptCode in languageToScriptCodes[languageName]!) {
153
        final LocaleInfo scriptBaseLocale = LocaleInfo.fromString('${languageName}_$scriptCode');
154 155 156
        output.writeln(generateClassDeclaration(
          scriptBaseLocale,
          generatedClassPrefix,
157
          '$generatedClassPrefix${languageLocale.camelCase()}',
158 159
        ));
        output.writeln(generateConstructor(scriptBaseLocale));
160
        final Map<String, String> scriptResources = localeToResources[scriptBaseLocale]!;
161
        for (final String key in scriptResources.keys.toList()..sort()) {
162
          if (languageResources[key] == scriptResources[key]) {
163
            continue;
164
          }
165
          final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
166
          output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale));
167 168 169
        }
        output.writeln('}');

170
        final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort();
171
        for (final LocaleInfo locale in localeCodes) {
172
          if (locale.originalString == languageName) {
173
            continue;
174 175
          }
          if (locale.originalString == '${languageName}_$scriptCode') {
176
            continue;
177 178
          }
          if (locale.scriptCode != scriptCode) {
179
            continue;
180
          }
181
          countryCodeCount += 1;
182 183 184
          output.writeln(generateClassDeclaration(
            locale,
            generatedClassPrefix,
185
            '$generatedClassPrefix${scriptBaseLocale.camelCase()}',
186 187
          ));
          output.writeln(generateConstructor(locale));
188
          final Map<String, String> localeResources = localeToResources[locale]!;
189
          for (final String key in localeResources.keys) {
190
            // When script fallback contains the key, we compare to it instead of language fallback.
191
            if (scriptResources.containsKey(key) ? scriptResources[key] == localeResources[key] : languageResources[key] == localeResources[key]) {
192
              continue;
193
            }
194
            final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
195
            output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
196 197 198 199 200 201 202
          }
         output.writeln('}');
        }
      }
    } else {
      // No scriptCode. Here, we do not compare against script default (because it
      // doesn't exist).
203
      final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort();
204
      for (final LocaleInfo locale in localeCodes) {
205
        if (locale.originalString == languageName) {
206
          continue;
207
        }
208
        countryCodeCount += 1;
209
        final Map<String, String> localeResources = localeToResources[locale]!;
210 211 212
        output.writeln(generateClassDeclaration(
          locale,
          generatedClassPrefix,
213
          '$generatedClassPrefix${languageLocale.camelCase()}',
214 215
        ));
        output.writeln(generateConstructor(locale));
216
        for (final String key in localeResources.keys) {
217
          if (languageResources[key] == localeResources[key]) {
218
            continue;
219
          }
220
          final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
221
          output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
222 223
        }
       output.writeln('}');
224
      }
225
    }
226

227
    final String scriptCodeMessage = scriptCodeCount == 0 ? '' : ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}';
228
    if (countryCodeCount == 0) {
229
      if (scriptCodeCount == 0) {
230
        supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)}');
231
      } else {
232
        supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})');
233
      }
234

235
    } else if (countryCodeCount == 1) {
236
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)');
237
    } else {
238
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)');
239 240 241
    }
  }

242 243
  // Generate the factory function. Given a Locale it returns the corresponding
  // base class implementation.
244 245
  output.writeln('''

246 247
/// The set of supported languages, as language code strings.
///
248
/// The [$baseClass.delegate] can generate localizations for
249 250 251 252 253 254 255
/// any [Locale] with a language code from this set, regardless of the region.
/// Some regions have specific support (e.g. `de` covers all forms of German,
/// but there is support for `de-CH` specifically to override some of the
/// translations for Switzerland).
///
/// See also:
///
256 257 258
///  * [$factoryName], whose documentation describes these values.
final Set<String> $supportedLanguagesConstant = HashSet<String>.from(const <String>[
${languageCodes.map<String>((String value) => "  '$value', // ${describeLocale(value)}").toList().join('\n')}
259 260
]);

261
/// Creates a [$baseClass] instance for the given `locale`.
262
///
263 264
/// All of the function's arguments except `locale` will be passed to the [
/// $baseClass] constructor. (The `localeName` argument of that
265 266 267 268 269
/// constructor is specified by the actual subclass constructor by this
/// function.)
///
/// The following locales are supported by this package:
///
270
/// {@template $supportedLanguagesDocMacro}
271 272 273
$supportedLocales/// {@endtemplate}
///
/// Generally speaking, this method is only intended to be used by
274 275
/// [$baseClass.delegate].
$factoryDeclaration
276
  switch (locale.languageCode) {''');
277
  for (final String language in languageToLocales.keys) {
278
    // Only one instance of the language.
279
    if (languageToLocales[language]!.length == 1) {
280
      output.writeln('''
281
    case '$language':
282
      return $generatedClassPrefix${languageToLocales[language]![0].camelCase()}($factoryArguments);''');
283
    } else if (!languageToScriptCodes.containsKey(language)) { // Does not distinguish between scripts. Switch on countryCode directly.
284
      output.writeln('''
285 286
    case '$language': {
      switch (locale.countryCode) {''');
287
      for (final LocaleInfo locale in languageToLocales[language]!) {
288
        if (locale.originalString == language) {
289
          continue;
290
        }
291
        assert(locale.length > 1);
292
        final String countryCode = locale.countryCode!;
293
        output.writeln('''
294
        case '$countryCode':
295
          return $generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
296 297 298
      }
      output.writeln('''
      }
299
      return $generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
300 301 302 303 304 305
    }''');
    } else { // Language has scriptCode, add additional switch logic.
      bool hasCountryCode = false;
      output.writeln('''
    case '$language': {
      switch (locale.scriptCode) {''');
306
      for (final String scriptCode in languageToScriptCodes[language]!) {
307
        final LocaleInfo scriptLocale = LocaleInfo.fromString('${language}_$scriptCode');
308 309 310 311 312
        output.writeln('''
        case '$scriptCode': {''');
        if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
          output.writeln('''
          switch (locale.countryCode) {''');
313
          for (final LocaleInfo locale in languageToLocales[language]!) {
314
            if (locale.countryCode == null) {
315
              continue;
316
            } else {
317
              hasCountryCode = true;
318 319
            }
            if (locale.originalString == language) {
320
              continue;
321 322
            }
            if (locale.scriptCode != scriptCode && locale.scriptCode != null) {
323
              continue;
324
            }
325
            final String countryCode = locale.countryCode!;
326 327
            output.writeln('''
            case '$countryCode':
328
              return $generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
329 330 331 332 333
          }
        }
        // Return a fallback locale that matches scriptCode, but not countryCode.
        //
        // Explicitly defined scriptCode fallback:
334
        if (languageToLocales[language]!.contains(scriptLocale)) {
335 336 337 338 339
          if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
            output.writeln('''
          }''');
          }
          output.writeln('''
340
          return $generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
341 342 343 344
        }''');
        } else {
          // Not Explicitly defined, fallback to first locale with the same language and
          // script:
345
          for (final LocaleInfo locale in languageToLocales[language]!) {
346
            if (locale.scriptCode != scriptCode) {
347
              continue;
348
            }
349 350 351 352 353
            if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
              output.writeln('''
          }''');
            }
            output.writeln('''
354
          return $generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
355 356 357 358
        }''');
            break;
          }
        }
359 360
      }
      output.writeln('''
361 362 363 364
      }''');
      if (hasCountryCode) {
      output.writeln('''
      switch (locale.countryCode) {''');
365
        for (final LocaleInfo locale in languageToLocales[language]!) {
366
          if (locale.originalString == language) {
367
            continue;
368
          }
369
          assert(locale.length > 1);
370
          if (locale.countryCode == null) {
371
            continue;
372
          }
373
          final String countryCode = locale.countryCode!;
374 375
          output.writeln('''
        case '$countryCode':
376
          return $generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
377 378 379
        }
        output.writeln('''
      }''');
380
      }
381
      output.writeln('''
382
      return $generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
383 384 385 386
    }''');
    }
  }
  output.writeln('''
387
  }
388
  assert(false, '$factoryName() called for unsupported locale "\$locale"');
389
  return null;
390
}''');
391 392 393 394

  return output.toString();
}

395 396 397 398 399
/// Returns the appropriate type for getters with the given attributes.
///
/// Typically "String", but some (e.g. "timeOfDayFormat") return enums.
///
/// Used by [generateGetter] below.
400
String generateType(Map<String, dynamic>? attributes) {
401 402
  bool optional = false;
  String type = 'String';
403
  if (attributes != null) {
404
    optional = attributes.containsKey('optional');
405
    switch (attributes['x-flutter-type'] as String?) {
406
      case 'icuShortTimePattern':
407
        type = 'TimeOfDayFormat';
408
      case 'scriptCategory':
409
        type = 'ScriptCategory';
410 411
    }
  }
412
  return type + (optional ? '?' : '');
413 414 415 416 417 418 419 420 421
}

/// Returns the appropriate name for getters with the given attributes.
///
/// Typically this is the key unmodified, but some have parameters, and
/// the GlobalMaterialLocalizations class does the substitution, and for
/// those we have to therefore provide an alternate name.
///
/// Used by [generateGetter] below.
422
String generateKey(String key, Map<String, dynamic>? attributes) {
423
  if (attributes != null) {
424
    if (attributes.containsKey('parameters')) {
425
      return '${key}Raw';
426
    }
427
    switch (attributes['x-flutter-type'] as String?) {
428 429 430 431
      case 'icuShortTimePattern':
        return '${key}Raw';
    }
  }
432
  if (key == 'datePickerDateOrder') {
433
    return 'datePickerDateOrderString';
434 435
  }
  if (key == 'datePickerDateTimeOrder') {
436
    return 'datePickerDateTimeOrderString';
437
  }
438 439 440 441 442 443 444 445 446 447 448 449 450 451
  return key;
}

const Map<String, String> _icuTimeOfDayToEnum = <String, String>{
  'HH:mm': 'TimeOfDayFormat.HH_colon_mm',
  'HH.mm': 'TimeOfDayFormat.HH_dot_mm',
  "HH 'h' mm": 'TimeOfDayFormat.frenchCanadian',
  'HH:mm .': 'TimeOfDayFormat.HH_colon_mm',
  'H:mm': 'TimeOfDayFormat.H_colon_mm',
  'h:mm a': 'TimeOfDayFormat.h_colon_mm_space_a',
  'a h:mm': 'TimeOfDayFormat.a_space_h_colon_mm',
  'ah:mm': 'TimeOfDayFormat.a_space_h_colon_mm',
};

452 453 454 455 456 457
const Map<String, String> _scriptCategoryToEnum = <String, String>{
  'English-like': 'ScriptCategory.englishLike',
  'dense': 'ScriptCategory.dense',
  'tall': 'ScriptCategory.tall',
};

458 459 460 461 462 463 464 465
/// Returns the literal that describes the value returned by getters
/// with the given attributes.
///
/// This handles cases like the value being a literal `null`, an enum, and so
/// on. The default is to treat the value as a string and escape it and quote
/// it.
///
/// Used by [generateGetter] below.
466
String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
467
  if (value == null) {
468
    return null;
469
  }
470
  // cupertino_en.arb doesn't use x-flutter-type.
471
  if (attributes != null) {
472
    switch (attributes['x-flutter-type'] as String?) {
473 474
      case 'icuShortTimePattern':
        if (!_icuTimeOfDayToEnum.containsKey(value)) {
475
          throw Exception(
476 477
            '"$value" is not one of the ICU short time patterns supported '
            'by the material library. Here is the list of supported '
478
            'patterns:\n  ${_icuTimeOfDayToEnum.keys.join('\n  ')}'
479 480 481
          );
        }
        return _icuTimeOfDayToEnum[value];
482 483 484 485 486
      case 'scriptCategory':
        if (!_scriptCategoryToEnum.containsKey(value)) {
          throw Exception(
            '"$value" is not one of the scriptCategory values supported '
            'by the material library. Here is the list of supported '
487
            'values:\n  ${_scriptCategoryToEnum.keys.join('\n  ')}'
488 489 490
          );
        }
        return _scriptCategoryToEnum[value];
491 492
    }
  }
493
  return  generateEncodedString(locale.languageCode, value);
494 495 496 497
}

/// Combines [generateType], [generateKey], and [generateValue] to return
/// the source of getters for the GlobalMaterialLocalizations subclass.
498
/// The locale is the locale for which the getter is being generated.
499
String generateGetter(String key, String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
500 501
  final String type = generateType(attributes);
  key = generateKey(key, attributes);
502
  final String? generatedValue = generateValue(value, attributes, locale);
503 504 505
      return '''

  @override
506
  $type get $key => $generatedValue;''';
507 508
}

509
void main(List<String> rawArgs) {
510 511
  checkCwdIsRepoRoot('gen_localizations');
  final GeneratorOptions options = parseArgs(rawArgs);
512 513 514 515 516

  // filenames are assumed to end in "prefix_lc.arb" or "prefix_lc_cc.arb", where prefix
  // is the 2nd command line argument, lc is a language code and cc is the country
  // code. In most cases both codes are just two characters.

517
  final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
518
  final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
519
  final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
520

521
  try {
522
    validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
523
    validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb')));
524 525 526 527
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }

528 529 530 531 532 533 534 535 536 537
  // Only rewrite material_kn.arb and cupertino_en.arb if overwriting the
  // Material and Cupertino localizations files.
  if (options.writeToFile) {
    // Encodes the material_kn.arb file and the cupertino_en.arb files before
    // generating localizations. This prevents a subset of Emacs users from
    // crashing when opening up the Flutter source code.
    // See https://github.com/flutter/flutter/issues/36704 for more context.
    encodeKnArbFiles(directory);
  }

538
  precacheLanguageAndRegionTags();
539

540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
  // Maps of locales to resource key/value pairs for Material ARBs.
  final Map<LocaleInfo, Map<String, String>> materialLocaleToResources = <LocaleInfo, Map<String, String>>{};
  // Maps of locales to resource key/attributes pairs for Material ARBs..
  // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
  final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
  // Maps of locales to resource key/value pairs for Cupertino ARBs.
  final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources = <LocaleInfo, Map<String, String>>{};
  // Maps of locales to resource key/attributes pairs for Cupertino ARBs..
  // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
  final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};

  loadMatchingArbsIntoBundleMaps(
    directory: directory,
    filenamePattern: materialFilenameRE,
    localeToResources: materialLocaleToResources,
    localeToResourceAttributes: materialLocaleToResourceAttributes,
  );
  loadMatchingArbsIntoBundleMaps(
    directory: directory,
    filenamePattern: cupertinoFilenameRE,
    localeToResources: cupertinoLocaleToResources,
    localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
  );
563

564
  try {
565 566
    validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
    validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
567 568 569
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
570

571 572 573 574 575
  if (options.removeUndefined) {
    removeUndefinedLocalizations(materialLocaleToResources);
    removeUndefinedLocalizations(cupertinoLocaleToResources);
  }

576
  final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly
577 578 579 580 581 582 583 584 585 586 587 588 589 590
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: materialLocaleToResources,
        localeToResourceAttributes: materialLocaleToResourceAttributes,
        generatedClassPrefix: 'MaterialLocalization',
        baseClass: 'GlobalMaterialLocalizations',
        generateHeader: generateMaterialHeader,
        generateConstructor: generateMaterialConstructor,
        factoryName: materialFactoryName,
        factoryDeclaration: materialFactoryDeclaration,
        factoryArguments: materialFactoryArguments,
        supportedLanguagesConstant: materialSupportedLanguagesConstant,
        supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
      )
      : null;
591
  final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly
592 593 594 595 596 597 598 599 600 601 602 603 604 605
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: cupertinoLocaleToResources,
        localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
        generatedClassPrefix: 'CupertinoLocalization',
        baseClass: 'GlobalCupertinoLocalizations',
        generateHeader: generateCupertinoHeader,
        generateConstructor: generateCupertinoConstructor,
        factoryName: cupertinoFactoryName,
        factoryDeclaration: cupertinoFactoryDeclaration,
        factoryArguments: cupertinoFactoryArguments,
        supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
        supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
      )
      : null;
606 607

  if (options.writeToFile) {
608
    final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
609
    materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
610
    final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart'));
611
    cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
612
  } else {
613 614 615 616 617 618
    if (!options.cupertinoOnly) {
      stdout.write(materialLocalizations);
    }
    if (!options.materialOnly) {
      stdout.write(cupertinoLocalizations);
    }
619
  }
620
}