1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// This program generates a getMaterialTranslation() and a 6// getCupertinoTranslation() function that look up the translations provided by 7// the arb files. The returned value is a generated instance of a 8// GlobalMaterialLocalizations or a GlobalCupertinoLocalizations that 9// corresponds to a single locale. 10// 11// The *.arb files are in packages/flutter_localizations/lib/src/l10n. 12// 13// The arb (JSON) format files must contain a single map indexed by locale. 14// Each map value is itself a map with resource identifier keys and localized 15// resource string values. 16// 17// The arb filenames are expected to have the form "material_(\w+)\.arb" or 18// "cupertino_(\w+)\.arb" where the group following "_" identifies the language 19// code and the country code, e.g. "material_en.arb" or "material_en_GB.arb". 20// In most cases both codes are just two characters. 21// 22// This app is typically run by hand when a module's .arb files have been 23// updated. 24// 25// ## Usage 26// 27// Run this program from the root of the git repository. 28// 29// The following outputs the generated Dart code to the console as a dry run: 30// 31// ``` 32// dart dev/tools/localization/gen_localizations.dart 33// ``` 34// 35// If the data looks good, use the `-w` or `--overwrite` option to overwrite the 36// packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart 37// and packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart file: 38// 39// ``` 40// dart dev/tools/localization/gen_localizations.dart --overwrite 41// ``` 42 43import 'dart:async'; 44import 'dart:io'; 45 46import 'package:path/path.dart' as path; 47import 'package:meta/meta.dart'; 48 49import 'gen_cupertino_localizations.dart'; 50import 'gen_material_localizations.dart'; 51import 'localizations_utils.dart'; 52import 'localizations_validator.dart'; 53 54/// This is the core of this script; it generates the code used for translations. 55String generateArbBasedLocalizationSubclasses({ 56 @required Map<LocaleInfo, Map<String, String>> localeToResources, 57 @required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes, 58 @required String generatedClassPrefix, 59 @required String baseClass, 60 @required HeaderGenerator generateHeader, 61 @required ConstructorGenerator generateConstructor, 62 @required String factoryName, 63 @required String factoryDeclaration, 64 @required String factoryArguments, 65 @required String supportedLanguagesConstant, 66 @required String supportedLanguagesDocMacro, 67}) { 68 assert(localeToResources != null); 69 assert(localeToResourceAttributes != null); 70 assert(generatedClassPrefix.isNotEmpty); 71 assert(baseClass.isNotEmpty); 72 assert(generateHeader != null); 73 assert(generateConstructor != null); 74 assert(factoryName.isNotEmpty); 75 assert(factoryDeclaration.isNotEmpty); 76 assert(factoryArguments.isNotEmpty); 77 assert(supportedLanguagesConstant.isNotEmpty); 78 assert(supportedLanguagesDocMacro.isNotEmpty); 79 80 final StringBuffer output = StringBuffer(); 81 output.writeln(generateHeader('dart dev/tools/localization/gen_localizations.dart --overwrite')); 82 83 final StringBuffer supportedLocales = StringBuffer(); 84 85 final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{}; 86 final Map<String, Set<String>> languageToScriptCodes = <String, Set<String>>{}; 87 // Used to calculate if there are any corresponding countries for a given language and script. 88 final Map<LocaleInfo, Set<String>> languageAndScriptToCountryCodes = <LocaleInfo, Set<String>>{}; 89 final Set<String> allResourceIdentifiers = <String>{}; 90 for (LocaleInfo locale in localeToResources.keys.toList()..sort()) { 91 if (locale.scriptCode != null) { 92 languageToScriptCodes[locale.languageCode] ??= <String>{}; 93 languageToScriptCodes[locale.languageCode].add(locale.scriptCode); 94 } 95 if (locale.countryCode != null && locale.scriptCode != null) { 96 final LocaleInfo key = LocaleInfo.fromString(locale.languageCode + '_' + locale.scriptCode); 97 languageAndScriptToCountryCodes[key] ??= <String>{}; 98 languageAndScriptToCountryCodes[key].add(locale.countryCode); 99 } 100 languageToLocales[locale.languageCode] ??= <LocaleInfo>[]; 101 languageToLocales[locale.languageCode].add(locale); 102 allResourceIdentifiers.addAll(localeToResources[locale].keys); 103 } 104 105 // We generate one class per supported language (e.g. 106 // `MaterialLocalizationEn`). These implement everything that is needed by the 107 // superclass (e.g. GlobalMaterialLocalizations). 108 109 // We also generate one subclass for each locale with a script code (e.g. 110 // `MaterialLocalizationZhHant`). Their superclasses are the aforementioned 111 // language classes for the same locale but without a script code (e.g. 112 // `MaterialLocalizationZh`). 113 114 // We also generate one subclass for each locale with a country code (e.g. 115 // `MaterialLocalizationEnGb`). Their superclasses are the aforementioned 116 // language classes for the same locale but without a country code (e.g. 117 // `MaterialLocalizationEn`). 118 119 // If scriptCodes for a language are defined, we expect a scriptCode to be 120 // defined for locales that contain a countryCode. The superclass becomes 121 // the script sublcass (e.g. `MaterialLocalizationZhHant`) and the generated 122 // subclass will also contain the script code (e.g. `MaterialLocalizationZhHantTW`). 123 124 // When scriptCodes are not defined for languages that use scriptCodes to distinguish 125 // between significantly differing scripts, we assume the scriptCodes in the 126 // [LocaleInfo.fromString] factory and add it to the [LocaleInfo]. We then generate 127 // the script classes based on the first locale that we assume to use the script. 128 129 final List<String> allKeys = allResourceIdentifiers.toList()..sort(); 130 final List<String> languageCodes = languageToLocales.keys.toList()..sort(); 131 final LocaleInfo canonicalLocale = LocaleInfo.fromString('en'); 132 for (String languageName in languageCodes) { 133 final LocaleInfo languageLocale = LocaleInfo.fromString(languageName); 134 output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass)); 135 output.writeln(generateConstructor(languageLocale)); 136 137 final Map<String, String> languageResources = localeToResources[languageLocale]; 138 for (String key in allKeys) { 139 final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key]; 140 output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale)); 141 } 142 output.writeln('}'); 143 int countryCodeCount = 0; 144 int scriptCodeCount = 0; 145 if (languageToScriptCodes.containsKey(languageName)) { 146 scriptCodeCount = languageToScriptCodes[languageName].length; 147 // Language has scriptCodes, so we need to properly fallback countries to corresponding 148 // script default values before language default values. 149 for (String scriptCode in languageToScriptCodes[languageName]) { 150 final LocaleInfo scriptBaseLocale = LocaleInfo.fromString(languageName + '_' + scriptCode); 151 output.writeln(generateClassDeclaration( 152 scriptBaseLocale, 153 generatedClassPrefix, 154 '$generatedClassPrefix${camelCase(languageLocale)}', 155 )); 156 output.writeln(generateConstructor(scriptBaseLocale)); 157 final Map<String, String> scriptResources = localeToResources[scriptBaseLocale]; 158 for (String key in scriptResources.keys) { 159 if (languageResources[key] == scriptResources[key]) 160 continue; 161 final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key]; 162 output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale)); 163 } 164 output.writeln('}'); 165 166 final List<LocaleInfo> localeCodes = languageToLocales[languageName]..sort(); 167 for (LocaleInfo locale in localeCodes) { 168 if (locale.originalString == languageName) 169 continue; 170 if (locale.originalString == languageName + '_' + scriptCode) 171 continue; 172 if (locale.scriptCode != scriptCode) 173 continue; 174 countryCodeCount += 1; 175 output.writeln(generateClassDeclaration( 176 locale, 177 generatedClassPrefix, 178 '$generatedClassPrefix${camelCase(scriptBaseLocale)}', 179 )); 180 output.writeln(generateConstructor(locale)); 181 final Map<String, String> localeResources = localeToResources[locale]; 182 for (String key in localeResources.keys) { 183 // When script fallback contains the key, we compare to it instead of language fallback. 184 if (scriptResources.containsKey(key) ? scriptResources[key] == localeResources[key] : languageResources[key] == localeResources[key]) 185 continue; 186 final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key]; 187 output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale)); 188 } 189 output.writeln('}'); 190 } 191 } 192 } else { 193 // No scriptCode. Here, we do not compare against script default (because it 194 // doesn't exist). 195 final List<LocaleInfo> localeCodes = languageToLocales[languageName]..sort(); 196 for (LocaleInfo locale in localeCodes) { 197 if (locale.originalString == languageName) 198 continue; 199 countryCodeCount += 1; 200 final Map<String, String> localeResources = localeToResources[locale]; 201 output.writeln(generateClassDeclaration( 202 locale, 203 generatedClassPrefix, 204 '$generatedClassPrefix${camelCase(languageLocale)}', 205 )); 206 output.writeln(generateConstructor(locale)); 207 for (String key in localeResources.keys) { 208 if (languageResources[key] == localeResources[key]) 209 continue; 210 final Map<String, dynamic> attributes = localeToResourceAttributes[canonicalLocale][key]; 211 output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale)); 212 } 213 output.writeln('}'); 214 } 215 } 216 final String scriptCodeMessage = scriptCodeCount == 0 ? '' : ' and $scriptCodeCount script' + (scriptCodeCount == 1 ? '' : 's'); 217 if (countryCodeCount == 0) { 218 if (scriptCodeCount == 0) 219 supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)}'); 220 else 221 supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script' + (scriptCodeCount == 1 ? '' : 's') + ')'); 222 223 } else if (countryCodeCount == 1) { 224 supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)'); 225 } else { 226 supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)'); 227 } 228 } 229 230 // Generate the factory function. Given a Locale it returns the corresponding 231 // base class implementation. 232 output.writeln(''' 233 234/// The set of supported languages, as language code strings. 235/// 236/// The [$baseClass.delegate] can generate localizations for 237/// any [Locale] with a language code from this set, regardless of the region. 238/// Some regions have specific support (e.g. `de` covers all forms of German, 239/// but there is support for `de-CH` specifically to override some of the 240/// translations for Switzerland). 241/// 242/// See also: 243/// 244/// * [$factoryName], whose documentation describes these values. 245final Set<String> $supportedLanguagesConstant = HashSet<String>.from(const <String>[ 246${languageCodes.map<String>((String value) => " '$value', // ${describeLocale(value)}").toList().join('\n')} 247]); 248 249/// Creates a [$baseClass] instance for the given `locale`. 250/// 251/// All of the function's arguments except `locale` will be passed to the [ 252/// $baseClass] constructor. (The `localeName` argument of that 253/// constructor is specified by the actual subclass constructor by this 254/// function.) 255/// 256/// The following locales are supported by this package: 257/// 258/// {@template $supportedLanguagesDocMacro} 259$supportedLocales/// {@endtemplate} 260/// 261/// Generally speaking, this method is only intended to be used by 262/// [$baseClass.delegate]. 263$factoryDeclaration 264 switch (locale.languageCode) {'''); 265 for (String language in languageToLocales.keys) { 266 // Only one instance of the language. 267 if (languageToLocales[language].length == 1) { 268 output.writeln(''' 269 case '$language': 270 return $generatedClassPrefix${camelCase(languageToLocales[language][0])}($factoryArguments);'''); 271 } else if (!languageToScriptCodes.containsKey(language)) { // Does not distinguish between scripts. Switch on countryCode directly. 272 output.writeln(''' 273 case '$language': { 274 switch (locale.countryCode) {'''); 275 for (LocaleInfo locale in languageToLocales[language]) { 276 if (locale.originalString == language) 277 continue; 278 assert(locale.length > 1); 279 final String countryCode = locale.countryCode; 280 output.writeln(''' 281 case '$countryCode': 282 return $generatedClassPrefix${camelCase(locale)}($factoryArguments);'''); 283 } 284 output.writeln(''' 285 } 286 return $generatedClassPrefix${camelCase(LocaleInfo.fromString(language))}($factoryArguments); 287 }'''); 288 } else { // Language has scriptCode, add additional switch logic. 289 bool hasCountryCode = false; 290 output.writeln(''' 291 case '$language': { 292 switch (locale.scriptCode) {'''); 293 for (String scriptCode in languageToScriptCodes[language]) { 294 final LocaleInfo scriptLocale = LocaleInfo.fromString(language + '_' + scriptCode); 295 output.writeln(''' 296 case '$scriptCode': {'''); 297 if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) { 298 output.writeln(''' 299 switch (locale.countryCode) {'''); 300 for (LocaleInfo locale in languageToLocales[language]) { 301 if (locale.countryCode == null) 302 continue; 303 else 304 hasCountryCode = true; 305 if (locale.originalString == language) 306 continue; 307 if (locale.scriptCode != scriptCode && locale.scriptCode != null) 308 continue; 309 final String countryCode = locale.countryCode; 310 output.writeln(''' 311 case '$countryCode': 312 return $generatedClassPrefix${camelCase(locale)}($factoryArguments);'''); 313 } 314 } 315 // Return a fallback locale that matches scriptCode, but not countryCode. 316 // 317 // Explicitly defined scriptCode fallback: 318 if (languageToLocales[language].contains(scriptLocale)) { 319 if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) { 320 output.writeln(''' 321 }'''); 322 } 323 output.writeln(''' 324 return $generatedClassPrefix${camelCase(scriptLocale)}($factoryArguments); 325 }'''); 326 } else { 327 // Not Explicitly defined, fallback to first locale with the same language and 328 // script: 329 for (LocaleInfo locale in languageToLocales[language]) { 330 if (locale.scriptCode != scriptCode) 331 continue; 332 if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) { 333 output.writeln(''' 334 }'''); 335 } 336 output.writeln(''' 337 return $generatedClassPrefix${camelCase(scriptLocale)}($factoryArguments); 338 }'''); 339 break; 340 } 341 } 342 } 343 output.writeln(''' 344 }'''); 345 if (hasCountryCode) { 346 output.writeln(''' 347 switch (locale.countryCode) {'''); 348 for (LocaleInfo locale in languageToLocales[language]) { 349 if (locale.originalString == language) 350 continue; 351 assert(locale.length > 1); 352 if (locale.countryCode == null) 353 continue; 354 final String countryCode = locale.countryCode; 355 output.writeln(''' 356 case '$countryCode': 357 return $generatedClassPrefix${camelCase(locale)}($factoryArguments);'''); 358 } 359 output.writeln(''' 360 }'''); 361 } 362 output.writeln(''' 363 return $generatedClassPrefix${camelCase(LocaleInfo.fromString(language))}($factoryArguments); 364 }'''); 365 } 366 } 367 output.writeln(''' 368 } 369 assert(false, '$factoryName() called for unsupported locale "\$locale"'); 370 return null; 371}'''); 372 373 return output.toString(); 374} 375 376/// Returns the appropriate type for getters with the given attributes. 377/// 378/// Typically "String", but some (e.g. "timeOfDayFormat") return enums. 379/// 380/// Used by [generateGetter] below. 381String generateType(Map<String, dynamic> attributes) { 382 if (attributes != null) { 383 switch (attributes['x-flutter-type']) { 384 case 'icuShortTimePattern': 385 return 'TimeOfDayFormat'; 386 case 'scriptCategory': 387 return 'ScriptCategory'; 388 } 389 } 390 return 'String'; 391} 392 393/// Returns the appropriate name for getters with the given attributes. 394/// 395/// Typically this is the key unmodified, but some have parameters, and 396/// the GlobalMaterialLocalizations class does the substitution, and for 397/// those we have to therefore provide an alternate name. 398/// 399/// Used by [generateGetter] below. 400String generateKey(String key, Map<String, dynamic> attributes) { 401 if (attributes != null) { 402 if (attributes.containsKey('parameters')) 403 return '${key}Raw'; 404 switch (attributes['x-flutter-type']) { 405 case 'icuShortTimePattern': 406 return '${key}Raw'; 407 } 408 } 409 if (key == 'datePickerDateOrder') 410 return 'datePickerDateOrderString'; 411 if (key == 'datePickerDateTimeOrder') 412 return 'datePickerDateTimeOrderString'; 413 return key; 414} 415 416const Map<String, String> _icuTimeOfDayToEnum = <String, String>{ 417 'HH:mm': 'TimeOfDayFormat.HH_colon_mm', 418 'HH.mm': 'TimeOfDayFormat.HH_dot_mm', 419 "HH 'h' mm": 'TimeOfDayFormat.frenchCanadian', 420 'HH:mm น.': 'TimeOfDayFormat.HH_colon_mm', 421 'H:mm': 'TimeOfDayFormat.H_colon_mm', 422 'h:mm a': 'TimeOfDayFormat.h_colon_mm_space_a', 423 'a h:mm': 'TimeOfDayFormat.a_space_h_colon_mm', 424 'ah:mm': 'TimeOfDayFormat.a_space_h_colon_mm', 425}; 426 427const Map<String, String> _scriptCategoryToEnum = <String, String>{ 428 'English-like': 'ScriptCategory.englishLike', 429 'dense': 'ScriptCategory.dense', 430 'tall': 'ScriptCategory.tall', 431}; 432 433/// Returns the literal that describes the value returned by getters 434/// with the given attributes. 435/// 436/// This handles cases like the value being a literal `null`, an enum, and so 437/// on. The default is to treat the value as a string and escape it and quote 438/// it. 439/// 440/// Used by [generateGetter] below. 441String generateValue(String value, Map<String, dynamic> attributes, LocaleInfo locale) { 442 if (value == null) 443 return null; 444 // cupertino_en.arb doesn't use x-flutter-type. 445 if (attributes != null) { 446 switch (attributes['x-flutter-type']) { 447 case 'icuShortTimePattern': 448 if (!_icuTimeOfDayToEnum.containsKey(value)) { 449 throw Exception( 450 '"$value" is not one of the ICU short time patterns supported ' 451 'by the material library. Here is the list of supported ' 452 'patterns:\n ' + _icuTimeOfDayToEnum.keys.join('\n ') 453 ); 454 } 455 return _icuTimeOfDayToEnum[value]; 456 case 'scriptCategory': 457 if (!_scriptCategoryToEnum.containsKey(value)) { 458 throw Exception( 459 '"$value" is not one of the scriptCategory values supported ' 460 'by the material library. Here is the list of supported ' 461 'values:\n ' + _scriptCategoryToEnum.keys.join('\n ') 462 ); 463 } 464 return _scriptCategoryToEnum[value]; 465 } 466 } 467 // Localization strings for the Kannada locale ('kn') are encoded because 468 // some of the localized strings contain characters that can crash Emacs on Linux. 469 // See packages/flutter_localizations/lib/src/l10n/README for more information. 470 return locale.languageCode == 'kn' ? generateEncodedString(value) : generateString(value); 471} 472 473/// Combines [generateType], [generateKey], and [generateValue] to return 474/// the source of getters for the GlobalMaterialLocalizations subclass. 475/// The locale is the locale for which the getter is being generated. 476String generateGetter(String key, String value, Map<String, dynamic> attributes, LocaleInfo locale) { 477 final String type = generateType(attributes); 478 key = generateKey(key, attributes); 479 value = generateValue(value, attributes, locale); 480 return ''' 481 482 @override 483 $type get $key => $value;'''; 484} 485 486Future<void> main(List<String> rawArgs) async { 487 checkCwdIsRepoRoot('gen_localizations'); 488 final GeneratorOptions options = parseArgs(rawArgs); 489 490 // filenames are assumed to end in "prefix_lc.arb" or "prefix_lc_cc.arb", where prefix 491 // is the 2nd command line argument, lc is a language code and cc is the country 492 // code. In most cases both codes are just two characters. 493 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 } on ValidationError catch (exception) { 502 exitWithError('$exception'); 503 } 504 505 await precacheLanguageAndRegionTags(); 506 507 // Maps of locales to resource key/value pairs for Material ARBs. 508 final Map<LocaleInfo, Map<String, String>> materialLocaleToResources = <LocaleInfo, Map<String, String>>{}; 509 // Maps of locales to resource key/attributes pairs for Material ARBs.. 510 // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes 511 final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{}; 512 // Maps of locales to resource key/value pairs for Cupertino ARBs. 513 final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources = <LocaleInfo, Map<String, String>>{}; 514 // Maps of locales to resource key/attributes pairs for Cupertino ARBs.. 515 // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes 516 final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{}; 517 518 loadMatchingArbsIntoBundleMaps( 519 directory: directory, 520 filenamePattern: materialFilenameRE, 521 localeToResources: materialLocaleToResources, 522 localeToResourceAttributes: materialLocaleToResourceAttributes, 523 ); 524 loadMatchingArbsIntoBundleMaps( 525 directory: directory, 526 filenamePattern: cupertinoFilenameRE, 527 localeToResources: cupertinoLocaleToResources, 528 localeToResourceAttributes: cupertinoLocaleToResourceAttributes, 529 ); 530 531 try { 532 validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes); 533 validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes); 534 } on ValidationError catch (exception) { 535 exitWithError('$exception'); 536 } 537 538 final String materialLocalizations = options.writeToFile || !options.cupertinoOnly 539 ? generateArbBasedLocalizationSubclasses( 540 localeToResources: materialLocaleToResources, 541 localeToResourceAttributes: materialLocaleToResourceAttributes, 542 generatedClassPrefix: 'MaterialLocalization', 543 baseClass: 'GlobalMaterialLocalizations', 544 generateHeader: generateMaterialHeader, 545 generateConstructor: generateMaterialConstructor, 546 factoryName: materialFactoryName, 547 factoryDeclaration: materialFactoryDeclaration, 548 factoryArguments: materialFactoryArguments, 549 supportedLanguagesConstant: materialSupportedLanguagesConstant, 550 supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro, 551 ) 552 : null; 553 final String cupertinoLocalizations = options.writeToFile || !options.materialOnly 554 ? generateArbBasedLocalizationSubclasses( 555 localeToResources: cupertinoLocaleToResources, 556 localeToResourceAttributes: cupertinoLocaleToResourceAttributes, 557 generatedClassPrefix: 'CupertinoLocalization', 558 baseClass: 'GlobalCupertinoLocalizations', 559 generateHeader: generateCupertinoHeader, 560 generateConstructor: generateCupertinoConstructor, 561 factoryName: cupertinoFactoryName, 562 factoryDeclaration: cupertinoFactoryDeclaration, 563 factoryArguments: cupertinoFactoryArguments, 564 supportedLanguagesConstant: cupertinoSupportedLanguagesConstant, 565 supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro, 566 ) 567 : null; 568 569 if (options.writeToFile) { 570 final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart')); 571 materialLocalizationsFile.writeAsStringSync(materialLocalizations, flush: true); 572 final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart')); 573 cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations, flush: true); 574 } else { 575 if (!options.cupertinoOnly) { 576 stdout.write(materialLocalizations); 577 } 578 if (!options.materialOnly) { 579 stdout.write(cupertinoLocalizations); 580 } 581 } 582} 583