gen_localizations.dart 25.6 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
import 'package:path/path.dart' as path;
46

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

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

79
  final StringBuffer output = StringBuffer();
80
  output.writeln(generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'));
81

82
  final StringBuffer supportedLocales = StringBuffer();
83

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

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

108 109 110 111 112
  // 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`).

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

  // If scriptCodes for a language are defined, we expect a scriptCode to be
  // defined for locales that contain a countryCode. The superclass becomes
120
  // the script subclass (e.g. `MaterialLocalizationZhHant`) and the generated
121 122 123 124 125 126
  // 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.
127 128 129

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

134 135 136
    output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
    output.writeln(generateConstructor(languageLocale));

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

166
        final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort();
167
        for (final LocaleInfo locale in localeCodes) {
168 169
          if (locale.originalString == languageName)
            continue;
170
          if (locale.originalString == '${languageName}_$scriptCode')
171 172 173 174
            continue;
          if (locale.scriptCode != scriptCode)
            continue;
          countryCodeCount += 1;
175 176 177
          output.writeln(generateClassDeclaration(
            locale,
            generatedClassPrefix,
178
            '$generatedClassPrefix${scriptBaseLocale.camelCase()}',
179 180
          ));
          output.writeln(generateConstructor(locale));
181
          final Map<String, String> localeResources = localeToResources[locale]!;
182
          for (final String key in localeResources.keys) {
183 184 185
            // 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;
186
            final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
187
            output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
188 189 190 191 192 193 194
          }
         output.writeln('}');
        }
      }
    } else {
      // No scriptCode. Here, we do not compare against script default (because it
      // doesn't exist).
195
      final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort();
196
      for (final LocaleInfo locale in localeCodes) {
197
        if (locale.originalString == languageName)
198
          continue;
199
        countryCodeCount += 1;
200
        final Map<String, String> localeResources = localeToResources[locale]!;
201 202 203
        output.writeln(generateClassDeclaration(
          locale,
          generatedClassPrefix,
204
          '$generatedClassPrefix${languageLocale.camelCase()}',
205 206
        ));
        output.writeln(generateConstructor(locale));
207
        for (final String key in localeResources.keys) {
208 209
          if (languageResources[key] == localeResources[key])
            continue;
210
          final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
211
          output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
212 213
        }
       output.writeln('}');
214
      }
215
    }
216

217
    final String scriptCodeMessage = scriptCodeCount == 0 ? '' : ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}';
218
    if (countryCodeCount == 0) {
219 220 221
      if (scriptCodeCount == 0)
        supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)}');
      else
222
        supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})');
223

224
    } else if (countryCodeCount == 1) {
225
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)');
226
    } else {
227
      supportedLocales.writeln('///  * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)');
228 229 230
    }
  }

231 232
  // Generate the factory function. Given a Locale it returns the corresponding
  // base class implementation.
233 234
  output.writeln('''

235 236
/// The set of supported languages, as language code strings.
///
237
/// The [$baseClass.delegate] can generate localizations for
238 239 240 241 242 243 244
/// 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:
///
245 246 247
///  * [$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')}
248 249
]);

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

  return output.toString();
}

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

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

433 434 435 436 437 438
const Map<String, String> _scriptCategoryToEnum = <String, String>{
  'English-like': 'ScriptCategory.englishLike',
  'dense': 'ScriptCategory.dense',
  'tall': 'ScriptCategory.tall',
};

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

/// Combines [generateType], [generateKey], and [generateValue] to return
/// the source of getters for the GlobalMaterialLocalizations subclass.
478
/// The locale is the locale for which the getter is being generated.
479
String generateGetter(String key, String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
480 481
  final String type = generateType(attributes);
  key = generateKey(key, attributes);
482
  final String? generatedValue = generateValue(value, attributes, locale);
483 484 485
      return '''

  @override
486
  $type get $key => $generatedValue;''';
487 488
}

489
void main(List<String> rawArgs) {
490 491
  checkCwdIsRepoRoot('gen_localizations');
  final GeneratorOptions options = parseArgs(rawArgs);
492 493 494 495 496

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

497
  final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
498
  final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
499
  final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
500

501
  try {
502
    validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
503
    validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb')));
504 505 506 507
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }

508 509 510 511 512 513 514 515 516 517
  // 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);
  }

518
  precacheLanguageAndRegionTags();
519

520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
  // 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,
  );
543

544
  try {
545 546
    validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes);
    validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes);
547 548 549
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
550

551
  final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly
552 553 554 555 556 557 558 559 560 561 562 563 564 565
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: materialLocaleToResources,
        localeToResourceAttributes: materialLocaleToResourceAttributes,
        generatedClassPrefix: 'MaterialLocalization',
        baseClass: 'GlobalMaterialLocalizations',
        generateHeader: generateMaterialHeader,
        generateConstructor: generateMaterialConstructor,
        factoryName: materialFactoryName,
        factoryDeclaration: materialFactoryDeclaration,
        factoryArguments: materialFactoryArguments,
        supportedLanguagesConstant: materialSupportedLanguagesConstant,
        supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
      )
      : null;
566
  final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly
567 568 569 570 571 572 573 574 575 576 577 578 579 580
      ? generateArbBasedLocalizationSubclasses(
        localeToResources: cupertinoLocaleToResources,
        localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
        generatedClassPrefix: 'CupertinoLocalization',
        baseClass: 'GlobalCupertinoLocalizations',
        generateHeader: generateCupertinoHeader,
        generateConstructor: generateCupertinoConstructor,
        factoryName: cupertinoFactoryName,
        factoryDeclaration: cupertinoFactoryDeclaration,
        factoryArguments: cupertinoFactoryArguments,
        supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
        supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
      )
      : null;
581 582

  if (options.writeToFile) {
583
    final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
584
    materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
585
    final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart'));
586
    cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
587
  } else {
588 589 590 591 592 593
    if (!options.cupertinoOnly) {
      stdout.write(materialLocalizations);
    }
    if (!options.materialOnly) {
      stdout.write(cupertinoLocalizations);
    }
594
  }
595
}