gen_l10n.dart 45.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
// @dart = 2.8

7
import 'package:meta/meta.dart';
8 9 10 11

import '../base/file_system.dart';
import '../base/logger.dart';
import '../convert.dart';
12
import '../flutter_manifest.dart';
13
import '../globals.dart' as globals;
14

15 16
import 'gen_l10n_templates.dart';
import 'gen_l10n_types.dart';
17 18
import 'localizations_utils.dart';

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
/// Run the localizations generation script with the configuration [options].
void generateLocalizations({
  @required Directory projectDir,
  @required Directory dependenciesDir,
  @required LocalizationOptions options,
  @required LocalizationsGenerator localizationsGenerator,
  @required Logger logger,
}) {
  // If generating a synthetic package, generate a warning if
  // flutter: generate is not set.
  final FlutterManifest flutterManifest = FlutterManifest.createFromPath(
    projectDir.childFile('pubspec.yaml').path,
    fileSystem: projectDir.fileSystem,
    logger: logger,
  );
  if (options.useSyntheticPackage && !flutterManifest.generateSyntheticPackage) {
    logger.printError(
      'Attempted to generate localizations code without having '
      'the flutter: generate flag turned on.'
      '\n'
      'Check pubspec.yaml and ensure that flutter: generate: true has '
      'been added and rebuild the project. Otherwise, the localizations '
      'source code will not be importable.'
    );
    throw Exception();
  }

  precacheLanguageAndRegionTags();

  final String inputPathString = options?.arbDirectory?.path ?? globals.fs.path.join('lib', 'l10n');
  final String templateArbFileName = options?.templateArbFile?.toFilePath() ?? 'app_en.arb';
  final String outputFileString = options?.outputLocalizationsFile?.toFilePath() ?? 'app_localizations.dart';

  try {
    localizationsGenerator
      ..initialize(
        inputsAndOutputsListPath: dependenciesDir?.path,
        projectPathString: projectDir.path,
        inputPathString: inputPathString,
        templateArbFileName: templateArbFileName,
        outputFileString: outputFileString,
        outputPathString: options?.outputDirectory?.path,
        classNameString: options.outputClass ?? 'AppLocalizations',
        preferredSupportedLocales: options.preferredSupportedLocales,
        headerString: options.header,
        headerFile: options?.headerFile?.toFilePath(),
        useDeferredLoading: options.deferredLoading ?? false,
        useSyntheticPackage: options.useSyntheticPackage ?? true,
        areResourceAttributesRequired: options.areResourceAttributesRequired ?? false,
        untranslatedMessagesFile: options?.untranslatedMessagesFile?.toFilePath(),
      )
      ..loadResources()
      ..writeOutputFiles(logger, isFromYaml: true);
  } on L10nException catch (e) {
    logger.printError(e.message);
    throw Exception();
  }
}

78 79 80 81
/// The path for the synthetic package.
final String defaultSyntheticPackagePath = globals.fs.path.join('.dart_tool', 'flutter_gen');

/// The default path used when the `_useSyntheticPackage` setting is set to true
82 83 84 85
/// in [LocalizationsGenerator].
///
/// See [LocalizationsGenerator.initialize] for where and how it is used by the
/// localizations tool.
86
final String syntheticL10nPackagePath = globals.fs.path.join(defaultSyntheticPackagePath, 'gen_l10n');
87

88 89 90
List<String> generateMethodParameters(Message message) {
  assert(message.placeholders.isNotEmpty);
  final Placeholder countPlaceholder = message.isPlural ? message.getCountPlaceholder() : null;
91
  return message.placeholders.map((Placeholder placeholder) {
92 93
    final String type = placeholder == countPlaceholder ? 'int' : placeholder.type;
    return '$type ${placeholder.name}';
94 95 96
  }).toList();
}

97
String generateDateFormattingLogic(Message message) {
98
  if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
99
    return '@(none)';
100
  }
101

102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
  final Iterable<String> formatStatements = message.placeholders
    .where((Placeholder placeholder) => placeholder.isDate)
    .map((Placeholder placeholder) {
      if (placeholder.format == null) {
        throw L10nException(
          'The placeholder, ${placeholder.name}, has its "type" resource attribute set to '
          'the "${placeholder.type}" type. To properly resolve for the right '
          '${placeholder.type} format, the "format" attribute needs to be set '
          'to determine which DateFormat to use. \n'
          'Check the intl library\'s DateFormat class constructors for allowed '
          'date formats.'
        );
      }
      if (!placeholder.hasValidDateFormat) {
        throw L10nException(
          'Date format "${placeholder.format}" for placeholder '
          '${placeholder.name} does not have a corresponding DateFormat '
          'constructor\n. Check the intl library\'s DateFormat class '
          'constructors for allowed date formats.'
        );
      }
      return dateFormatTemplate
        .replaceAll('@(placeholder)', placeholder.name)
        .replaceAll('@(format)', placeholder.format);
    });
127

128
  return formatStatements.isEmpty ? '@(none)' : formatStatements.join('');
129 130
}

131 132 133
String generateNumberFormattingLogic(Message message) {
  if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
    return '@(none)';
134
  }
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
  final Iterable<String> formatStatements = message.placeholders
    .where((Placeholder placeholder) => placeholder.isNumber)
    .map((Placeholder placeholder) {
      if (!placeholder.hasValidNumberFormat) {
        throw L10nException(
          'Number format ${placeholder.format} for the ${placeholder.name} '
          'placeholder does not have a corresponding NumberFormat constructor.\n'
          'Check the intl library\'s NumberFormat class constructors for allowed '
          'number formats.'
        );
      }
      final Iterable<String> parameters =
        placeholder.optionalParameters.map<String>((OptionalParameter parameter) {
          return '${parameter.name}: ${parameter.value}';
        },
      );
      return numberFormatTemplate
        .replaceAll('@(placeholder)', placeholder.name)
        .replaceAll('@(format)', placeholder.format)
        .replaceAll('@(parameters)', parameters.join(',    \n'));
    });
157

