gen_localizations.dart 26.5 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
// If the data looks good, use the `-w` or `--overwrite` option to overwrite the
36 37
// packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart
// and packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart file:
38 39
//
// ```
40
// dart dev/tools/localization/bin/gen_localizations.dart --overwrite
41
// ```
42 43 44

import 'dart:io';

45 46
import 'package:path/path.dart' as path;
import 'package:meta/meta.dart';
47

48 49 50 51
import '../gen_cupertino_localizations.dart';
import '../gen_material_localizations.dart';
import '../localizations_utils.dart';
import '../localizations_validator.dart';
52

53 54
import 'encode_kn_arb_files.dart';

55 56 57 58 59 60 61 62
/// This is the core of this script; it generates the code used for translations.
String generateArbBasedLocalizationSubclasses({
  @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,
63
  @required String factoryName,
64 65
  @required String factoryDeclaration,
  @required String factoryArguments,
66 67
  @required String supportedLanguagesConstant,
  @required String supportedLanguagesDocMacro,
68 69 70 71 72 73 74
}) {
  assert(localeToResources != null);
  assert(localeToResourceAttributes != null);
  assert(generatedClassPrefix.isNotEmpty);
  assert(baseClass.isNotEmpty);
  assert(generateHeader != null);
  assert(generateConstructor != null);
75
  assert(factoryName.isNotEmpty);
76 77
  assert(factoryDeclaration.isNotEmpty);
  assert(factoryArguments.isNotEmpty);
78 79
  assert(supportedLanguagesConstant.isNotEmpty);
  assert(supportedLanguagesDocMacro.isNotEmpty);
80

81 82 83 84 85
  // See https://github.com/flutter/flutter/issues/53036 for context on why
  // 'no' is being used as a synonym for 'nb'. It only uses this synonym
  // if 'nb' is not detected as a valid arb file.
  bool isNbSynonymOfNo = false;

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 100 101 102
      languageToScriptCodes[locale.languageCode].add(locale.scriptCode);
    }
    if (locale.countryCode != null && locale.scriptCode != null) {
      final LocaleInfo key = LocaleInfo.fromString(locale.languageCode + '_' + locale.scriptCode);
103
      languageAndScriptToCountryCodes[key] ??= <String>{};
104 105 106 107
      languageAndScriptToCountryCodes[key].add(locale.countryCode);
    }
    languageToLocales[locale.languageCode] ??= <LocaleInfo>[];
    languageToLocales[locale.languageCode].add(locale);
108
    allResourceIdentifiers.addAll(localeToResources[locale].keys.toList()..sort());
109 110
  }

111 112 113 114 115 116
  if (languageToLocales['no'] != null && languageToLocales['nb'] == null) {
    languageToLocales['nb'] ??= <LocaleInfo>[];
    languageToLocales['nb'].add(LocaleInfo.fromString('nb'));
    isNbSynonymOfNo = true;
  }

117
  // We generate one class per supported language (e.g.
118 119
  // `MaterialLocalizationEn`). These implement everything that is needed by the
  // superclass (e.g. GlobalMaterialLocalizations).
120

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

126 127 128
  // 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.
129 130 131 132
  // `MaterialLocalizationEn`).

  // If scriptCodes for a language are defined, we expect a scriptCode to be
  // defined for locales that contain a countryCode. The superclass becomes
133
  // the script subclass (e.g. `MaterialLocalizationZhHant`) and the generated
134 135 136 137 138 139
  // 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.
140 141 142

  final List<String> allKeys = allResourceIdentifiers.toList()..sort();
  final List<String> languageCodes = languageToLocales.keys.toList()..sort();
143
  final LocaleInfo canonicalLocale = LocaleInfo.fromString('en');
144
  for (final String languageName in languageCodes) {
145
    final LocaleInfo languageLocale = LocaleInfo.fromString(languageName);
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161

    // See https://github.com/flutter/flutter/issues/53036 for context on why
    // 'no' is being used as a synonym for 'nb'. It only uses this synonym
    // if 'nb' is not detected as a valid arb file.
    if (languageName == 'nb' && isNbSynonymOfNo) {
      output.writeln(generateClassDeclaration(
        languageLocale,
        generatedClassPrefix,
        '${generatedClassPrefix}No'),
      );
      output.writeln(generateConstructor(languageLocale));
      output.writeln('}');
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)}, which, in this library, is a synonym of `no`');
      continue;
    }

162 163 164
    output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
    output.writeln(generateConstructor(languageLocale));

