gen_localizations.dart 26.3 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
          if (languageResources[key] == scriptResources[key]) {
167
            continue;
168
          }
169
          final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
170
          output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale));
171 172 173
        }
        output.writeln('}');

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

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

239
    } else if (countryCodeCount == 1) {
240
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)');
241
    } else {
242
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)');
243 244 245
    }
  }

246 247
  // Generate the factory function. Given a Locale it returns the corresponding
  // base class implementation.
248 249
  output.writeln('''

250 251
/// The set of supported languages, as language code strings.
///
252
/// The [$baseClass.delegate] can generate localizations for
253 254 255 256 257 258 259
/// 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:
///
260 261 262
///  * [$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')}
263 264
]);

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

  return output.toString();
}

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

/// 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.
428
String generateKey(String key, Map<String, dynamic>? attributes) {
429
  if (attributes != null) {
430
    if (attributes.containsKey('parameters')) {
431
      return '${key}Raw';
432
    }
433
    switch (attributes['x-flutter-type'] as String?) {
434 435 436 437
      case 'icuShortTimePattern':
        return '${key}Raw';
    }
  }
438
  if (key == 'datePickerDateOrder') {
439
    return 'datePickerDateOrderString';
440 441
  }
  if (key == 'datePickerDateTimeOrder') {
442
    return 'datePickerDateTimeOrderString';
443
  }
444 445 446 447 448 449 450 451 452 453 454 455 456 457
  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',
};

458 459 460 461 462 463
const Map<String, String> _scriptCategoryToEnum = <String, String>{
  'English-like': 'ScriptCategory.englishLike',
  'dense': 'ScriptCategory.dense',
  'tall': 'ScriptCategory.tall',
};

464 465 466 467 468 469 470 471
/// 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.
472
String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
473
  if (value == null) {
474
    return null;
475
  }
476
  // cupertino_en.arb doesn't use x-flutter-type.
477
  if (attributes != null) {
478
    switch (attributes['x-flutter-type'] as String?) {
479 480
      case 'icuShortTimePattern':
        if (!_icuTimeOfDayToEnum.containsKey(value)) {
481
          throw Exception(
482 483
            '"$value" is not one of the ICU short time patterns supported '
            'by the material library. Here is the list of supported '
484
            'patterns:\n  ${_icuTimeOfDayToEnum.keys.join('\n  ')}'
485 486 487
          );
        }
        return _icuTimeOfDayToEnum[value];
488 489 490 491 492
      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 '
493
            'values:\n  ${_scriptCategoryToEnum.keys.join('\n  ')}'
494 495 496
          );
        }
        return _scriptCategoryToEnum[value];
497 498
    }
  }
499
  return  generateEncodedString(locale.languageCode, value);
500 501 502 503
}

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

  @override
512
  $type get $key => $generatedValue;''';
513 514
}

515
void main(List<String> rawArgs) {
516 517
  checkCwdIsRepoRoot('gen_localizations');
  final GeneratorOptions options = parseArgs(rawArgs);
518 519 520 521 522

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

523
  final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
524
  final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
525
  final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
526

527
  try {
528
    validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
529
    validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb')));
530 531 532 533
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }

534 535 536 537 538 539 540 541 542 543
  // 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);
  }

544
  precacheLanguageAndRegionTags();
545

546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
  // 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,
  );
569

570
  try {
571 572
    validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
    validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
573 574 575
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
576

577 578 579 580 581
  if (options.removeUndefined) {
    removeUndefinedLocalizations(materialLocaleToResources);
    removeUndefinedLocalizations(cupertinoLocaleToResources);
  }

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

  if (options.writeToFile) {
614
    final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
615
    materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
616
    final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart'));
617
    cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
618
  } else {
619 620 621 622 623 624
    if (!options.cupertinoOnly) {
      stdout.write(materialLocalizations);
    }
    if (!options.materialOnly) {
      stdout.write(cupertinoLocalizations);
    }
625
  }
626
}