158
  return formatStatements.isEmpty ? '@(none)' : formatStatements.join('');
159 160
}

161
String generatePluralMethod(Message message, AppResourceBundle bundle) {
162
  if (message.placeholders.isEmpty) {
163
    throw L10nException(
164
      'Unable to find placeholders for the plural message: ${message.resourceId}.\n'
165 166 167
      'Check to see if the plural message is in the proper ICU syntax format '
      'and ensure that placeholders are properly specified.'
    );
168
  }
169 170 171

  // To make it easier to parse the plurals message, temporarily replace each
  // "{placeholder}" parameter with "#placeholder#".
172
  String easyMessage = bundle.translationFor(message);
173
  for (final Placeholder placeholder in message.placeholders) {
174
    easyMessage = easyMessage.replaceAll('{${placeholder.name}}', '#${placeholder.name}#');
175
  }
176

177 178 179 180 181 182 183 184 185
  final Placeholder countPlaceholder = message.getCountPlaceholder();
  if (countPlaceholder == null) {
    throw L10nException(
      'Unable to find the count placeholder for the plural message: ${message.resourceId}.\n'
      'Check to see if the plural message is in the proper ICU syntax format '
      'and ensure that placeholders are properly specified.'
    );
  }

186 187 188 189 190 191 192 193
  const Map<String, String> pluralIds = <String, String>{
    '=0': 'zero',
    '=1': 'one',
    '=2': 'two',
    'few': 'few',
    'many': 'many',
    'other': 'other'
  };
194

195
  final List<String> pluralLogicArgs = <String>[];
196
  for (final String pluralKey in pluralIds.keys) {
197
    final RegExp expRE = RegExp('($pluralKey)\\s*{([^}]+)}');
198
    final RegExpMatch match = expRE.firstMatch(easyMessage);
199
    if (match != null && match.groupCount == 2) {
200
      String argValue = generateString(match.group(2));
201
      for (final Placeholder placeholder in message.placeholders) {
202
        if (placeholder != countPlaceholder && placeholder.requiresFormatting) {
203
          argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}');
204
        } else {
205
          argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}');
206 207
        }
      }
208
      pluralLogicArgs.add('      ${pluralIds[pluralKey]}: $argValue');
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
    }
  }

  final List<String> parameters = message.placeholders.map((Placeholder placeholder) {
    final String placeholderType = placeholder == countPlaceholder ? 'int' : placeholder.type;
    return '$placeholderType ${placeholder.name}';
  }).toList();

  final String comment = message.description ?? 'No description provided in @${message.resourceId}';

  return pluralMethodTemplate
    .replaceAll('@(comment)', comment)
    .replaceAll('@(name)', message.resourceId)
    .replaceAll('@(parameters)', parameters.join(', '))
    .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
    .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
    .replaceAll('@(count)', countPlaceholder.name)
    .replaceAll('@(pluralLogicArgs)', pluralLogicArgs.join(',\n'))
    .replaceAll('@(none)\n', '');
}

String generateMethod(Message message, AppResourceBundle bundle) {
  String generateMessage() {
232
    String messageValue = generateString(bundle.translationFor(message));
233 234 235 236 237 238
    for (final Placeholder placeholder in message.placeholders) {
      if (placeholder.requiresFormatting) {
        messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}String}');
      } else {
        messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}}');
      }
239
    }
240

241
    return messageValue;
242 243
  }

244
  if (message.isPlural) {
245
    return generatePluralMethod(message, bundle);
246 247
  }

248
  if (message.placeholdersRequireFormatting) {
249 250 251
    return formatMethodTemplate
      .replaceAll('@(name)', message.resourceId)
      .replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
252 253
      .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
      .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
254 255
      .replaceAll('@(message)', generateMessage())
      .replaceAll('@(none)\n', '');
256
  }
257

258 259 260 261 262 263 264 265 266 267 268 269
  if (message.placeholders.isNotEmpty) {
    return methodTemplate
      .replaceAll('@(name)', message.resourceId)
      .replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
      .replaceAll('@(message)', generateMessage());
  }

  return getterTemplate
    .replaceAll('@(name)', message.resourceId)
    .replaceAll('@(message)', generateMessage());
}

270 271 272 273 274 275
String generateBaseClassMethod(Message message, LocaleInfo templateArbLocale) {
  final String comment = message.description ?? 'No description provided for @${message.resourceId}.';
  final String templateLocaleTranslationComment = '''
  /// In $templateArbLocale, this message translates to:
  /// **${generateString(message.value)}**''';

276 277 278
  if (message.placeholders.isNotEmpty) {
    return baseClassMethodTemplate
      .replaceAll('@(comment)', comment)
279
      .replaceAll('@(templateLocaleTranslationComment)', templateLocaleTranslationComment)
280 281 282 283 284
      .replaceAll('@(name)', message.resourceId)
      .replaceAll('@(parameters)', generateMethodParameters(message).join(', '));
  }
  return baseClassGetterTemplate
    .replaceAll('@(comment)', comment)
285
    .replaceAll('@(templateLocaleTranslationComment)', templateLocaleTranslationComment)
286 287 288
    .replaceAll('@(name)', message.resourceId);
}

289 290 291 292
String _generateLookupByAllCodes(
  AppResourceBundleCollection allBundles,
  String Function(LocaleInfo) generateSwitchClauseTemplate,
) {
293 294 295 296 297 298 299 300 301
  final Iterable<LocaleInfo> localesWithAllCodes = allBundles.locales.where((LocaleInfo locale) {
    return locale.scriptCode != null && locale.countryCode != null;
  });

  if (localesWithAllCodes.isEmpty) {
    return '';
  }

  final Iterable<String> switchClauses = localesWithAllCodes.map<String>((LocaleInfo locale) {
302 303
    return generateSwitchClauseTemplate(locale)
      .replaceAll('@(case)', locale.toString());
304 305 306 307 308 309 310 311
  });

  return allCodesLookupTemplate.replaceAll(
    '@(allCodesSwitchClauses)',
    switchClauses.join('\n    '),
  );
}

