1// Copyright 2014 The Flutter 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 getMaterialTranslation(), getCupertinoTranslation(),
6// and getWidgetsTranslation() functions that look up the translations provided by
7// the arb files. The returned value is a generated instance of a
8// GlobalMaterialLocalizations, GlobalCupertinoLocalizations, or
9// GlobalWidgetsLocalizations object that 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/bin/gen_localizations.dart
33// ```
34//
35// If you have removed localizations from the canonical localizations, then
36// add the '--remove-undefined' flag to also remove them from the other files.
37//
38// ```
39// dart dev/tools/localization/bin/gen_localizations.dart --remove-undefined
40// ```
41//
42// If the data looks good, use the `-w` or `--overwrite` option to overwrite the
43// generated_material_localizations.dart, generated_cupertino_localizations.dart,
44// and generated_widgets_localizations.dart files in packages/flutter_localizations/lib/src/l10n/:
45//
46// ```
47// dart dev/tools/localization/bin/gen_localizations.dart --overwrite
48// ```
49
50import 'dart:io';
51
52import 'package:path/path.dart' as path;
53
54import '../gen_cupertino_localizations.dart';
55import '../gen_material_localizations.dart';
56import '../gen_widgets_localizations.dart';
57import '../localizations_utils.dart';
58import '../localizations_validator.dart';
59import 'encode_kn_arb_files.dart';
60
61/// This is the core of this script; it generates the code used for translations.
62String generateArbBasedLocalizationSubclasses({
63 required Map<LocaleInfo, Map<String, String>> localeToResources,
64 required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes,
65 required String generatedClassPrefix,
66 required String baseClass,
67 required HeaderGenerator generateHeader,
68 required ConstructorGenerator generateConstructor,
69 ConstructorGenerator? generateConstructorForCountrySubClass,
70 required String factoryName,
71 required String factoryDeclaration,
72 required bool callsFactoryWithConst,
73 required String factoryArguments,
74 required String supportedLanguagesConstant,
75 required String supportedLanguagesDocMacro,
76}) {
77 assert(generatedClassPrefix.isNotEmpty);
78 assert(baseClass.isNotEmpty);
79 assert(factoryName.isNotEmpty);
80 assert(factoryDeclaration.isNotEmpty);
81 assert(factoryArguments.isNotEmpty);
82 assert(supportedLanguagesConstant.isNotEmpty);
83 assert(supportedLanguagesDocMacro.isNotEmpty);
84 generateConstructorForCountrySubClass ??= generateConstructor;
85 final StringBuffer output = StringBuffer();
86 output.writeln(
87 generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'),
88 );
89
90 final StringBuffer supportedLocales = StringBuffer();
91
92 final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{};
93 final Map<String, Set<String>> languageToScriptCodes = <String, Set<String>>{};
94 // Used to calculate if there are any corresponding countries for a given language and script.
95 final Map<LocaleInfo, Set<String>> languageAndScriptToCountryCodes = <LocaleInfo, Set<String>>{};
96 final Set<String> allResourceIdentifiers = <String>{};
97 for (final LocaleInfo locale in localeToResources.keys.toList()..sort()) {
98 if (locale.scriptCode != null) {
99 languageToScriptCodes[locale.languageCode] ??= <String>{};
100 languageToScriptCodes[locale.languageCode]!.add(locale.scriptCode!);
101 }
102 if (locale.countryCode != null && locale.scriptCode != null) {
103 final LocaleInfo key = LocaleInfo.fromString('${locale.languageCode}_${locale.scriptCode}');
104 languageAndScriptToCountryCodes[key] ??= <String>{};
105 languageAndScriptToCountryCodes[key]!.add(locale.countryCode!);
106 }
107 languageToLocales[locale.languageCode] ??= <LocaleInfo>[];
108 languageToLocales[locale.languageCode]!.add(locale);
109 allResourceIdentifiers.addAll(localeToResources[locale]!.keys.toList()..sort());
110 }
111
112 // We generate one class per supported language (e.g.
113 // `MaterialLocalizationEn`). These implement everything that is needed by the
114 // superclass (e.g. GlobalMaterialLocalizations).
115
116 // We also generate one subclass for each locale with a script code (e.g.
117 // `MaterialLocalizationZhHant`). Their superclasses are the aforementioned
118 // language classes for the same locale but without a script code (e.g.
119 // `MaterialLocalizationZh`).
120
121 // We also generate one subclass for each locale with a country code (e.g.
122 // `MaterialLocalizationEnGb`). Their superclasses are the aforementioned
123 // language classes for the same locale but without a country code (e.g.
124 // `MaterialLocalizationEn`).
125
126 // If scriptCodes for a language are defined, we expect a scriptCode to be
127 // defined for locales that contain a countryCode. The superclass becomes
128 // the script subclass (e.g. `MaterialLocalizationZhHant`) and the generated
129 // subclass will also contain the script code (e.g. `MaterialLocalizationZhHantTW`).
130
131 // When scriptCodes are not defined for languages that use scriptCodes to distinguish
132 // between significantly differing scripts, we assume the scriptCodes in the
133 // [LocaleInfo.fromString] factory and add it to the [LocaleInfo]. We then generate
134 // the script classes based on the first locale that we assume to use the script.
135
136 final List<String> allKeys = allResourceIdentifiers.toList()..sort();
137 final List<String> languageCodes = languageToLocales.keys.toList()..sort();
138 final LocaleInfo canonicalLocale = LocaleInfo.fromString('en');
139 for (final String languageName in languageCodes) {
140 final LocaleInfo languageLocale = LocaleInfo.fromString(languageName);
141 output.writeln(generateClassDeclaration(languageLocale, generatedClassPrefix, baseClass));
142 output.writeln(generateConstructor(languageLocale));
143
144 final Map<String, String> languageResources = localeToResources[languageLocale]!;
145 for (final String key in allKeys) {
146 final Map<String, dynamic>? attributes =
147 localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
148 output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale));
149 }
150 output.writeln('}');
151 int countryCodeCount = 0;
152 int scriptCodeCount = 0;
153 if (languageToScriptCodes.containsKey(languageName)) {
154 scriptCodeCount = languageToScriptCodes[languageName]!.length;
155 // Language has scriptCodes, so we need to properly fallback countries to corresponding
156 // script default values before language default values.
157 for (final String scriptCode in languageToScriptCodes[languageName]!) {
158 final LocaleInfo scriptBaseLocale = LocaleInfo.fromString('${languageName}_$scriptCode');
159 output.writeln(
160 generateClassDeclaration(
161 scriptBaseLocale,
162 generatedClassPrefix,
163 '$generatedClassPrefix${languageLocale.camelCase()}',
164 ),
165 );
166 output.writeln(generateConstructorForCountrySubClass(scriptBaseLocale));
167 final Map<String, String> scriptResources = localeToResources[scriptBaseLocale]!;
168 for (final String key in scriptResources.keys.toList()..sort()) {
169 if (languageResources[key] == scriptResources[key]) {
170 continue;
171 }
172 final Map<String, dynamic>? attributes =
173 localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
174 output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale));
175 }
176 output.writeln('}');
177
178 final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort();
179 for (final LocaleInfo locale in localeCodes) {
180 if (locale.originalString == languageName) {
181 continue;
182 }
183 if (locale.originalString == '${languageName}_$scriptCode') {
184 continue;
185 }
186 if (locale.scriptCode != scriptCode) {
187 continue;
188 }
189 countryCodeCount += 1;
190 output.writeln(
191 generateClassDeclaration(
192 locale,
193 generatedClassPrefix,
194 '$generatedClassPrefix${scriptBaseLocale.camelCase()}',
195 ),
196 );
197 output.writeln(generateConstructorForCountrySubClass(locale));
198 final Map<String, String> localeResources = localeToResources[locale]!;
199 for (final String key in localeResources.keys) {
200 // When script fallback contains the key, we compare to it instead of language fallback.
201 if (scriptResources.containsKey(key)
202 ? scriptResources[key] == localeResources[key]
203 : languageResources[key] == localeResources[key]) {
204 continue;
205 }
206 final Map<String, dynamic>? attributes =
207 localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
208 output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
209 }
210 output.writeln('}');
211 }
212 }
213 } else {
214 // No scriptCode. Here, we do not compare against script default (because it
215 // doesn't exist).
216 final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort();
217 for (final LocaleInfo locale in localeCodes) {
218 if (locale.originalString == languageName) {
219 continue;
220 }
221 countryCodeCount += 1;
222 final Map<String, String> localeResources = localeToResources[locale]!;
223 output.writeln(
224 generateClassDeclaration(
225 locale,
226 generatedClassPrefix,
227 '$generatedClassPrefix${languageLocale.camelCase()}',
228 ),
229 );
230 output.writeln(generateConstructorForCountrySubClass(locale));
231 for (final String key in localeResources.keys) {
232 if (languageResources[key] == localeResources[key]) {
233 continue;
234 }
235 final Map<String, dynamic>? attributes =
236 localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
237 output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
238 }
239 output.writeln('}');
240 }
241 }
242
243 final String scriptCodeMessage = scriptCodeCount == 0
244 ? ''
245 : ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}';
246 if (countryCodeCount == 0) {
247 if (scriptCodeCount == 0) {
248 supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)}');
249 } else {
250 supportedLocales.writeln(
251 '/// * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})',
252 );
253 }
254 } else if (countryCodeCount == 1) {
255 supportedLocales.writeln(
256 '/// * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)',
257 );
258 } else {
259 supportedLocales.writeln(
260 '/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)',
261 );
262 }
263 }
264
265 // Generate the factory function. Given a Locale it returns the corresponding
266 // base class implementation.
267 output.writeln('''
268
269/// The set of supported languages, as language code strings.
270///
271/// The [$baseClass.delegate] can generate localizations for
272/// any [Locale] with a language code from this set, regardless of the region.
273/// Some regions have specific support (e.g. `de` covers all forms of German,
274/// but there is support for `de-CH` specifically to override some of the
275/// translations for Switzerland).
276///
277/// See also:
278///
279/// * [$factoryName], whose documentation describes these values.
280final Set<String> $supportedLanguagesConstant = HashSet<String>.from(const <String>[
281${languageCodes.map<String>((String value) => " '$value', // ${describeLocale(value)}").toList().join('\n')}
282]);
283
284/// Creates a [$baseClass] instance for the given `locale`.
285///
286/// All of the function's arguments except `locale` will be passed to the [
287/// $baseClass] constructor. (The `localeName` argument of that
288/// constructor is specified by the actual subclass constructor by this
289/// function.)
290///
291/// The following locales are supported by this package:
292///
293/// {@template $supportedLanguagesDocMacro}
294$supportedLocales/// {@endtemplate}
295///
296/// Generally speaking, this method is only intended to be used by
297/// [$baseClass.delegate].
298$factoryDeclaration
299 switch (locale.languageCode) {''');
300 for (final String language in languageToLocales.keys) {
301 // Only one instance of the language.
302 if (languageToLocales[language]!.length == 1) {
303 output.writeln(
304 '''
305 case '$language':
306 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${languageToLocales[language]![0].camelCase()}($factoryArguments);''',
307 );
308 } else if (!languageToScriptCodes.containsKey(language)) {
309 // Does not distinguish between scripts. Switch on countryCode directly.
310 output.writeln('''
311 case '$language': {
312 switch (locale.countryCode) {''');
313 for (final LocaleInfo locale in languageToLocales[language]!) {
314 if (locale.originalString == language) {
315 continue;
316 }
317 assert(locale.length > 1);
318 final String countryCode = locale.countryCode!;
319 output.writeln(
320 '''
321 case '$countryCode':
322 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
323 );
324 }
325 output.writeln('''
326 }
327 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
328 }''');
329 } else {
330 // Language has scriptCode, add additional switch logic.
331 bool hasCountryCode = false;
332 output.writeln('''
333 case '$language': {
334 switch (locale.scriptCode) {''');
335 for (final String scriptCode in languageToScriptCodes[language]!) {
336 final LocaleInfo scriptLocale = LocaleInfo.fromString('${language}_$scriptCode');
337 output.writeln('''
338 case '$scriptCode': {''');
339 if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
340 output.writeln('''
341 switch (locale.countryCode) {''');
342 for (final LocaleInfo locale in languageToLocales[language]!) {
343 if (locale.countryCode == null) {
344 continue;
345 } else {
346 hasCountryCode = true;
347 }
348 if (locale.originalString == language) {
349 continue;
350 }
351 if (locale.scriptCode != scriptCode && locale.scriptCode != null) {
352 continue;
353 }
354 final String countryCode = locale.countryCode!;
355 output.writeln(
356 '''
357 case '$countryCode':
358 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
359 );
360 }
361 }
362 // Return a fallback locale that matches scriptCode, but not countryCode.
363 //
364 // Explicitly defined scriptCode fallback:
365 if (languageToLocales[language]!.contains(scriptLocale)) {
366 if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
367 output.writeln('''
368 }''');
369 }
370 output.writeln('''
371 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
372 }''');
373 } else {
374 // Not Explicitly defined, fallback to first locale with the same language and
375 // script:
376 for (final LocaleInfo locale in languageToLocales[language]!) {
377 if (locale.scriptCode != scriptCode) {
378 continue;
379 }
380 if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) {
381 output.writeln('''
382 }''');
383 }
384 output.writeln('''
385 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
386 }''');
387 break;
388 }
389 }
390 }
391 output.writeln('''
392 }''');
393 if (hasCountryCode) {
394 output.writeln('''
395 switch (locale.countryCode) {''');
396 for (final LocaleInfo locale in languageToLocales[language]!) {
397 if (locale.originalString == language) {
398 continue;
399 }
400 assert(locale.length > 1);
401 if (locale.countryCode == null) {
402 continue;
403 }
404 final String countryCode = locale.countryCode!;
405 output.writeln(
406 '''
407 case '$countryCode':
408 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
409 );
410 }
411 output.writeln('''
412 }''');
413 }
414 output.writeln('''
415 return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
416 }''');
417 }
418 }
419 output.writeln('''
420 }
421 assert(false, '$factoryName() called for unsupported locale "\$locale"');
422 return null;
423}''');
424
425 return output.toString();
426}
427
428/// Returns the appropriate type for getters with the given attributes.
429///
430/// Typically "String", but some (e.g. "timeOfDayFormat") return enums.
431///
432/// Used by [generateGetter] below.
433String generateType(Map<String, dynamic>? attributes) {
434 final bool optional = attributes?.containsKey('optional') ?? false;
435 final String type = switch (attributes?['x-flutter-type']) {
436 'icuShortTimePattern' => 'TimeOfDayFormat',
437 'scriptCategory' => 'ScriptCategory',
438 _ => 'String',
439 };
440 return type + (optional ? '?' : '');
441}
442
443/// Returns the appropriate name for getters with the given attributes.
444///
445/// Typically this is the key unmodified, but some have parameters, and
446/// the GlobalMaterialLocalizations class does the substitution, and for
447/// those we have to therefore provide an alternate name.
448///
449/// Used by [generateGetter] below.
450String generateKey(String key, Map<String, dynamic>? attributes) {
451 if (attributes != null) {
452 if (attributes.containsKey('parameters')) {
453 return '${key}Raw';
454 }
455 switch (attributes['x-flutter-type'] as String?) {
456 case 'icuShortTimePattern':
457 return '${key}Raw';
458 }
459 }
460 if (key == 'datePickerDateOrder') {
461 return 'datePickerDateOrderString';
462 }
463 if (key == 'datePickerDateTimeOrder') {
464 return 'datePickerDateTimeOrderString';
465 }
466 return key;
467}
468
469const Map<String, String> _icuTimeOfDayToEnum = <String, String>{
470 'HH:mm': 'TimeOfDayFormat.HH_colon_mm',
471 'HH.mm': 'TimeOfDayFormat.HH_dot_mm',
472 "HH 'h' mm": 'TimeOfDayFormat.frenchCanadian',
473 'HH:mm น.': 'TimeOfDayFormat.HH_colon_mm',
474 'H:mm': 'TimeOfDayFormat.H_colon_mm',
475 'h:mm a': 'TimeOfDayFormat.h_colon_mm_space_a',
476 'a h:mm': 'TimeOfDayFormat.a_space_h_colon_mm',
477 'ah:mm': 'TimeOfDayFormat.a_space_h_colon_mm',
478};
479
480const Map<String, String> _scriptCategoryToEnum = <String, String>{
481 'English-like': 'ScriptCategory.englishLike',
482 'dense': 'ScriptCategory.dense',
483 'tall': 'ScriptCategory.tall',
484};
485
486/// Returns the literal that describes the value returned by getters
487/// with the given attributes.
488///
489/// This handles cases like the value being a literal `null`, an enum, and so
490/// on. The default is to treat the value as a string and escape it and quote
491/// it.
492///
493/// Used by [generateGetter] below.
494String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
495 if (value == null) {
496 return null;
497 }
498 // cupertino_en.arb doesn't use x-flutter-type.
499 if (attributes != null) {
500 switch (attributes['x-flutter-type'] as String?) {
501 case 'icuShortTimePattern':
502 if (!_icuTimeOfDayToEnum.containsKey(value)) {
503 throw Exception(
504 '"$value" is not one of the ICU short time patterns supported '
505 'by the material library. Here is the list of supported '
506 'patterns:\n ${_icuTimeOfDayToEnum.keys.join('\n ')}',
507 );
508 }
509 return _icuTimeOfDayToEnum[value];
510 case 'scriptCategory':
511 if (!_scriptCategoryToEnum.containsKey(value)) {
512 throw Exception(
513 '"$value" is not one of the scriptCategory values supported '
514 'by the material library. Here is the list of supported '
515 'values:\n ${_scriptCategoryToEnum.keys.join('\n ')}',
516 );
517 }
518 return _scriptCategoryToEnum[value];
519 }
520 }
521 return generateEncodedString(locale.languageCode, value);
522}
523
524/// Combines [generateType], [generateKey], and [generateValue] to return
525/// the source of getters for the GlobalMaterialLocalizations subclass.
526/// The locale is the locale for which the getter is being generated.
527String generateGetter(
528 String key,
529 String? value,
530 Map<String, dynamic>? attributes,
531 LocaleInfo locale,
532) {
533 final String type = generateType(attributes);
534 key = generateKey(key, attributes);
535 final String? generatedValue = generateValue(value, attributes, locale);
536 return '''
537
538 @override
539 $type get $key => $generatedValue;''';
540}
541
542void main(List<String> rawArgs) {
543 checkCwdIsRepoRoot('gen_localizations');
544 final GeneratorOptions options = parseArgs(rawArgs);
545
546 // filenames are assumed to end in "prefix_lc.arb" or "prefix_lc_cc.arb", where prefix
547 // is the 2nd command line argument, lc is a language code and cc is the country
548 // code. In most cases both codes are just two characters.
549
550 final Directory directory = Directory(
551 path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'),
552 );
553 final RegExp widgetsFilenameRE = RegExp(r'widgets_(\w+)\.arb$');
554 final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
555 final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
556
557 try {
558 validateEnglishLocalizations(File(path.join(directory.path, 'widgets_en.arb')));
559 validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb')));
560 validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb')));
561 } on ValidationError catch (exception) {
562 exitWithError('$exception');
563 }
564
565 // Only rewrite material_kn.arb and cupertino_en.arb if overwriting the
566 // Material and Cupertino localizations files.
567 if (options.writeToFile) {
568 // Encodes the material_kn.arb file and the cupertino_en.arb files before
569 // generating localizations. This prevents a subset of Emacs users from
570 // crashing when opening up the Flutter source code.
571 // See https://github.com/flutter/flutter/issues/36704 for more context.
572 encodeKnArbFiles(directory);
573 }
574
575 precacheLanguageAndRegionTags();
576
577 // Maps of locales to resource key/value pairs for Widgets ARBs.
578 final Map<LocaleInfo, Map<String, String>> widgetsLocaleToResources =
579 <LocaleInfo, Map<String, String>>{};
580 // Maps of locales to resource key/attributes pairs for Widgets ARBs.
581 // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
582 final Map<LocaleInfo, Map<String, dynamic>> widgetsLocaleToResourceAttributes =
583 <LocaleInfo, Map<String, dynamic>>{};
584
585 // Maps of locales to resource key/value pairs for Material ARBs.
586 final Map<LocaleInfo, Map<String, String>> materialLocaleToResources =
587 <LocaleInfo, Map<String, String>>{};
588 // Maps of locales to resource key/attributes pairs for Material ARBs.
589 // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
590 final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes =
591 <LocaleInfo, Map<String, dynamic>>{};
592
593 // Maps of locales to resource key/value pairs for Cupertino ARBs.
594 final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources =
595 <LocaleInfo, Map<String, String>>{};
596 // Maps of locales to resource key/attributes pairs for Cupertino ARBs.
597 // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
598 final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes =
599 <LocaleInfo, Map<String, dynamic>>{};
600
601 loadMatchingArbsIntoBundleMaps(
602 directory: directory,
603 filenamePattern: widgetsFilenameRE,
604 localeToResources: widgetsLocaleToResources,
605 localeToResourceAttributes: widgetsLocaleToResourceAttributes,
606 );
607 loadMatchingArbsIntoBundleMaps(
608 directory: directory,
609 filenamePattern: materialFilenameRE,
610 localeToResources: materialLocaleToResources,
611 localeToResourceAttributes: materialLocaleToResourceAttributes,
612 );
613 loadMatchingArbsIntoBundleMaps(
614 directory: directory,
615 filenamePattern: cupertinoFilenameRE,
616 localeToResources: cupertinoLocaleToResources,
617 localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
618 );
619
620 try {
621 validateLocalizations(
622 widgetsLocaleToResources,
623 widgetsLocaleToResourceAttributes,
624 removeUndefined: options.removeUndefined,
625 );
626 validateLocalizations(
627 materialLocaleToResources,
628 materialLocaleToResourceAttributes,
629 removeUndefined: options.removeUndefined,
630 );
631 validateLocalizations(
632 cupertinoLocaleToResources,
633 cupertinoLocaleToResourceAttributes,
634 removeUndefined: options.removeUndefined,
635 );
636 } on ValidationError catch (exception) {
637 exitWithError('$exception');
638 }
639 if (options.removeUndefined) {
640 removeUndefinedLocalizations(widgetsLocaleToResources);
641 removeUndefinedLocalizations(materialLocaleToResources);
642 removeUndefinedLocalizations(cupertinoLocaleToResources);
643 }
644
645 final String? widgetsLocalizations = options.writeToFile || !options.cupertinoOnly
646 ? generateArbBasedLocalizationSubclasses(
647 localeToResources: widgetsLocaleToResources,
648 localeToResourceAttributes: widgetsLocaleToResourceAttributes,
649 generatedClassPrefix: 'WidgetsLocalization',
650 baseClass: 'GlobalWidgetsLocalizations',
651 generateHeader: generateWidgetsHeader,
652 generateConstructor: generateWidgetsConstructor,
653 generateConstructorForCountrySubClass: generateWidgetsConstructorForCountrySubclass,
654 factoryName: widgetsFactoryName,
655 factoryDeclaration: widgetsFactoryDeclaration,
656 callsFactoryWithConst: true,
657 factoryArguments: widgetsFactoryArguments,
658 supportedLanguagesConstant: widgetsSupportedLanguagesConstant,
659 supportedLanguagesDocMacro: widgetsSupportedLanguagesDocMacro,
660 )
661 : null;
662 final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly
663 ? generateArbBasedLocalizationSubclasses(
664 localeToResources: materialLocaleToResources,
665 localeToResourceAttributes: materialLocaleToResourceAttributes,
666 generatedClassPrefix: 'MaterialLocalization',
667 baseClass: 'GlobalMaterialLocalizations',
668 generateHeader: generateMaterialHeader,
669 generateConstructor: generateMaterialConstructor,
670 factoryName: materialFactoryName,
671 factoryDeclaration: materialFactoryDeclaration,
672 callsFactoryWithConst: false,
673 factoryArguments: materialFactoryArguments,
674 supportedLanguagesConstant: materialSupportedLanguagesConstant,
675 supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
676 )
677 : null;
678 final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly
679 ? generateArbBasedLocalizationSubclasses(
680 localeToResources: cupertinoLocaleToResources,
681 localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
682 generatedClassPrefix: 'CupertinoLocalization',
683 baseClass: 'GlobalCupertinoLocalizations',
684 generateHeader: generateCupertinoHeader,
685 generateConstructor: generateCupertinoConstructor,
686 factoryName: cupertinoFactoryName,
687 factoryDeclaration: cupertinoFactoryDeclaration,
688 callsFactoryWithConst: false,
689 factoryArguments: cupertinoFactoryArguments,
690 supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
691 supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
692 )
693 : null;
694
695 if (options.writeToFile) {
696 final File widgetsLocalizationsFile = File(
697 path.join(directory.path, 'generated_widgets_localizations.dart'),
698 );
699 widgetsLocalizationsFile.writeAsStringSync(widgetsLocalizations!, flush: true);
700 final File materialLocalizationsFile = File(
701 path.join(directory.path, 'generated_material_localizations.dart'),
702 );
703 materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
704 final File cupertinoLocalizationsFile = File(
705 path.join(directory.path, 'generated_cupertino_localizations.dart'),
706 );
707 cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
708 } else {
709 if (options.cupertinoOnly) {
710 stdout.write(cupertinoLocalizations);
711 } else if (options.materialOnly) {
712 stdout.write(materialLocalizations);
713 } else if (options.widgetsOnly) {
714 stdout.write(widgetsLocalizations);
715 } else {
716 stdout.write(widgetsLocalizations);
717 stdout.write(materialLocalizations);
718 stdout.write(cupertinoLocalizations);
719 }
720 }
721}
722