gen_localizations.dart 26 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7 8
// This program generates a getTranslation() function that looks up the
// translations contained by the arb files. The returned value is an
// instance of GlobalMaterialLocalizations that corresponds to a single
// locale.
9
//
10
// The *.arb files are in packages/flutter_localizations/lib/src/l10n.
11 12 13 14 15
//
// 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.
//
16 17 18 19
// The arb filenames are expected to have the form "material_(\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.
20 21 22 23
//
// This app is typically run by hand when a module's .arb files have been
// updated.
//
24 25 26 27 28 29 30 31 32 33
// ## 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:
//
// ```
// dart dev/tools/gen_localizations.dart
// ```
//
34
// If the data looks good, use the `-w` or `--overwrite` option to overwrite the
35 36 37 38 39
// packages/flutter_localizations/lib/src/l10n/localizations.dart file:
//
// ```
// dart dev/tools/gen_localizations.dart --overwrite
// ```
40

41 42
import 'dart:async';
import 'dart:convert';
43 44
import 'dart:io';

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

import 'localizations_utils.dart';
49 50
import 'localizations_validator.dart';

51 52 53 54 55
const String outputHeader = '''
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

56
// This file has been automatically generated. Please do not edit it manually.
57 58
// To regenerate the file, use:
// @(regenerate)
59 60 61 62 63 64 65 66

import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;

import '../material_localizations.dart';
67 68
''';

69
/// Maps locales to resource key/value pairs.
70
final Map<LocaleInfo, Map<String, String>> localeToResources = <LocaleInfo, Map<String, String>>{};
71

72
/// Maps locales to resource key/attributes pairs.
73
///
74
/// See also: <https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes>
75 76 77 78 79 80 81 82 83
final Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};

/// Set that holds the locales that were assumed from the existing locales.
///
/// For example, when the data lacks data for zh_Hant, we will use the data of
/// the first Hant Chinese locale as a default by repeating the data. If an
/// explicit match is later found, we can reference this set to see if we should
/// overwrite the existing assumed data.
final Set<LocaleInfo> assumedLocales = Set<LocaleInfo>();
84

85 86 87 88 89 90 91 92 93
/// Return `s` as a Dart-parseable raw string in single or double quotes.
///
/// Double quotes are expanded:
///
/// ```
/// foo => r'foo'
/// foo "bar" => r'foo "bar"'
/// foo 'bar' => r'foo ' "'" r'bar' "'"
/// ```
94
String generateString(String s) {
95 96
  if (!s.contains("'"))
    return "r'$s'";
97

98
  final StringBuffer output = StringBuffer();
99 100
  bool started = false; // Have we started writing a raw string.
  for (int i = 0; i < s.length; i++) {
101
    if (s[i] == "'") {
102
      if (started)
103 104
        output.write("'");
      output.write(' "\'" ');
105 106
      started = false;
    } else if (!started) {
107
      output.write("r'${s[i]}");
108 109 110 111 112 113
      started = true;
    } else {
      output.write(s[i]);
    }
  }
  if (started)
114
    output.write("'");
115 116 117
  return output.toString();
}