312 313 314 315
String _generateLookupByScriptCode(
  AppResourceBundleCollection allBundles,
  String Function(LocaleInfo) generateSwitchClauseTemplate,
) {
316 317
  final Iterable<String> switchClauses = allBundles.languages.map((String language) {
    final Iterable<LocaleInfo> locales = allBundles.localesForLanguage(language);
318 319 320 321
    final Iterable<LocaleInfo> localesWithScriptCodes = locales.where((LocaleInfo locale) {
      return locale.scriptCode != null && locale.countryCode == null;
    });

322
    if (localesWithScriptCodes.isEmpty) {
323
      return null;
324
    }
325

326
    return nestedSwitchTemplate
327
      .replaceAll('@(languageCode)', language)
328 329
      .replaceAll('@(code)', 'scriptCode')
      .replaceAll('@(switchClauses)', localesWithScriptCodes.map((LocaleInfo locale) {
330 331
          return generateSwitchClauseTemplate(locale)
            .replaceAll('@(case)', locale.scriptCode);
332 333 334 335 336 337 338 339 340 341 342 343 344
        }).join('\n        '));
  }).where((String switchClause) => switchClause != null);

  if (switchClauses.isEmpty) {
    return '';
  }

  return languageCodeSwitchTemplate
    .replaceAll('@(comment)', '// Lookup logic when language+script codes are specified.')
    .replaceAll('@(switchClauses)', switchClauses.join('\n    '),
  );
}

345 346 347 348
String _generateLookupByCountryCode(
  AppResourceBundleCollection allBundles,
  String Function(LocaleInfo) generateSwitchClauseTemplate,
) {
349 350 351 352 353 354
  final Iterable<String> switchClauses = allBundles.languages.map((String language) {
    final Iterable<LocaleInfo> locales = allBundles.localesForLanguage(language);
    final Iterable<LocaleInfo> localesWithCountryCodes = locales.where((LocaleInfo locale) {
      return locale.countryCode != null && locale.scriptCode == null;
    });

355
    if (localesWithCountryCodes.isEmpty) {
356
      return null;
357
    }
358 359 360 361

    return nestedSwitchTemplate
      .replaceAll('@(languageCode)', language)
      .replaceAll('@(code)', 'countryCode')
362
      .replaceAll('@(switchClauses)', localesWithCountryCodes.map((LocaleInfo locale) {
363 364
          return generateSwitchClauseTemplate(locale)
            .replaceAll('@(case)', locale.countryCode);
365
        }).join('\n        '));
366 367 368 369 370 371 372 373 374 375 376
  }).where((String switchClause) => switchClause != null);

  if (switchClauses.isEmpty) {
    return '';
  }

  return languageCodeSwitchTemplate
    .replaceAll('@(comment)', '// Lookup logic when language+country codes are specified.')
    .replaceAll('@(switchClauses)', switchClauses.join('\n    '));
}

377 378 379 380
String _generateLookupByLanguageCode(
  AppResourceBundleCollection allBundles,
  String Function(LocaleInfo) generateSwitchClauseTemplate,
) {
381 382 383 384 385 386
  final Iterable<String> switchClauses = allBundles.languages.map((String language) {
    final Iterable<LocaleInfo> locales = allBundles.localesForLanguage(language);
    final Iterable<LocaleInfo> localesWithLanguageCode = locales.where((LocaleInfo locale) {
      return locale.countryCode == null && locale.scriptCode == null;
    });

387
    if (localesWithLanguageCode.isEmpty) {
388
      return null;
389
    }
390 391

    return localesWithLanguageCode.map((LocaleInfo locale) {
392 393
      return generateSwitchClauseTemplate(locale)
        .replaceAll('@(case)', locale.languageCode);
394 395 396 397 398 399 400 401 402 403 404 405
    }).join('\n        ');
  }).where((String switchClause) => switchClause != null);

  if (switchClauses.isEmpty) {
    return '';
  }

  return languageCodeSwitchTemplate
    .replaceAll('@(comment)', '// Lookup logic when only language code is specified.')
    .replaceAll('@(switchClauses)', switchClauses.join('\n    '));
}

406 407 408 409 410 411 412 413 414 415 416 417 418
String _generateLookupBody(
  AppResourceBundleCollection allBundles,
  String className,
  bool useDeferredLoading,
  String fileName,
) {
  final String Function(LocaleInfo) generateSwitchClauseTemplate = (LocaleInfo locale) {
    return (useDeferredLoading ?
      switchClauseDeferredLoadingTemplate : switchClauseTemplate)
      .replaceAll('@(localeClass)', '$className${locale.camelCase()}')
      .replaceAll('@(appClass)', className)
      .replaceAll('@(library)', '${fileName}_${locale.languageCode}');
  };
419
  return lookupBodyTemplate
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 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
    .replaceAll('@(lookupAllCodesSpecified)', _generateLookupByAllCodes(
      allBundles,
      generateSwitchClauseTemplate,
    ))
    .replaceAll('@(lookupScriptCodeSpecified)', _generateLookupByScriptCode(
      allBundles,
      generateSwitchClauseTemplate,
    ))
    .replaceAll('@(lookupCountryCodeSpecified)', _generateLookupByCountryCode(
      allBundles,
      generateSwitchClauseTemplate,
    ))
    .replaceAll('@(lookupLanguageCodeSpecified)', _generateLookupByLanguageCode(
      allBundles,
      generateSwitchClauseTemplate,
    ));
}

String _generateDelegateClass({
  AppResourceBundleCollection allBundles,
  String className,
  Set<String> supportedLanguageCodes,
  bool useDeferredLoading,
  String fileName,
}) {

  final String lookupBody = _generateLookupBody(
    allBundles,
    className,
    useDeferredLoading,
    fileName,
  );
  final String loadBody = (
    useDeferredLoading ? loadBodyDeferredLoadingTemplate : loadBodyTemplate
  )
    .replaceAll('@(class)', className)
    .replaceAll('@(lookupName)', '_lookup$className');
  final String lookupFunction = (useDeferredLoading ?
  lookupFunctionDeferredLoadingTemplate : lookupFunctionTemplate)
    .replaceAll('@(class)', className)
    .replaceAll('@(lookupName)', '_lookup$className')
    .replaceAll('@(lookupBody)', lookupBody);
  return delegateClassTemplate
    .replaceAll('@(class)', className)
    .replaceAll('@(loadBody)', loadBody)
    .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', '))
    .replaceAll('@(lookupFunction)', lookupFunction);
