gen_localizations.dart 26.1 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 76 77 78 79
}) {
  assert(localeToResources != null);
  assert(localeToResourceAttributes != null);
  assert(generatedClassPrefix.isNotEmpty);
  assert(baseClass.isNotEmpty);
  assert(generateHeader != null);
  assert(generateConstructor != null);
80
  assert(factoryName.isNotEmpty);
81 82
  assert(factoryDeclaration.isNotEmpty);
  assert(factoryArguments.isNotEmpty);
83 84
  assert(supportedLanguagesConstant.isNotEmpty);
  assert(supportedLanguagesDocMacro.isNotEmpty);
85

86
  final StringBuffer output = StringBuffer();
87
  output.writeln(generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'));
88

89
  final StringBuffer supportedLocales = StringBuffer();
90

91 92 93 94
  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>>{};
95
  final Set<String> allResourceIdentifiers = <String>{};
96
  for (final LocaleInfo locale in localeToResources.keys.toList()..sort()) {
97
    if (locale.scriptCode != null) {
98
      languageToScriptCodes[locale.languageCode] ??= <String>{};
99
      languageToScriptCodes[locale.languageCode]!.add(locale.scriptCode!);
100 101
    }
    if (locale.countryCode != null && locale.scriptCode != null) {
102
      final LocaleInfo key = LocaleInfo.fromString('${locale.languageCode}_${locale.scriptCode}');
103
      languageAndScriptToCountryCodes[key] ??= <String>{};
104
      languageAndScriptToCountryCodes[key]!.add(locale.countryCode!);
105 106
    }
    languageToLocales[locale.languageCode] ??= <LocaleInfo>[];
107 108
    languageToLocales[locale.languageCode]!.add(locale);
    allResourceIdentifiers.addAll(localeToResources[locale]!.keys.toList()..sort());
109 110
  }

111
  // We generate one class per supported language (e.g.
112 113
  // `MaterialLocalizationEn`). These implement everything that is needed by the
  // superclass (e.g. GlobalMaterialLocalizations).
114

115 116 117 118 119
  // 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`).

120 121 122
  // 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.
123 124 125 126
  // `MaterialLocalizationEn`).

  // If scriptCodes for a language are defined, we expect a scriptCode to be
  // defined for locales that contain a countryCode. The superclass becomes
127
  // the script subclass (e.g. `MaterialLocalizationZhHant`) and the generated
128 129 130 131 132 133
  // 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.
134 135 136

  final List<String> allKeys = allResourceIdentifiers.toList()..sort();
  final List<String> languageCodes = languageToLocales.keys.toList()..sort();
137
  final LocaleInfo canonicalLocale = LocaleInfo.fromString('en');
138
  for (final String languageName in languageCodes) {
139
    final LocaleInfo languageLocale = LocaleInfo.fromString(languageName);
140

141 142 143
    output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
    output.writeln(generateConstructor(languageLocale));

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

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

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

231
    } else if (countryCodeCount == 1) {
232
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)');
233
    } else {
234
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)');
235 236 237
    }
  }

238 239
  // Generate the factory function. Given a Locale it returns the corresponding
  // base class implementation.