118
/// This is the core of this script; it generates the code used for translations.
119
String generateTranslationBundles() {
120 121
  final StringBuffer output = StringBuffer();
  final StringBuffer supportedLocales = StringBuffer();
122

123 124 125 126
  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>>{};
127
  final Set<String> allResourceIdentifiers = Set<String>();
128 129 130 131 132 133 134 135 136 137 138 139
  for (LocaleInfo locale in localeToResources.keys.toList()..sort()) {
    if (locale.scriptCode != null) {
      languageToScriptCodes[locale.languageCode] ??= Set<String>();
      languageToScriptCodes[locale.languageCode].add(locale.scriptCode);
    }
    if (locale.countryCode != null && locale.scriptCode != null) {
      final LocaleInfo key = LocaleInfo.fromString(locale.languageCode + '_' + locale.scriptCode);
      languageAndScriptToCountryCodes[key] ??= Set<String>();
      languageAndScriptToCountryCodes[key].add(locale.countryCode);
    }
    languageToLocales[locale.languageCode] ??= <LocaleInfo>[];
    languageToLocales[locale.languageCode].add(locale);
140 141 142
    allResourceIdentifiers.addAll(localeToResources[locale].keys);
  }

143
  output.writeln('''
144 145
// The classes defined here encode all of the translations found in the
// `flutter_localizations/lib/src/l10n/*.arb` files.
146
//
147 148 149 150 151 152 153 154
// These classes are constructed by the [getTranslation] method at the bottom of
// this file, and used by the [_MaterialLocalizationsDelegate.load] method defined
// in `flutter_localizations/lib/src/material_localizations.dart`.''');

  // We generate one class per supported language (e.g.
  // `MaterialLocalizationEn`). These implement everything that is needed by
  // GlobalMaterialLocalizations.

155 156 157 158 159
  // 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`).

160 161 162
  // 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.
163 164 165 166 167 168 169 170 171 172 173
  // `MaterialLocalizationEn`).

  // If scriptCodes for a language are defined, we expect a scriptCode to be
  // defined for locales that contain a countryCode. The superclass becomes
  // the script sublcass (e.g. `MaterialLocalizationZhHant`) and the generated
  // 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.
174 175 176

  final List<String> allKeys = allResourceIdentifiers.toList()..sort();
  final List<String> languageCodes = languageToLocales.keys.toList()..sort();
177
  final LocaleInfo canonicalLocale = LocaleInfo.fromString('en');
178
  for (String languageName in languageCodes) {
179 180 181
    final LocaleInfo languageLocale = LocaleInfo.fromString(languageName);
    writeClassHeader(output, languageLocale, 'GlobalMaterialLocalizations');
    final Map<String, String> languageResources = localeToResources[languageLocale];
182
    for (String key in allKeys) {
183
      final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key];
184
      output.writeln(generateGetter(key, languageResources[key], attributes));
185
    }
186 187
    output.writeln('}');
    int countryCodeCount = 0;
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
    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.
      for (String scriptCode in languageToScriptCodes[languageName]) {
        final LocaleInfo scriptBaseLocale = LocaleInfo.fromString(languageName + '_' + scriptCode);
        writeClassHeader(output, scriptBaseLocale, 'MaterialLocalization${camelCase(languageLocale)}');
        final Map<String, String> scriptResources = localeToResources[scriptBaseLocale];
        for (String key in scriptResources.keys) {
          if (languageResources[key] == scriptResources[key])
            continue;
          final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key];
          output.writeln(generateGetter(key, scriptResources[key], attributes));
        }
        output.writeln('}');

        final List<LocaleInfo> localeCodes = languageToLocales[languageName]..sort();
        for (LocaleInfo locale in localeCodes) {
          if (locale.originalString == languageName)
            continue;
          if (locale.originalString == languageName + '_' + scriptCode)
            continue;
          if (locale.scriptCode != scriptCode)
            continue;
          countryCodeCount += 1;
          writeClassHeader(output, locale, 'MaterialLocalization${camelCase(scriptBaseLocale)}');
          final Map<String, String> localeResources = localeToResources[locale];
          for (String key in localeResources.keys) {
            // 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;
            final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key];
            output.writeln(generateGetter(key, localeResources[key], attributes));
          }
         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();
      for (LocaleInfo locale in localeCodes) {
        if (locale.originalString == languageName)
232
          continue;
233 234 235 236 237 238 239 240 241 242
        countryCodeCount += 1;
        final Map<String, String> localeResources = localeToResources[locale];
        writeClassHeader(output, locale, 'MaterialLocalization${camelCase(languageLocale)}');
        for (String key in localeResources.keys) {
          if (languageResources[key] == localeResources[key])
            continue;
          final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key];
          output.writeln(generateGetter(key, localeResources[key], attributes));
        }
       output.writeln('}');
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 getTranslation function. Given a Locale it returns the
  // corresponding const GlobalMaterialLocalizations.
261 262
  output.writeln('''

263 264 265 266 267 268 269 270 271 272 273
/// The set of supported languages, as language code strings.
///
/// The [GlobalMaterialLocalizations.delegate] can generate localizations for
/// 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:
///
///  * [getTranslation], whose documentation describes these values.
274
final Set<String> kSupportedLanguages = HashSet<String>.from(const <String>[
275
${languageCodes.map<String>((String value) => "  '$value', // ${describeLocale(value)}").toList().join('\n')}
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
]);

/// Creates a [GlobalMaterialLocalizations] instance for the given `locale`.
///
/// All of the function's arguments except `locale` will be passed to the [new
/// GlobalMaterialLocalizations] constructor. (The `localeName` argument of that
/// constructor is specified by the actual subclass constructor by this
/// function.)
///
/// The following locales are supported by this package:
///
/// {@template flutter.localizations.languages}
$supportedLocales/// {@endtemplate}
///
/// Generally speaking, this method is only intended to be used by
/// [GlobalMaterialLocalizations.delegate].
GlobalMaterialLocalizations getTranslation(
  Locale locale,
  intl.DateFormat fullYearFormat,
  intl.DateFormat mediumDateFormat,
  intl.DateFormat longDateFormat,
  intl.DateFormat yearMonthFormat,
  intl.NumberFormat decimalFormat,
  intl.NumberFormat twoDigitZeroPaddedFormat,
) {
301
  switch (locale.languageCode) {''');
302
  const String arguments = 'fullYearFormat: fullYearFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat';
303
  for (String language in languageToLocales.keys) {
304
    // Only one instance of the language.
305 306
    if (languageToLocales[language].length == 1) {
      output.writeln('''
307
    case '$language':
308
      return MaterialLocalization${camelCase(languageToLocales[language][0])}($arguments);''');
309
    } else if (!languageToScriptCodes.containsKey(language)) { // Does not distinguish between scripts. Switch on countryCode directly.
310
      output.writeln('''
311 312
    case '$language': {
      switch (locale.countryCode) {''');
313 314
      for (LocaleInfo locale in languageToLocales[language]) {
        if (locale.originalString == language)
315
          continue;
316 317
        assert(locale.length > 1);
        final String countryCode = locale.countryCode;
318
        output.writeln('''
319
        case '$countryCode':
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
          return MaterialLocalization${camelCase(locale)}($arguments);''');
      }
      output.writeln('''
      }
      return MaterialLocalization${camelCase(LocaleInfo.fromString(language))}($arguments);
    }''');
    } else { // Language has scriptCode, add additional switch logic.
      bool hasCountryCode = false;
      output.writeln('''
    case '$language': {
      switch (locale.scriptCode) {''');
      for (String scriptCode in languageToScriptCodes[language]) {
        final LocaleInfo scriptLocale = LocaleInfo.fromString(language + '_' + scriptCode);
        output.writeln('''
        case '$scriptCode': {''');
        if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
          output.writeln('''
          switch (locale.countryCode) {''');
          for (LocaleInfo locale in languageToLocales[language]) {
            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':
              return MaterialLocalization${camelCase(locale)}($arguments);''');
          }
        }
        // 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('''
          return MaterialLocalization${camelCase(scriptLocale)}($arguments);
        }''');
        } else {
          // Not Explicitly defined, fallback to first locale with the same language and
          // script:
          for (LocaleInfo locale in languageToLocales[language]) {
            if (locale.scriptCode != scriptCode)
              continue;
            if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
              output.writeln('''
          }''');
            }
            output.writeln('''
          return MaterialLocalization${camelCase(scriptLocale)}($arguments);
        }''');
            break;
          }
        }
380 381
      }
      output.writeln('''
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
      }''');
      if (hasCountryCode) {
      output.writeln('''
      switch (locale.countryCode) {''');
        for (LocaleInfo locale in languageToLocales[language]) {
          if (locale.originalString == language)
            continue;
          assert(locale.length > 1);
          if (locale.countryCode == null)
            continue;
          final String countryCode = locale.countryCode;
          output.writeln('''
        case '$countryCode':
          return MaterialLocalization${camelCase(locale)}($arguments);''');
        }
        output.writeln('''
      }''');
399
      }
400 401
      output.writeln('''
      return MaterialLocalization${camelCase(LocaleInfo.fromString(language))}($arguments);
402 403 404 405
    }''');
    }
  }
  output.writeln('''