467 468
}

469 470 471 472 473
class LocalizationsGenerator {
  /// Creates an instance of the localizations generator class.
  ///
  /// It takes in a [FileSystem] representation that the class will act upon.
  LocalizationsGenerator(this._fs);
474

475
  final FileSystem _fs;
476 477
  Iterable<Message> _allMessages;
  AppResourceBundleCollection _allBundles;
478
  LocaleInfo _templateArbLocale;
479
  bool _useSyntheticPackage = true;
480

481 482
  /// The directory that contains the project's arb files, as well as the
  /// header file, if specified.
483 484
  ///
  /// It is assumed that all input files (e.g. [templateArbFile], arb files
485 486 487 488 489
  /// for translated messages, header file templates) will reside here.
  ///
  /// This directory is specified with the [initialize] method.
  Directory inputDirectory;

490 491 492 493 494
  /// The Flutter project's root directory.
  ///
  /// This directory is specified with the [initialize] method.
  Directory projectDirectory;

495 496 497
  /// The directory to generate the project's localizations files in.
  ///
  /// It is assumed that all output files (e.g. The localizations
498 499 500 501
  /// [outputFile], `messages_<locale>.dart` and `messages_all.dart`)
  /// will reside here.
  ///
  /// This directory is specified with the [initialize] method.
502
  Directory outputDirectory;
503

504 505 506 507 508
  /// The input arb file which defines all of the messages that will be
  /// exported by the generated class that's written to [outputFile].
  ///
  /// This file is specified with the [initialize] method.
  File templateArbFile;
509

510 511 512 513
  /// The file to write the generated abstract localizations and
  /// localizations delegate classes to. Separate localizations
  /// files will also be generated for each language using this
  /// filename as a prefix and the locale as the suffix.
514 515
  ///
  /// This file is specified with the [initialize] method.
516
  File baseOutputFile;
517 518 519 520 521 522 523 524 525

  /// The class name to be used for the localizations class in [outputFile].
  ///
  /// For example, if 'AppLocalizations' is passed in, a class named
  /// AppLocalizations will be used for localized message lookups.
  ///
  /// The class name is specified with the [initialize] method.
  String get className => _className;
  String _className;
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540

  /// The list of preferred supported locales.
  ///
  /// By default, the list of supported locales in the localizations class
  /// will be sorted in alphabetical order. However, this option
  /// allows for a set of preferred locales to appear at the top of the
  /// list.
  ///
  /// The order of locales in this list will also be the order of locale
  /// priority. For example, if a device supports 'en' and 'es' and
  /// ['es', 'en'] is passed in, the 'es' locale will take priority over 'en'.
  ///
  /// The list of preferred locales is specified with the [initialize] method.
  List<LocaleInfo> get preferredSupportedLocales => _preferredSupportedLocales;
  List<LocaleInfo> _preferredSupportedLocales;
541

542
  /// The list of all arb path strings in [inputDirectory].
543 544 545
  List<String> get arbPathStrings {
    return _allBundles.bundles.map((AppResourceBundle bundle) => bundle.file.path).toList();
  }
546 547

  /// The supported language codes as found in the arb files located in
548
  /// [inputDirectory].
549
  final Set<String> supportedLanguageCodes = <String>{};
550 551

  /// The supported locales as found in the arb files located in
552
  /// [inputDirectory].
553 554
  final Set<LocaleInfo> supportedLocales = <LocaleInfo>{};

555 556 557
  /// The header to be prepended to the generated Dart localization file.
  String header = '';

558 559
  final Map<LocaleInfo, List<String>> _unimplementedMessages = <LocaleInfo, List<String>>{};

560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576
  /// Whether to generate the Dart localization file with locales imported as
  /// deferred, allowing for lazy loading of each locale in Flutter web.
  ///
  /// This can reduce a web app’s initial startup time by decreasing the size of
  /// the JavaScript bundle. When [_useDeferredLoading] is set to true, the
  /// messages for a particular locale are only downloaded and loaded by the
  /// Flutter app as they are needed. For projects with a lot of different
  /// locales and many localization strings, it can be an performance
  /// improvement to have deferred loading. For projects with a small number of
  /// locales, the difference is negligible, and might slow down the start up
  /// compared to bundling the localizations with the rest of the application.
  ///
  /// Note that this flag does not affect other platforms such as mobile or
  /// desktop.
  bool get useDeferredLoading => _useDeferredLoading;
  bool _useDeferredLoading;

577 578 579 580 581 582 583 584
  /// Contains a map of each output language file to its corresponding content in
  /// string format.
  final Map<File, String> _languageFileMap = <File, String>{};