240 241
  output.writeln('''

242 243
/// The set of supported languages, as language code strings.
///
244
/// The [$baseClass.delegate] can generate localizations for
245 246 247 248 249 250 251
/// 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:
///
252 253 254
///  * [$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')}
255 256
]);

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

  return output.toString();
}

384 385 386 387 388
/// Returns the appropriate type for getters with the given attributes.
///
/// Typically "String", but some (e.g. "timeOfDayFormat") return enums.
///
/// Used by [generateGetter] below.
389
String generateType(Map<String, dynamic>? attributes) {
390 391
  bool optional = false;
  String type = 'String';
392
  if (attributes != null) {
393
    optional = attributes.containsKey('optional');
394
    switch (attributes['x-flutter-type'] as String?) {
395
      case 'icuShortTimePattern':
396 397
        type = 'TimeOfDayFormat';
        break;
398
      case 'scriptCategory':
399 400
        type = 'ScriptCategory';
        break;
401 402
    }
  }
403
  return type + (optional ? '?' : '');
404 405 406 407 408 409 410 411 412
}

/// 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.
413
String generateKey(String key, Map<String, dynamic>? attributes) {
414 415 416
  if (attributes != null) {
    if (attributes.containsKey('parameters'))
      return '${key}Raw';
417
    switch (attributes['x-flutter-type'] as String?) {
418 419 420 421
      case 'icuShortTimePattern':
        return '${key}Raw';
    }
  }
422 423 424 425
  if (key == 'datePickerDateOrder')
    return 'datePickerDateOrderString';
  if (key == 'datePickerDateTimeOrder')
    return 'datePickerDateTimeOrderString';
426 427 428 429 430 431 432 433 434 435 436 437 438 439
  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',
};

440 441 442 443 444 445
const Map<String, String> _scriptCategoryToEnum = <String, String>{
  'English-like': 'ScriptCategory.englishLike',
  'dense': 'ScriptCategory.dense',
  'tall': 'ScriptCategory.tall',
};

446 447 448 449 450 451 452 453
/// 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.
454
String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
455 456
  if (value == null)
    return null;
457
  // cupertino_en.arb doesn't use x-flutter-type.
458
  if (attributes != null) {
459
    switch (attributes['x-flutter-type'] as String?) {
460 461
      case 'icuShortTimePattern':
        if (!_icuTimeOfDayToEnum.containsKey(value)) {
462
          throw Exception(
463 464
            '"$value" is not one of the ICU short time patterns supported '
            'by the material library. Here is the list of supported '
465
            'patterns:\n  ${_icuTimeOfDayToEnum.keys.join('\n  ')}'
466 467 468
          );
        }
        return _icuTimeOfDayToEnum[value];
469 470 471 472 473
      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 '
474
            'values:\n  ${_scriptCategoryToEnum.keys.join('\n  ')}'
475 476 477
          );
        }
        return _scriptCategoryToEnum[value];
478 479
    }
  }
480
  return  generateEncodedString(locale.languageCode, value);
481 482 483 484
}

/// Combines [generateType], [generateKey], and [generateValue] to return
/// the source of getters for the GlobalMaterialLocalizations subclass.
485
/// The locale is the locale for which the getter is being generated.
486
String generateGetter(String key, String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
487 488
  final String type = generateType(attributes);
  key = generateKey(key, attributes);
489
  final String? generatedValue = generateValue(value, attributes, locale);
490 491 492
      return '''

  @override
493
  $type get $key => $generatedValue;''';
494 495
}

496
void main(List<String> rawArgs) {
497 498
  checkCwdIsRepoRoot('gen_localizations');
  final GeneratorOptions options = parseArgs(rawArgs);
499 500 501 502 503

  // 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.

504
  final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
505
  final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
506
  final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
507

508
  try {
509
    validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
510
    validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb')));
511 512 513 514
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }

515 516 517 518 519 520 521 522 523 524
  // 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);
  }

525
  precacheLanguageAndRegionTags();
526

527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
  // 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,
  );
550

551
  try {
552 553
    validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
    validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
554 555 556
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
557

558 559 560 561 562
  if (options.removeUndefined) {
    removeUndefinedLocalizations(materialLocaleToResources);
    removeUndefinedLocalizations(cupertinoLocaleToResources);
  }

563
  final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly
564 565 566 567 568 569 570 571 572 573 574 575 576 577
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: materialLocaleToResources,
        localeToResourceAttributes: materialLocaleToResourceAttributes,
        generatedClassPrefix: 'MaterialLocalization',
        baseClass: 'GlobalMaterialLocalizations',
        generateHeader: generateMaterialHeader,
        generateConstructor: generateMaterialConstructor,
        factoryName: materialFactoryName,
        factoryDeclaration: materialFactoryDeclaration,
        factoryArguments: materialFactoryArguments,
        supportedLanguagesConstant: materialSupportedLanguagesConstant,
        supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
      )
      : null;
578
  final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly
579 580 581 582 583 584 585 586 587 588 589 590 591 592
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: cupertinoLocaleToResources,
        localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
        generatedClassPrefix: 'CupertinoLocalization',
        baseClass: 'GlobalCupertinoLocalizations',
        generateHeader: generateCupertinoHeader,
        generateConstructor: generateCupertinoConstructor,
        factoryName: cupertinoFactoryName,
        factoryDeclaration: cupertinoFactoryDeclaration,
        factoryArguments: cupertinoFactoryArguments,
        supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
        supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
      )
      : null;
593 594

  if (options.writeToFile) {
595
    final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
596
    materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
597
    final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart'));
598
    cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
599
  } else {
600 601 602 603 604 605
    if (!options.cupertinoOnly) {
      stdout.write(materialLocalizations);
    }
    if (!options.materialOnly) {
      stdout.write(cupertinoLocalizations);
    }
606
  }
607
}