406
  }
407 408
  assert(false, 'getTranslation() called for unsupported locale "\$locale"');
  return null;
409
}''');
410 411 412 413

  return output.toString();
}

414 415 416 417 418 419 420 421 422 423 424
/// Writes the header of each class which corresponds to a locale.
void writeClassHeader(StringBuffer output, LocaleInfo locale, String superClass) {
  final String camelCaseName = camelCase(locale);
  final String className = 'MaterialLocalization$camelCaseName';
  final String constructor = generateConstructor(className, locale);
  output.writeln('');
  output.writeln('/// The translations for ${describeLocale(locale.originalString)} (`${locale.originalString}`).');
  output.writeln('class $className extends $superClass {');
  output.writeln(constructor);
}

425 426 427 428 429 430 431 432 433 434
/// 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) {
    switch (attributes['x-flutter-type']) {
      case 'icuShortTimePattern':
        return 'TimeOfDayFormat';
435 436
      case 'scriptCategory':
        return 'ScriptCategory';
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
    }
  }
  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';
    switch (attributes['x-flutter-type']) {
      case 'icuShortTimePattern':
        return '${key}Raw';
    }
  }
  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',
};

472 473 474 475 476 477
const Map<String, String> _scriptCategoryToEnum = <String, String>{
  'English-like': 'ScriptCategory.englishLike',
  'dense': 'ScriptCategory.dense',
  'tall': 'ScriptCategory.tall',
};

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
/// 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.
String generateValue(String value, Map<String, dynamic> attributes) {
  if (value == null)
    return null;
  if (attributes != null) {
    switch (attributes['x-flutter-type']) {
      case 'icuShortTimePattern':
        if (!_icuTimeOfDayToEnum.containsKey(value)) {
493
          throw Exception(
494 495 496 497 498 499
            '"$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];
500 501 502 503 504 505 506 507 508
      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];
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
    }
  }
  return generateString(value);
}

/// Combines [generateType], [generateKey], and [generateValue] to return
/// the source of getters for the GlobalMaterialLocalizations subclass.
String generateGetter(String key, String value, Map<String, dynamic> attributes) {
  final String type = generateType(attributes);
  key = generateKey(key, attributes);
  value = generateValue(value, attributes);
      return '''

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