  /// Contains the generated application's localizations and localizations delegate
  /// classes.
  String _generatedLocalizationsFile;

585 586 587 588
  /// A generated file that will contain the list of messages for each locale
  /// that do not have a translation yet.
  File _untranslatedMessagesFile;

589 590 591 592 593 594
  /// The file that contains the list of inputs and outputs for generating
  /// localizations.
  File _inputsAndOutputsListFile;
  List<String> _inputFileList;
  List<String> _outputFileList;

595 596 597 598 599 600
  /// Whether or not resource attributes are required for each corresponding
  /// resource id.
  ///
  /// Resource attributes provide metadata about the message.
  bool _areResourceAttributesRequired;

601 602
  /// Initializes [inputDirectory], [outputDirectory], [templateArbFile],
  /// [outputFile] and [className].
603 604 605 606 607 608 609
  ///
  /// Throws an [L10nException] when a provided configuration is not allowed
  /// by [LocalizationsGenerator].
  ///
  /// Throws a [FileSystemException] when a file operation necessary for setting
  /// up the [LocalizationsGenerator] cannot be completed.
  void initialize({
610 611
    String inputPathString,
    String outputPathString,
612 613 614
    String templateArbFileName,
    String outputFileString,
    String classNameString,
615
    List<String> preferredSupportedLocales,
616 617
    String headerString,
    String headerFile,
618
    bool useDeferredLoading = false,
619
    String inputsAndOutputsListPath,
620
    bool useSyntheticPackage = true,
621
    String projectPathString,
622
    bool areResourceAttributesRequired = false,
623
    String untranslatedMessagesFile,
624
  }) {
625
    _useSyntheticPackage = useSyntheticPackage;
626
    setProjectDir(projectPathString);
627
    setInputDirectory(inputPathString);
628
    setOutputDirectory(outputPathString ?? inputPathString);
629
    setTemplateArbFile(templateArbFileName);
630
    setBaseOutputFile(outputFileString);
631
    setPreferredSupportedLocales(preferredSupportedLocales);
632
    _setHeader(headerString, headerFile);
633
    _setUseDeferredLoading(useDeferredLoading);
634
    _setUntranslatedMessagesFile(untranslatedMessagesFile);
635
    className = classNameString;
636
    _setInputsAndOutputsListFile(inputsAndOutputsListPath);
637
    _areResourceAttributesRequired = areResourceAttributesRequired;
638
  }
639

640 641 642 643 644 645 646 647 648 649 650 651 652 653
  static bool _isNotReadable(FileStat fileStat) {
    final String rawStatString = fileStat.modeString();
    // Removes potential prepended permission bits, such as '(suid)' and '(guid)'.
    final String statString = rawStatString.substring(rawStatString.length - 9);
    return !(statString[0] == 'r' || statString[3] == 'r' || statString[6] == 'r');
  }

  static bool _isNotWritable(FileStat fileStat) {
    final String rawStatString = fileStat.modeString();
    // Removes potential prepended permission bits, such as '(suid)' and '(guid)'.
    final String statString = rawStatString.substring(rawStatString.length - 9);
    return !(statString[1] == 'w' || statString[4] == 'w' || statString[7] == 'w');
  }

654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
  @visibleForTesting
  void setProjectDir(String projectPathString) {
    if (projectPathString == null) {
      return;
    }

    final Directory directory = _fs.directory(projectPathString);
    if (!directory.existsSync()) {
      throw L10nException(
        'Directory does not exist: $directory.\n'
        'Please select a directory that contains the project\'s localizations '
        'resource files.'
      );
    }
    projectDirectory = directory;
  }

671
  /// Sets the reference [Directory] for [inputDirectory].
672
  @visibleForTesting
673
  void setInputDirectory(String inputPathString) {
674
    if (inputPathString == null) {
675
      throw L10nException('inputPathString argument cannot be null');
676
    }
677 678 679 680 681 682
    inputDirectory = _fs.directory(
      projectDirectory != null
        ? _getAbsoluteProjectPath(inputPathString)
        : inputPathString
    );

683 684
    if (!inputDirectory.existsSync()) {
      throw L10nException(
685
        "The 'arb-dir' directory, '$inputDirectory', does not exist.\n"
686 687
        'Make sure that the correct path was provided.'
      );
688
    }
689

690
    final FileStat fileStat = inputDirectory.statSync();
691 692
    if (_isNotReadable(fileStat) || _isNotWritable(fileStat)) {
      throw L10nException(
693
        "The 'arb-dir' directory, '$inputDirectory', doesn't allow reading and writing.\n"
694 695
        'Please ensure that the user has read and write permissions.'
      );
696
    }
697 698
  }

699 700
  /// Sets the reference [Directory] for [outputDirectory].
  @visibleForTesting
701
  void setOutputDirectory(
702
    String outputPathString,
703 704
  ) {
    if (_useSyntheticPackage) {
705 706
      outputDirectory = _fs.directory(
        projectDirectory != null
707 708
          ? _getAbsoluteProjectPath(syntheticL10nPackagePath)
          : syntheticL10nPackagePath
709 710
      );
    } else {
711
      if (outputPathString == null) {
712 713 714 715
        throw L10nException(
          'outputPathString argument cannot be null if not using '
          'synthetic package option.'
        );
716
      }
717 718 719 720 721 722 723

      outputDirectory = _fs.directory(
        projectDirectory != null
          ? _getAbsoluteProjectPath(outputPathString)
          : outputPathString
      );
    }
724 725
  }

726 727 728
  /// Sets the reference [File] for [templateArbFile].
  @visibleForTesting
  void setTemplateArbFile(String templateArbFileName) {
729
    if (templateArbFileName == null) {
730
      throw L10nException('templateArbFileName argument cannot be null');
731 732
    }
    if (inputDirectory == null) {
733
      throw L10nException('inputDirectory cannot be null when setting template arb file');
734
    }
735

736
    templateArbFile = _fs.file(globals.fs.path.join(inputDirectory.path, templateArbFileName));
737
    final String templateArbFileStatModeString = templateArbFile.statSync().modeString();
738 739
    if (templateArbFileStatModeString[0] == '-' && templateArbFileStatModeString[3] == '-') {
      throw L10nException(
740 741 742
        "The 'template-arb-file', $templateArbFile, is not readable.\n"
        'Please ensure that the user has read permissions.'
      );
743
    }
744 745 746 747
  }

