gen_localizations.dart 25.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
  final StringBuffer output = StringBuffer();
82
  output.writeln(generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'));
83

84
  final StringBuffer supportedLocales = StringBuffer();
85

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

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

110 111 112 113 114
  // 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`).

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

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

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

136 137 138
    output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
    output.writeln(generateConstructor(languageLocale));

139
    final Map<String, String> languageResources = localeToResources[languageLocale];
140
    for (final String key in allKeys) {
141
      final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key] as Map<String, dynamic>;
142
      output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale));
143
    }
144 145
    output.writeln('}');
    int countryCodeCount = 0;
146 147 148 149 150
    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.
151
      for (final String scriptCode in languageToScriptCodes[languageName]) {
152
        final LocaleInfo scriptBaseLocale = LocaleInfo.fromString(languageName + '_' + scriptCode);
153 154 155
        output.writeln(generateClassDeclaration(
          scriptBaseLocale,
          generatedClassPrefix,
156
          '$generatedClassPrefix${languageLocale.camelCase()}',
157 158
        ));
        output.writeln(generateConstructor(scriptBaseLocale));
159
        final Map<String, String> scriptResources = localeToResources[scriptBaseLocale];
160
        for (final String key in scriptResources.keys.toList()..sort()) {
161 162
          if (languageResources[key] == scriptResources[key])
            continue;
163
          final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key] as Map<String, dynamic>;
164
          output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale));
165 166 167 168
        }
        output.writeln('}');

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

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

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

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

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

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

  return output.toString();
}

379 380 381 382 383 384 385
/// 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) {
386
    switch (attributes['x-flutter-type'] as String) {
387 388
      case 'icuShortTimePattern':
        return 'TimeOfDayFormat';
389 390
      case 'scriptCategory':
        return 'ScriptCategory';
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    }
  }
  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';
407
    switch (attributes['x-flutter-type'] as String) {
408 409 410 411
      case 'icuShortTimePattern':
        return '${key}Raw';
    }
  }
412 413 414 415
  if (key == 'datePickerDateOrder')
    return 'datePickerDateOrderString';
  if (key == 'datePickerDateTimeOrder')
    return 'datePickerDateTimeOrderString';
416 417 418 419 420 421 422 423 424 425 426 427 428 429
  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',
};

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

436 437 438 439 440 441 442 443
/// 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.
444
String generateValue(String value, Map<String, dynamic> attributes, LocaleInfo locale) {
445 446
  if (value == null)
    return null;
447
  // cupertino_en.arb doesn't use x-flutter-type.
448
  if (attributes != null) {
449
    switch (attributes['x-flutter-type'] as String) {
450 451
      case 'icuShortTimePattern':
        if (!_icuTimeOfDayToEnum.containsKey(value)) {
452
          throw Exception(
453 454 455 456 457 458
            '"$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];
459 460 461 462 463 464 465 466 467
      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];
468 469
    }
  }
470
  return  generateEncodedString(locale.languageCode, value);
471 472 473 474
}

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

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

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

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

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

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

505 506 507 508 509 510 511 512 513 514
  // 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);
  }

515
  precacheLanguageAndRegionTags();
516

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

541
  try {
542 543
    validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes);
    validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes);
544 545 546
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
547

548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577
  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;
578 579

  if (options.writeToFile) {
580 581 582 583
    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);
584
  } else {
585 586 587 588 589 590
    if (!options.cupertinoOnly) {
      stdout.write(materialLocalizations);
    }
    if (!options.materialOnly) {
      stdout.write(cupertinoLocalizations);
    }
591
  }
592
}