/// Returns the source of the constructor for a GlobalMaterialLocalizations
/// subclass.
528 529
String generateConstructor(String className, LocaleInfo locale) {
  final String localeName = locale.originalString;
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
  return '''
  /// Create an instance of the translation bundle for ${describeLocale(localeName)}.
  ///
  /// For details on the meaning of the arguments, see [GlobalMaterialLocalizations].
  const $className({
    String localeName = '$localeName',
    @required intl.DateFormat fullYearFormat,
    @required intl.DateFormat mediumDateFormat,
    @required intl.DateFormat longDateFormat,
    @required intl.DateFormat yearMonthFormat,
    @required intl.NumberFormat decimalFormat,
    @required intl.NumberFormat twoDigitZeroPaddedFormat,
  }) : super(
    localeName: localeName,
    fullYearFormat: fullYearFormat,
    mediumDateFormat: mediumDateFormat,
    longDateFormat: longDateFormat,
    yearMonthFormat: yearMonthFormat,
    decimalFormat: decimalFormat,
    twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat,
  );''';
}

/// Parse the data for a locale from a file, and store it in the [attributes]
/// and [resources] keys.
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
void processBundle(File file, { @required String localeString }) {
  assert(localeString != null);
  // Helper method to fill the maps with the correct data from file.
  void populateResources(LocaleInfo locale) {
    final Map<String, String> resources = localeToResources[locale];
    final Map<String, dynamic> attributes = localeToResourceAttributes[locale];
    final Map<String, dynamic> bundle = json.decode(file.readAsStringSync());
    for (String key in bundle.keys) {
      // The ARB file resource "attributes" for foo are called @foo.
      if (key.startsWith('@'))
        attributes[key.substring(1)] = bundle[key];
      else
        resources[key] = bundle[key];
    }
  }
  // Only pre-assume scriptCode if there is a country or script code to assume off of.
  // When we assume scriptCode based on languageCode-only, we want this initial pass
  // to use the un-assumed version as a base class.
  LocaleInfo locale = LocaleInfo.fromString(localeString, assume: localeString.split('_').length > 1);
  // Allow overwrite if the existing data is assumed.
  if (assumedLocales.contains(locale)) {
    localeToResources[locale] = <String, String>{};
    localeToResourceAttributes[locale] = <String, dynamic>{};
    assumedLocales.remove(locale);
  } else {
    localeToResources[locale] ??= <String, String>{};
    localeToResourceAttributes[locale] ??= <String, dynamic>{};
  }
  populateResources(locale);
  // Add an assumed locale to default to when there is no info on scriptOnly locales.
  locale = LocaleInfo.fromString(localeString, assume: true);
  if (locale.scriptCode != null) {
    final LocaleInfo scriptLocale = LocaleInfo.fromString(locale.languageCode + '_' + locale.scriptCode);
    if (!localeToResources.containsKey(scriptLocale)) {
      assumedLocales.add(scriptLocale);
      localeToResources[scriptLocale] ??= <String, String>{};
      localeToResourceAttributes[scriptLocale] ??= <String, dynamic>{};
      populateResources(scriptLocale);
    }
594 595 596
  }
}