  /// Sets the reference [File] for the localizations delegate [outputFile].
  @visibleForTesting
748
  void setBaseOutputFile(String outputFileString) {
749
    if (outputFileString == null) {
750
      throw L10nException('outputFileString argument cannot be null');
751 752
    }
    baseOutputFile = _fs.file(globals.fs.path.join(outputDirectory.path, outputFileString));
753 754
  }

755 756
  static bool _isValidClassName(String className) {
    // Public Dart class name cannot begin with an underscore
757
    if (className[0] == '_') {
758
      return false;
759
    }
760
    // Dart class name cannot contain non-alphanumeric symbols
761
    if (className.contains(RegExp(r'[^a-zA-Z_\d]'))) {
762
      return false;
763
    }
764
    // Dart class name must start with upper case character
765
    if (className[0].contains(RegExp(r'[a-z]'))) {
766
      return false;
767
    }
768
    // Dart class name cannot start with a number
769
    if (className[0].contains(RegExp(r'\d'))) {
770
      return false;
771
    }
772 773 774
    return true;
  }

775 776
  /// Sets the [className] for the localizations and localizations delegate
  /// classes.
777 778
  @visibleForTesting
  set className(String classNameString) {
779
    if (classNameString == null || classNameString.isEmpty) {
780
      throw L10nException('classNameString argument cannot be null or empty');
781 782
    }
    if (!_isValidClassName(classNameString)) {
783
      throw L10nException(
784
        "The 'output-class', $classNameString, is not a valid public Dart class name.\n"
785
      );
786
    }
787 788 789
    _className = classNameString;
  }

790 791 792
  /// Sets [preferredSupportedLocales] so that this particular list of locales
  /// will take priority over the other locales.
  @visibleForTesting
793 794
  void setPreferredSupportedLocales(List<String> inputLocales) {
    if (inputLocales == null || inputLocales.isEmpty) {
795 796
      _preferredSupportedLocales = const <LocaleInfo>[];
    } else {
797 798
      _preferredSupportedLocales = inputLocales.map((String localeString) {
        return LocaleInfo.fromString(localeString);
799 800 801 802
      }).toList();
    }
  }

803 804 805 806 807 808 809 810 811 812 813 814
  void _setHeader(String headerString, String headerFile) {
    if (headerString != null && headerFile != null) {
      throw L10nException(
        'Cannot accept both header and header file arguments. \n'
        'Please make sure to define only one or the other. '
      );
    }

    if (headerString != null) {
      header = headerString;
    } else if (headerFile != null) {
      try {
815
        header = _fs.file(globals.fs.path.join(inputDirectory.path, headerFile)).readAsStringSync();
816 817 818 819 820 821 822 823 824
      } on FileSystemException catch (error) {
        throw L10nException (
          'Failed to read header file: "$headerFile". \n'
          'FileSystemException: ${error.message}'
        );
      }
    }
  }

825
  String _getAbsoluteProjectPath(String relativePath) => globals.fs.path.join(projectDirectory.path, relativePath);
826

827 828 829 830 831 832 833
  void _setUseDeferredLoading(bool useDeferredLoading) {
    if (useDeferredLoading == null) {
      throw L10nException('useDeferredLoading argument cannot be null.');
    }
    _useDeferredLoading = useDeferredLoading;
  }

834 835 836 837 838 839 840 841 842 843
  void _setUntranslatedMessagesFile(String untranslatedMessagesFileString) {
    if (untranslatedMessagesFileString == null || untranslatedMessagesFileString.isEmpty) {
      return;
    }

    _untranslatedMessagesFile = _fs.file(
      globals.fs.path.join(untranslatedMessagesFileString),
    );
  }

844
  void _setInputsAndOutputsListFile(String inputsAndOutputsListPath) {
845
    if (inputsAndOutputsListPath == null) {
846
      return;
847
    }
848 849

    _inputsAndOutputsListFile = _fs.file(
850
      globals.fs.path.join(inputsAndOutputsListPath, 'gen_l10n_inputs_and_outputs.json'),
851
    );
852

853 854 855 856
    _inputFileList = <String>[];
    _outputFileList = <String>[];
  }

857 858
  static bool _isValidGetterAndMethodName(String name) {
    // Public Dart method name must not start with an underscore
859
    if (name[0] == '_') {
860
      return false;
861
    }
862
    // Dart getter and method name cannot contain non-alphanumeric symbols
863
    if (name.contains(RegExp(r'[^a-zA-Z_\d]'))) {
864
      return false;
865
    }
866
    // Dart method name must start with lower case character
867
    if (name[0].contains(RegExp(r'[A-Z]'))) {
868
      return false;
869
    }
870
    // Dart class name cannot start with a number
871
    if (name[0].contains(RegExp(r'\d'))) {
872
      return false;
873
    }
874 875 876
    return true;
  }

877
  // Load _allMessages from templateArbFile and _allBundles from all of the ARB
878
  // files in inputDirectory. Also initialized: supportedLocales.
879 880
  void loadResources() {
    final AppResourceBundle templateBundle = AppResourceBundle(templateArbFile);
881
    _templateArbLocale = templateBundle.locale;
882 883 884
    _allMessages = templateBundle.resourceIds.map((String id) => Message(
      templateBundle.resources, id, _areResourceAttributesRequired,
    ));
885
    for (final String resourceId in templateBundle.resourceIds) {
886 887 888 889 890 891 892 893
      if (!_isValidGetterAndMethodName(resourceId)) {
        throw L10nException(
          'Invalid ARB resource name "$resourceId" in $templateArbFile.\n'
          'Resources names must be valid Dart method names: they have to be '
          'camel case, cannot start with a number or underscore, and cannot '
          'contain non-alphanumeric characters.'
        );
      }
894
    }
895

896
    _allBundles = AppResourceBundleCollection(inputDirectory);
897 898 899 900 901
    if (_inputsAndOutputsListFile != null) {
      _inputFileList.addAll(_allBundles.bundles.map((AppResourceBundle bundle) {
        return bundle.file.absolute.path;
      }));
    }
902

903 904 905 906
    final List<LocaleInfo> allLocales = List<LocaleInfo>.from(_allBundles.locales);
    for (final LocaleInfo preferredLocale in preferredSupportedLocales) {
      final int index = allLocales.indexOf(preferredLocale);
      if (index == -1) {
907
        throw L10nException(
908 909 910 911
          "The preferred supported locale, '$preferredLocale', cannot be "
          'added. Please make sure that there is a corresponding ARB file '
          'with translations for the locale, or remove the locale from the '
          'preferred supported locale list.'
912
        );
913
      }
914 915
      allLocales.removeAt(index);
      allLocales.insertAll(0, preferredSupportedLocales);
916
    }
917
    supportedLocales.addAll(allLocales);
918 919
  }

920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984
  void _addUnimplementedMessage(LocaleInfo locale, String message) {
    if (_unimplementedMessages.containsKey(locale)) {
      _unimplementedMessages[locale].add(message);
    } else {
      _unimplementedMessages.putIfAbsent(locale, () => <String>[message]);
    }
  }

