gen_localizations.dart 29.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
import '../gen_cupertino_localizations.dart';
import '../gen_material_localizations.dart';
56
import '../gen_widgets_localizations.dart';
57 58
import '../localizations_utils.dart';
import '../localizations_validator.dart';
59 60
import 'encode_kn_arb_files.dart';

61 62
/// This is the core of this script; it generates the code used for translations.
String generateArbBasedLocalizationSubclasses({
63 64 65 66 67 68
  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,
69
  ConstructorGenerator? generateConstructorForCountrySubClass,
70 71
  required String factoryName,
  required String factoryDeclaration,
72
  required bool callsFactoryWithConst,
73 74 75
  required String factoryArguments,
  required String supportedLanguagesConstant,
  required String supportedLanguagesDocMacro,
76 77 78
}) {
  assert(generatedClassPrefix.isNotEmpty);
  assert(baseClass.isNotEmpty);
79
  assert(factoryName.isNotEmpty);
80 81
  assert(factoryDeclaration.isNotEmpty);
  assert(factoryArguments.isNotEmpty);
82 83
  assert(supportedLanguagesConstant.isNotEmpty);
  assert(supportedLanguagesDocMacro.isNotEmpty);
84
  generateConstructorForCountrySubClass ??= generateConstructor;
85
  final StringBuffer output = StringBuffer();
86
  output.writeln(generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'));
87

88
  final StringBuffer supportedLocales = StringBuffer();
89

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

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

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

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

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

  final List<String> allKeys = allResourceIdentifiers.toList()..sort();
  final List<String> languageCodes = languageToLocales.keys.toList()..sort();
136
  final LocaleInfo canonicalLocale = LocaleInfo.fromString('en');
137
  for (final String languageName in languageCodes) {
138
    final LocaleInfo languageLocale = LocaleInfo.fromString(languageName);
139 140 141
    output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
    output.writeln(generateConstructor(languageLocale));

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

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

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

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

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

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

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

  return output.toString();
}

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

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

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

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

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

  @override
508
  $type get $key => $generatedValue;''';
509 510
}

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

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

519
  final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
520
  final RegExp widgetsFilenameRE = RegExp(r'widgets_(\w+)\.arb$');
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, 'widgets_en.arb')));
526
    validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
527
    validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb')));
528 529 530 531
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }

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

542
  precacheLanguageAndRegionTags();
543

544 545 546 547 548 549
  // Maps of locales to resource key/value pairs for Widgets ARBs.
  final Map<LocaleInfo, Map<String, String>> widgetsLocaleToResources = <LocaleInfo, Map<String, String>>{};
  // Maps of locales to resource key/attributes pairs for Widgets ARBs..
  // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
  final Map<LocaleInfo, Map<String, dynamic>> widgetsLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};

550 551 552 553 554
  // 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>>{};
555

556 557 558 559 560 561
  // 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>>{};

562 563 564 565 566 567
  loadMatchingArbsIntoBundleMaps(
    directory: directory,
    filenamePattern: widgetsFilenameRE,
    localeToResources: widgetsLocaleToResources,
    localeToResourceAttributes: widgetsLocaleToResourceAttributes,
  );
568 569 570 571 572 573 574 575 576 577 578 579
  loadMatchingArbsIntoBundleMaps(
    directory: directory,
    filenamePattern: materialFilenameRE,
    localeToResources: materialLocaleToResources,
    localeToResourceAttributes: materialLocaleToResourceAttributes,
  );
  loadMatchingArbsIntoBundleMaps(
    directory: directory,
    filenamePattern: cupertinoFilenameRE,
    localeToResources: cupertinoLocaleToResources,
    localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
  );
580

581
  try {
582
    validateLocalizations(widgetsLocaleToResources, widgetsLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
583 584
    validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
    validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
585 586 587
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
588
  if (options.removeUndefined) {
589
    removeUndefinedLocalizations(widgetsLocaleToResources);
590 591 592 593
    removeUndefinedLocalizations(materialLocaleToResources);
    removeUndefinedLocalizations(cupertinoLocaleToResources);
  }

594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610
  final String? widgetsLocalizations = options.writeToFile || !options.cupertinoOnly
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: widgetsLocaleToResources,
        localeToResourceAttributes: widgetsLocaleToResourceAttributes,
        generatedClassPrefix: 'WidgetsLocalization',
        baseClass: 'GlobalWidgetsLocalizations',
        generateHeader: generateWidgetsHeader,
        generateConstructor: generateWidgetsConstructor,
        generateConstructorForCountrySubClass: generateWidgetsConstructorForCountrySubclass,
        factoryName: widgetsFactoryName,
        factoryDeclaration: widgetsFactoryDeclaration,
        callsFactoryWithConst: true,
        factoryArguments: widgetsFactoryArguments,
        supportedLanguagesConstant: widgetsSupportedLanguagesConstant,
        supportedLanguagesDocMacro: widgetsSupportedLanguagesDocMacro,
      )
      : null;
611
  final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly
612 613 614 615 616 617 618 619 620
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: materialLocaleToResources,
        localeToResourceAttributes: materialLocaleToResourceAttributes,
        generatedClassPrefix: 'MaterialLocalization',
        baseClass: 'GlobalMaterialLocalizations',
        generateHeader: generateMaterialHeader,
        generateConstructor: generateMaterialConstructor,
        factoryName: materialFactoryName,
        factoryDeclaration: materialFactoryDeclaration,
621
        callsFactoryWithConst: false,
622 623 624 625 626
        factoryArguments: materialFactoryArguments,
        supportedLanguagesConstant: materialSupportedLanguagesConstant,
        supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
      )
      : null;
627
  final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly
628 629 630 631 632 633 634 635 636
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: cupertinoLocaleToResources,
        localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
        generatedClassPrefix: 'CupertinoLocalization',
        baseClass: 'GlobalCupertinoLocalizations',
        generateHeader: generateCupertinoHeader,
        generateConstructor: generateCupertinoConstructor,
        factoryName: cupertinoFactoryName,
        factoryDeclaration: cupertinoFactoryDeclaration,
637
        callsFactoryWithConst: false,
638 639 640 641 642
        factoryArguments: cupertinoFactoryArguments,
        supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
        supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
      )
      : null;
643 644

  if (options.writeToFile) {
645 646
    final File widgetsLocalizationsFile = File(path.join(directory.path, 'generated_widgets_localizations.dart'));
    widgetsLocalizationsFile.writeAsStringSync(widgetsLocalizations!, flush: true);
647
    final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
648
    materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
649
    final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart'));
650
    cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
651
  } else {
652 653 654 655 656 657 658 659
    if (options.cupertinoOnly) {
      stdout.write(cupertinoLocalizations);
    } else if (options.materialOnly) {
      stdout.write(materialLocalizations);
    } else if (options.widgetsOnly) {
      stdout.write(widgetsLocalizations);
    } else {
      stdout.write(widgetsLocalizations);
660 661 662
      stdout.write(materialLocalizations);
      stdout.write(cupertinoLocalizations);
    }
663
  }
664
}