597
Future<void> main(List<String> rawArgs) async {
598 599
  checkCwdIsRepoRoot('gen_localizations');
  final GeneratorOptions options = parseArgs(rawArgs);
600 601 602 603 604

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

605 606
  final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
  final RegExp filenameRE = RegExp(r'material_(\w+)\.arb$');
607

608
  try {
609
    validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
610 611 612 613 614
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }

  await precacheLanguageAndRegionTags();
615

616
  for (FileSystemEntity entity in directory.listSync()) {
617 618
    final String entityPath = entity.path;
    if (FileSystemEntity.isFileSync(entityPath) && filenameRE.hasMatch(entityPath)) {
619
      processBundle(File(entityPath), localeString: filenameRE.firstMatch(entityPath)[1]);
620 621
    }
  }
622

623 624 625 626 627
  try {
    validateLocalizations(localeToResources, localeToResourceAttributes);
  } on ValidationError catch (exception) {
    exitWithError('$exception');
  }
628

629
  final StringBuffer buffer = StringBuffer();
630
  buffer.writeln(outputHeader.replaceFirst('@(regenerate)', 'dart dev/tools/gen_localizations.dart --overwrite'));
631
  buffer.write(generateTranslationBundles());
632 633

  if (options.writeToFile) {
634
    final File localizationsFile = File(path.join(directory.path, 'localizations.dart'));
635
    localizationsFile.writeAsStringSync(buffer.toString());
636
  } else {
637
    stdout.write(buffer.toString());
638
  }
639
}