  String _generateBaseClassFile(
    String className,
    String fileName,
    String header,
    AppResourceBundle bundle,
    AppResourceBundle templateBundle,
    Iterable<Message> messages,
  ) {
    final LocaleInfo locale = bundle.locale;

    final Iterable<String> methods = messages.map((Message message) {
      if (bundle.translationFor(message) == null) {
        _addUnimplementedMessage(locale, message.resourceId);
      }

      return generateMethod(
        message,
        bundle.translationFor(message) == null ? templateBundle : bundle,
      );
    });

    return classFileTemplate
      .replaceAll('@(header)', header)
      .replaceAll('@(language)', describeLocale(locale.toString()))
      .replaceAll('@(baseClass)', className)
      .replaceAll('@(fileName)', fileName)
      .replaceAll('@(class)', '$className${locale.camelCase()}')
      .replaceAll('@(localeName)', locale.toString())
      .replaceAll('@(methods)', methods.join('\n\n'));
  }

  String _generateSubclass(
    String className,
    AppResourceBundle bundle,
    Iterable<Message> messages,
  ) {
    final LocaleInfo locale = bundle.locale;
    final String baseClassName = '$className${LocaleInfo.fromString(locale.languageCode).camelCase()}';

    messages
      .where((Message message) => bundle.translationFor(message) == null)
      .forEach((Message message) {
        _addUnimplementedMessage(locale, message.resourceId);
      });

    final Iterable<String> methods = messages
      .where((Message message) => bundle.translationFor(message) != null)
      .map((Message message) => generateMethod(message, bundle));

    return subclassTemplate
      .replaceAll('@(language)', describeLocale(locale.toString()))
      .replaceAll('@(baseLanguageClassName)', baseClassName)
      .replaceAll('@(class)', '$className${locale.camelCase()}')
      .replaceAll('@(localeName)', locale.toString())
      .replaceAll('@(methods)', methods.join('\n\n'));
  }

985
  // Generate the AppLocalizations class, its LocalizationsDelegate subclass,
986 987 988
  // and all AppLocalizations subclasses for every locale. This method by
  // itself does not generate the output files.
  void _generateCode() {
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004
    bool isBaseClassLocale(LocaleInfo locale, String language) {
      return locale.languageCode == language
          && locale.countryCode == null
          && locale.scriptCode == null;
    }

    List<LocaleInfo> getLocalesForLanguage(String language) {
      return _allBundles.bundles
        // Return locales for the language specified, except for the base locale itself
        .where((AppResourceBundle bundle) {
          final LocaleInfo locale = bundle.locale;
          return !isBaseClassLocale(locale, language) && locale.languageCode == language;
        })
        .map((AppResourceBundle bundle) => bundle.locale).toList();
    }

1005 1006
    final String directory = globals.fs.path.basename(outputDirectory.path);
    final String outputFileName = globals.fs.path.basename(baseOutputFile.path);
1007 1008

    final Iterable<String> supportedLocalesCode = supportedLocales.map((LocaleInfo locale) {
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
      final String languageCode = locale.languageCode;
      final String countryCode = locale.countryCode;
      final String scriptCode = locale.scriptCode;

      if (countryCode == null && scriptCode == null) {
        return 'Locale(\'$languageCode\')';
      } else if (countryCode != null && scriptCode == null) {
        return 'Locale(\'$languageCode\', \'$countryCode\')';
      } else if (countryCode != null && scriptCode != null) {
        return 'Locale.fromSubtags(languageCode: \'$languageCode\', countryCode: \'$countryCode\', scriptCode: \'$scriptCode\')';
      } else {
        return 'Locale.fromSubtags(languageCode: \'$languageCode\', scriptCode: \'$scriptCode\')';
      }
1022 1023 1024 1025
    });

    final Set<String> supportedLanguageCodes = Set<String>.from(
      _allBundles.locales.map<String>((LocaleInfo locale) => '\'${locale.languageCode}\'')
1026
    );
1027 1028

    final List<LocaleInfo> allLocales = _allBundles.locales.toList()..sort();
1029
    final String fileName = outputFileName.split('.')[0];
1030
    for (final LocaleInfo locale in allLocales) {
1031
      if (isBaseClassLocale(locale, locale.languageCode)) {
1032
        final File languageMessageFile = _fs.file(
1033
          globals.fs.path.join(outputDirectory.path, '${fileName}_$locale.dart'),
1034 1035 1036 1037 1038
        );

        // Generate the template for the base class file. Further string
        // interpolation will be done to determine if there are
        // subclasses that extend the base class.
1039
        final String languageBaseClassFile = _generateBaseClassFile(
1040 1041 1042 1043
          className,
          outputFileName,
          header,
          _allBundles.bundleFor(locale),
1044
          _allBundles.bundleFor(_templateArbLocale),
1045 1046 1047 1048 1049 1050 1051 1052
          _allMessages,
        );

        // Every locale for the language except the base class.
        final List<LocaleInfo> localesForLanguage = getLocalesForLanguage(locale.languageCode);

        // Generate every subclass that is needed for the particular language
        final Iterable<String> subclasses = localesForLanguage.map<String>((LocaleInfo locale) {
1053 1054 1055 1056
          return _generateSubclass(
            className,
            _allBundles.bundleFor(locale),
            _allMessages,
1057 1058 1059
          );
        });

1060 1061 1062
        _languageFileMap.putIfAbsent(languageMessageFile, () {
          return languageBaseClassFile.replaceAll('@(subclasses)', subclasses.join());
        });
1063
      }
1064 1065
    }

1066
    final List<String> sortedClassImports = supportedLocales
1067 1068
      .where((LocaleInfo locale) => isBaseClassLocale(locale, locale.languageCode))
      .map((LocaleInfo locale) {
1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085
        final String library = '${fileName}_${locale.toString()}';
        if (useDeferredLoading) {
          return "import '$library.dart' deferred as $library;";
        } else {
          return "import '$library.dart';";
        }
      })
      .toList()
      ..sort();

    final String delegateClass = _generateDelegateClass(
      allBundles: _allBundles,
      className: className,
      supportedLanguageCodes: supportedLanguageCodes,
      useDeferredLoading: useDeferredLoading,
      fileName: fileName,
    );
1086

1087
    _generatedLocalizationsFile = fileTemplate
1088
      .replaceAll('@(header)', header)
1089
      .replaceAll('@(class)', className)
1090
      .replaceAll('@(methods)', _allMessages.map((Message message) => generateBaseClassMethod(message, _templateArbLocale)).join('\n'))
1091 1092 1093
      .replaceAll('@(importFile)', '$directory/$outputFileName')
      .replaceAll('@(supportedLocales)', supportedLocalesCode.join(',\n    '))
      .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', '))