165
    final Map<String, String> languageResources = localeToResources[languageLocale];
166
    for (final String key in allKeys) {
167
      final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key] as Map<String, dynamic>;
168
      output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale));
169
    }
170 171
    output.writeln('}');
    int countryCodeCount = 0;
172 173 174 175 176
    int scriptCodeCount = 0;
    if (languageToScriptCodes.containsKey(languageName)) {
      scriptCodeCount = languageToScriptCodes[languageName].length;
      // Language has scriptCodes, so we need to properly fallback countries to corresponding
      // script default values before language default values.
177
      for (final String scriptCode in languageToScriptCodes[languageName]) {
178
        final LocaleInfo scriptBaseLocale = LocaleInfo.fromString(languageName + '_' + scriptCode);
179 180 181
        output.writeln(generateClassDeclaration(
          scriptBaseLocale,
          generatedClassPrefix,
182
          '$generatedClassPrefix${languageLocale.camelCase()}',
183 184
        ));
        output.writeln(generateConstructor(scriptBaseLocale));
185
        final Map<String, String> scriptResources = localeToResources[scriptBaseLocale];
186
        for (final String key in scriptResources.keys.toList()..sort()) {
187 188
          if (languageResources[key] == scriptResources[key])
            continue;
189
          final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key] as Map<String, dynamic>;
190
          output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale));
191 192 193 194
        }
        output.writeln('}');

        final List<LocaleInfo> localeCodes = languageToLocales[languageName]..sort();
195
        for (final LocaleInfo locale in localeCodes) {
196 197 198 199 200 201 202
          if (locale.originalString == languageName)
            continue;
          if (locale.originalString == languageName + '_' + scriptCode)
            continue;
          if (locale.scriptCode != scriptCode)
            continue;
          countryCodeCount += 1;
203 204 205
          output.writeln(generateClassDeclaration(
            locale,
            generatedClassPrefix,
206
            '$generatedClassPrefix${scriptBaseLocale.camelCase()}',
207 208
          ));
          output.writeln(generateConstructor(locale));
209
          final Map<String, String> localeResources = localeToResources[locale];
210
          for (final String key in localeResources.keys) {
211 212 213
            // 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;
214
            final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key] as Map<String, dynamic>;
215
            output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
216 217 218 219 220 221 222 223
          }
         output.writeln('}');
        }
      }
    } else {
      // No scriptCode. Here, we do not compare against script default (because it
      // doesn't exist).
      final List<LocaleInfo> localeCodes = languageToLocales[languageName]..sort();
224
      for (final LocaleInfo locale in localeCodes) {
225
        if (locale.originalString == languageName)
226
          continue;
227 228
        countryCodeCount += 1;
        final Map<String, String> localeResources = localeToResources[locale];
229 230 231
        output.writeln(generateClassDeclaration(
          locale,
          generatedClassPrefix,
232
          '$generatedClassPrefix${languageLocale.camelCase()}',
233 234
        ));
        output.writeln(generateConstructor(locale));
235
        for (final String key in localeResources.keys) {
236 237
          if (languageResources[key] == localeResources[key])
            continue;
238
          final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key] as Map<String, dynamic>;
239
          output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
240 241
        }
       output.writeln('}');
242
      }
243
    }
244

245
    final String scriptCodeMessage = scriptCodeCount == 0 ? '' : ' and $scriptCodeCount script' + (scriptCodeCount == 1 ? '' : 's');
246
    if (countryCodeCount == 0) {
247 248 249 250 251
      if (scriptCodeCount == 0)
        supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)}');
      else
        supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script' + (scriptCodeCount == 1 ? '' : 's') + ')');

252
    } else if (countryCodeCount == 1) {
253
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)');
254
    } else {
255
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)');
256 257 258
    }
  }

259 260
  // Generate the factory function. Given a Locale it returns the corresponding
  // base class implementation.
261 262
  output.writeln('''

263 264
/// The set of supported languages, as language code strings.
///
265
/// The [$baseClass.delegate] can generate localizations for
266 267 268 269 270 271 272
/// 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:
///
273 274 275
///  * [$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')}
276 277
]);

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

  return output.toString();
}

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

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

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

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

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

  @override
  $type get $key => $value;''';
}

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

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

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

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

531 532 533 534 535 536 537 538 539 540
  // 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);
  }

541
  precacheLanguageAndRegionTags();
542

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

567
  try {
568 569
    validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes);
    validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes);
570 571 572
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
573

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

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