1094 1095
      .replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
      .replaceAll('@(delegateClass)', delegateClass);
1096 1097
  }

1098
  void writeOutputFiles(Logger logger, { bool isFromYaml = false }) {
1099
    // First, generate the string contents of all necessary files.
1100
    _generateCode();
1101

1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112
    // A pubspec.yaml file is required when using a synthetic package. If it does not
    // exist, create a blank one.
    if (_useSyntheticPackage) {
      final Directory syntheticPackageDirectory = _fs.directory(defaultSyntheticPackagePath);
      syntheticPackageDirectory.createSync(recursive: true);
      final File flutterGenPubspec = syntheticPackageDirectory.childFile('pubspec.yaml');
      if (!flutterGenPubspec.existsSync()) {
        flutterGenPubspec.writeAsStringSync(emptyPubspecTemplate);
      }
    }

1113 1114
    // Since all validity checks have passed up to this point,
    // write the contents into the directory.
1115
    outputDirectory.createSync(recursive: true);
1116 1117 1118

    // Ensure that the created directory has read/write permissions.
    final FileStat fileStat = outputDirectory.statSync();
1119 1120
    if (_isNotReadable(fileStat) || _isNotWritable(fileStat)) {
      throw L10nException(
1121 1122 1123
        "The 'output-dir' directory, $outputDirectory, doesn't allow reading and writing.\n"
        'Please ensure that the user has read and write permissions.'
      );
1124
    }
1125 1126 1127 1128

    // Generate the required files for localizations.
    _languageFileMap.forEach((File file, String contents) {
      file.writeAsStringSync(contents);
1129 1130 1131
      if (_inputsAndOutputsListFile != null) {
        _outputFileList.add(file.absolute.path);
      }
1132
    });
1133 1134

    baseOutputFile.writeAsStringSync(_generatedLocalizationsFile);
1135 1136 1137 1138 1139 1140 1141

    if (_untranslatedMessagesFile != null) {
      _generateUntranslatedMessagesFile(logger);
    } else if (_unimplementedMessages.isNotEmpty) {
      _unimplementedMessages.forEach((LocaleInfo locale, List<String> messages) {
        logger.printStatus('"$locale": ${messages.length} untranslated message(s).');
      });
1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157
      if (isFromYaml) {
        logger.printStatus(
          'To see a detailed report, use the untranslated-messages-file \n'
          'option in the l10n.yaml file:\n'
          'untranslated-messages-file: desiredFileName.txt\n'
          '<other option>: <other selection> \n\n'
        );
      } else {
        logger.printStatus(
          'To see a detailed report, use the --untranslated-messages-file \n'
          'option in the flutter gen-l10n tool:\n'
          'flutter gen-l10n --untranslated-messages-file=desiredFileName.txt\n'
          '<other options> \n\n'
        );
      }

1158
      logger.printStatus(
1159 1160
        'This will generate a JSON format file containing all messages that \n'
        'need to be translated.'
1161 1162 1163
      );
    }

1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178
    if (_inputsAndOutputsListFile != null) {
      _outputFileList.add(baseOutputFile.absolute.path);

      // Generate a JSON file containing the inputs and outputs of the gen_l10n script.
      if (!_inputsAndOutputsListFile.existsSync()) {
        _inputsAndOutputsListFile.createSync(recursive: true);
      }

      _inputsAndOutputsListFile.writeAsStringSync(
        json.encode(<String, Object> {
          'inputs': _inputFileList,
          'outputs': _outputFileList,
        }),
      );
    }
1179
  }
1180

1181
  void _generateUntranslatedMessagesFile(Logger logger) {
1182 1183 1184 1185 1186 1187
    if (logger == null) {
      throw L10nException(
        'Logger must be defined when generating untranslated messages file.'
      );
    }

1188
    if (_unimplementedMessages.isEmpty) {
1189 1190 1191 1192
      _untranslatedMessagesFile.writeAsStringSync('{}');
      if (_inputsAndOutputsListFile != null) {
        _outputFileList.add(_untranslatedMessagesFile.absolute.path);
      }
1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218
      return;
    }

    String resultingFile = '{\n';
    int count = 0;
    final int numberOfLocales = _unimplementedMessages.length;
    _unimplementedMessages.forEach((LocaleInfo locale, List<String> messages) {
      resultingFile += '  "$locale": [\n';

      for (int i = 0; i < messages.length; i += 1) {
        resultingFile += '    "${messages[i]}"';
        if (i != messages.length - 1) {
          resultingFile += ',';
        }
        resultingFile += '\n';
      }

      resultingFile += '  ]';
      count += 1;
      if (count < numberOfLocales) {
        resultingFile += ',\n';
      }
      resultingFile += '\n';
    });

    resultingFile += '}\n';
1219 1220 1221 1222
    _untranslatedMessagesFile.writeAsStringSync(resultingFile);
    if (_inputsAndOutputsListFile != null) {
      _outputFileList.add(_untranslatedMessagesFile.absolute.path);
    }
1223
  }
1224
}