| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| // This program generates getMaterialTranslation(), getCupertinoTranslation(), | |
| // and getWidgetsTranslation() functions that look up the translations provided by | |
| // the arb files. The returned value is a generated instance of a | |
| // GlobalMaterialLocalizations, GlobalCupertinoLocalizations, or | |
| // GlobalWidgetsLocalizations object that corresponds to a single locale. | |
| // | |
| // The *.arb files are in packages/flutter_localizations/lib/src/l10n. | |
| // | |
| // The arb (JSON) format files must contain a single map indexed by locale. | |
| // Each map value is itself a map with resource identifier keys and localized | |
| // resource string values. | |
| // | |
| // The arb filenames are expected to have the form "material_(\w+)\.arb" or | |
| // "cupertino_(\w+)\.arb" where the group following "_" identifies the language | |
| // code and the country code, e.g. "material_en.arb" or "material_en_GB.arb". | |
| // In most cases both codes are just two characters. | |
| // | |
| // This app is typically run by hand when a module's .arb files have been | |
| // updated. | |
| // | |
| // ## Usage | |
| // | |
| // Run this program from the root of the git repository. | |
| // | |
| // The following outputs the generated Dart code to the console as a dry run: | |
| // | |
| // ``` | |
| // dart dev/tools/localization/bin/gen_localizations.dart | |
| // ``` | |
| // | |
| // If you have removed localizations from the canonical localizations, then | |
| // add the '--remove-undefined' flag to also remove them from the other files. | |
| // | |
| // ``` | |
| // dart dev/tools/localization/bin/gen_localizations.dart --remove-undefined | |
| // ``` | |
| // | |
| // If the data looks good, use the `-w` or `--overwrite` option to overwrite the | |
| // generated_material_localizations.dart, generated_cupertino_localizations.dart, | |
| // and generated_widgets_localizations.dart files in packages/flutter_localizations/lib/src/l10n/: | |
| // | |
| // ``` | |
| // dart dev/tools/localization/bin/gen_localizations.dart --overwrite | |
| // ``` | |
| import 'dart:io'; | |
| import 'package:path/path.dart' as path; | |
| import '../gen_cupertino_localizations.dart'; | |
| import '../gen_material_localizations.dart'; | |
| import '../gen_widgets_localizations.dart'; | |
| import '../localizations_utils.dart'; | |
| import '../localizations_validator.dart'; | |
| import 'encode_kn_arb_files.dart'; | |
| /// This is the core of this script; it generates the code used for translations. | |
| String generateArbBasedLocalizationSubclasses({ | |
| required Map<LocaleInfo, Map<String, String>> localeToResources, | |
| required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes, | |
| required String generatedClassPrefix, | |
| required String baseClass, | |
| required HeaderGenerator generateHeader, | |
| required ConstructorGenerator generateConstructor, | |
| ConstructorGenerator? generateConstructorForCountrySubClass, | |
| required String factoryName, | |
| required String factoryDeclaration, | |
| required bool callsFactoryWithConst, | |
| required String factoryArguments, | |
| required String supportedLanguagesConstant, | |
| required String supportedLanguagesDocMacro, | |
| String? deprecatedReplacementPackage, | |
| Set<String>? generatedClassNames, | |
| }) { | |
| assert(generatedClassPrefix.isNotEmpty); | |
| assert(baseClass.isNotEmpty); | |
| assert(factoryName.isNotEmpty); | |
| assert(factoryDeclaration.isNotEmpty); | |
| assert(factoryArguments.isNotEmpty); | |
| assert(supportedLanguagesConstant.isNotEmpty); | |
| assert(supportedLanguagesDocMacro.isNotEmpty); | |
| generateConstructorForCountrySubClass ??= generateConstructor; | |
| final output = StringBuffer(); | |
| output.writeln( | |
| generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'), | |
| ); | |
| final supportedLocales = StringBuffer(); | |
| final languageToLocales = <String, List<LocaleInfo>>{}; | |
| final languageToScriptCodes = <String, Set<String>>{}; | |
| // Used to calculate if there are any corresponding countries for a given language and script. | |
| final languageAndScriptToCountryCodes = <LocaleInfo, Set<String>>{}; | |
| final allResourceIdentifiers = <String>{}; | |
| for (final LocaleInfo locale in localeToResources.keys.toList()..sort()) { | |
| if (locale.scriptCode != null) { | |
| languageToScriptCodes[locale.languageCode] ??= <String>{}; | |
| languageToScriptCodes[locale.languageCode]!.add(locale.scriptCode!); | |
| } | |
| if (locale.countryCode != null && locale.scriptCode != null) { | |
| final key = LocaleInfo.fromString('${locale.languageCode}_${locale.scriptCode}'); | |
| languageAndScriptToCountryCodes[key] ??= <String>{}; | |
| languageAndScriptToCountryCodes[key]!.add(locale.countryCode!); | |
| } | |
| languageToLocales[locale.languageCode] ??= <LocaleInfo>[]; | |
| languageToLocales[locale.languageCode]!.add(locale); | |
| allResourceIdentifiers.addAll(localeToResources[locale]!.keys.toList()..sort()); | |
| } | |
| // We generate one class per supported language (e.g. | |
| // `MaterialLocalizationEn`). These implement everything that is needed by the | |
| // superclass (e.g. GlobalMaterialLocalizations). | |
| // We also generate one subclass for each locale with a script code (e.g. | |
| // `MaterialLocalizationZhHant`). Their superclasses are the aforementioned | |
| // language classes for the same locale but without a script code (e.g. | |
| // `MaterialLocalizationZh`). | |
| // We also generate one subclass for each locale with a country code (e.g. | |
| // `MaterialLocalizationEnGb`). Their superclasses are the aforementioned | |
| // language classes for the same locale but without a country code (e.g. | |
| // `MaterialLocalizationEn`). | |
| // If scriptCodes for a language are defined, we expect a scriptCode to be | |
| // defined for locales that contain a countryCode. The superclass becomes | |
| // the script subclass (e.g. `MaterialLocalizationZhHant`) and the generated | |
| // subclass will also contain the script code (e.g. `MaterialLocalizationZhHantTW`). | |
| // When scriptCodes are not defined for languages that use scriptCodes to distinguish | |
| // between significantly differing scripts, we assume the scriptCodes in the | |
| // [LocaleInfo.fromString] factory and add it to the [LocaleInfo]. We then generate | |
| // the script classes based on the first locale that we assume to use the script. | |
| final List<String> allKeys = allResourceIdentifiers.toList()..sort(); | |
| final List<String> languageCodes = languageToLocales.keys.toList()..sort(); | |
| final canonicalLocale = LocaleInfo.fromString('en'); | |
| for (final languageName in languageCodes) { | |
| final languageLocale = LocaleInfo.fromString(languageName); | |
| generatedClassNames?.add('$generatedClassPrefix${languageLocale.camelCase()}'); | |
| output.writeln( | |
| generateClassDeclaration( | |
| languageLocale, | |
| generatedClassPrefix, | |
| baseClass, | |
| deprecatedReplacementPackage: deprecatedReplacementPackage, | |
| ), | |
| ); | |
| output.writeln(generateConstructor(languageLocale)); | |
| final Map<String, String> languageResources = localeToResources[languageLocale]!; | |
| for (final key in allKeys) { | |
| final attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?; | |
| output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale)); | |
| } | |
| output.writeln('}'); | |
| var countryCodeCount = 0; | |
| var scriptCodeCount = 0; | |
| if (languageToScriptCodes.containsKey(languageName)) { | |
| scriptCodeCount = languageToScriptCodes[languageName]!.length; | |
| // Language has scriptCodes, so we need to properly fallback countries to corresponding | |
| // script default values before language default values. | |
| for (final String scriptCode in languageToScriptCodes[languageName]!) { | |
| final scriptBaseLocale = LocaleInfo.fromString('${languageName}_$scriptCode'); | |
| generatedClassNames?.add('$generatedClassPrefix${scriptBaseLocale.camelCase()}'); | |
| output.writeln( | |
| generateClassDeclaration( | |
| scriptBaseLocale, | |
| generatedClassPrefix, | |
| '$generatedClassPrefix${languageLocale.camelCase()}', | |
| deprecatedReplacementPackage: deprecatedReplacementPackage, | |
| ), | |
| ); | |
| output.writeln(generateConstructorForCountrySubClass(scriptBaseLocale)); | |
| final Map<String, String> scriptResources = localeToResources[scriptBaseLocale]!; | |
| for (final String key in scriptResources.keys.toList()..sort()) { | |
| if (languageResources[key] == scriptResources[key]) { | |
| continue; | |
| } | |
| final attributes = | |
| localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?; | |
| output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale)); | |
| } | |
| output.writeln('}'); | |
| final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort(); | |
| for (final locale in localeCodes) { | |
| if (locale.originalString == languageName) { | |
| continue; | |
| } | |
| if (locale.originalString == '${languageName}_$scriptCode') { | |
| continue; | |
| } | |
| if (locale.scriptCode != scriptCode) { | |
| continue; | |
| } | |
| countryCodeCount += 1; | |
| generatedClassNames?.add('$generatedClassPrefix${locale.camelCase()}'); | |
| output.writeln( | |
| generateClassDeclaration( | |
| locale, | |
| generatedClassPrefix, | |
| '$generatedClassPrefix${scriptBaseLocale.camelCase()}', | |
| deprecatedReplacementPackage: deprecatedReplacementPackage, | |
| ), | |
| ); | |
| output.writeln(generateConstructorForCountrySubClass(locale)); | |
| final Map<String, String> localeResources = localeToResources[locale]!; | |
| for (final String key in localeResources.keys) { | |
| // When script fallback contains the key, we compare to it instead of language fallback. | |
| if (scriptResources.containsKey(key) | |
| ? scriptResources[key] == localeResources[key] | |
| : languageResources[key] == localeResources[key]) { | |
| continue; | |
| } | |
| final attributes = | |
| localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?; | |
| output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale)); | |
| } | |
| output.writeln('}'); | |
| } | |
| } | |
| } else { | |
| // No scriptCode. Here, we do not compare against script default (because it | |
| // doesn't exist). | |
| final List<LocaleInfo> localeCodes = languageToLocales[languageName]!..sort(); | |
| for (final locale in localeCodes) { | |
| if (locale.originalString == languageName) { | |
| continue; | |
| } | |
| countryCodeCount += 1; | |
| final Map<String, String> localeResources = localeToResources[locale]!; | |
| generatedClassNames?.add('$generatedClassPrefix${locale.camelCase()}'); | |
| output.writeln( | |
| generateClassDeclaration( | |
| locale, | |
| generatedClassPrefix, | |
| '$generatedClassPrefix${languageLocale.camelCase()}', | |
| deprecatedReplacementPackage: deprecatedReplacementPackage, | |
| ), | |
| ); | |
| output.writeln(generateConstructorForCountrySubClass(locale)); | |
| for (final String key in localeResources.keys) { | |
| if (languageResources[key] == localeResources[key]) { | |
| continue; | |
| } | |
| final attributes = | |
| localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?; | |
| output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale)); | |
| } | |
| output.writeln('}'); | |
| } | |
| } | |
| final scriptCodeMessage = scriptCodeCount == 0 | |
| ? '' | |
| : ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}'; | |
| if (countryCodeCount == 0) { | |
| if (scriptCodeCount == 0) { | |
| supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)}'); | |
| } else { | |
| supportedLocales.writeln( | |
| '/// * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})', | |
| ); | |
| } | |
| } else if (countryCodeCount == 1) { | |
| supportedLocales.writeln( | |
| '/// * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)', | |
| ); | |
| } else { | |
| supportedLocales.writeln( | |
| '/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)', | |
| ); | |
| } | |
| } | |
| // Generate the factory function. Given a Locale it returns the corresponding | |
| // base class implementation. | |
| final supportedLanguagesDeprecation = deprecatedReplacementPackage != null | |
| ? ''' | |
| @Deprecated( | |
| 'Use $supportedLanguagesConstant from $deprecatedReplacementPackage instead. ' | |
| 'This feature was deprecated after v3.47.0-0.0.pre.', | |
| ) | |
| ''' | |
| : ''; | |
| final factoryDeprecation = deprecatedReplacementPackage != null | |
| ? ''' | |
| @Deprecated( | |
| 'Use $factoryName from $deprecatedReplacementPackage instead. ' | |
| 'This feature was deprecated after v3.47.0-0.0.pre.', | |
| ) | |
| ''' | |
| : ''; | |
| output.writeln(''' | |
| /// The set of supported languages, as language code strings. | |
| /// | |
| /// The [$baseClass.delegate] can generate localizations for | |
| /// any [Locale] with a language code from this set, regardless of the region. | |
| /// Some regions have specific support (e.g. `de` covers all forms of German, | |
| /// but there is support for `de-CH` specifically to override some of the | |
| /// translations for Switzerland). | |
| /// | |
| /// See also: | |
| /// | |
| /// * [$factoryName], whose documentation describes these values. | |
| ${supportedLanguagesDeprecation}final Set<String> $supportedLanguagesConstant = HashSet<String>.from(const <String>[ | |
| ${languageCodes.map<String>((String value) => " '$value', // ${describeLocale(value)}").toList().join('\n')} | |
| ]); | |
| /// Creates a [$baseClass] instance for the given `locale`. | |
| /// | |
| /// All of the function's arguments except `locale` will be passed to the [ | |
| /// $baseClass] constructor. (The `localeName` argument of that | |
| /// constructor is specified by the actual subclass constructor by this | |
| /// function.) | |
| /// | |
| /// The following locales are supported by this package: | |
| /// | |
| /// {@template $supportedLanguagesDocMacro} | |
| $supportedLocales/// {@endtemplate} | |
| /// | |
| /// Generally speaking, this method is only intended to be used by | |
| /// [$baseClass.delegate]. | |
| $factoryDeprecation$factoryDeclaration | |
| switch (locale.languageCode) {'''); | |
| for (final String language in languageToLocales.keys) { | |
| // Only one instance of the language. | |
| if (languageToLocales[language]!.length == 1) { | |
| output.writeln( | |
| ''' | |
| case '$language': | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${languageToLocales[language]![0].camelCase()}($factoryArguments);''', | |
| ); | |
| } else if (!languageToScriptCodes.containsKey(language)) { | |
| // Does not distinguish between scripts. Switch on countryCode directly. | |
| output.writeln(''' | |
| case '$language': { | |
| switch (locale.countryCode) {'''); | |
| for (final LocaleInfo locale in languageToLocales[language]!) { | |
| if (locale.originalString == language) { | |
| continue; | |
| } | |
| assert(locale.length > 1); | |
| final String countryCode = locale.countryCode!; | |
| output.writeln( | |
| ''' | |
| case '$countryCode': | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''', | |
| ); | |
| } | |
| output.writeln(''' | |
| } | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments); | |
| }'''); | |
| } else { | |
| // Language has scriptCode, add additional switch logic. | |
| var hasCountryCode = false; | |
| output.writeln(''' | |
| case '$language': { | |
| switch (locale.scriptCode) {'''); | |
| for (final String scriptCode in languageToScriptCodes[language]!) { | |
| final scriptLocale = LocaleInfo.fromString('${language}_$scriptCode'); | |
| output.writeln(''' | |
| case '$scriptCode': {'''); | |
| if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) { | |
| output.writeln(''' | |
| switch (locale.countryCode) {'''); | |
| for (final LocaleInfo locale in languageToLocales[language]!) { | |
| if (locale.countryCode == null) { | |
| continue; | |
| } else { | |
| hasCountryCode = true; | |
| } | |
| if (locale.originalString == language) { | |
| continue; | |
| } | |
| if (locale.scriptCode != scriptCode && locale.scriptCode != null) { | |
| continue; | |
| } | |
| final String countryCode = locale.countryCode!; | |
| output.writeln( | |
| ''' | |
| case '$countryCode': | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''', | |
| ); | |
| } | |
| } | |
| // Return a fallback locale that matches scriptCode, but not countryCode. | |
| // | |
| // Explicitly defined scriptCode fallback: | |
| if (languageToLocales[language]!.contains(scriptLocale)) { | |
| if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) { | |
| output.writeln(''' | |
| }'''); | |
| } | |
| output.writeln(''' | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments); | |
| }'''); | |
| } else { | |
| // Not Explicitly defined, fallback to first locale with the same language and | |
| // script: | |
| for (final LocaleInfo locale in languageToLocales[language]!) { | |
| if (locale.scriptCode != scriptCode) { | |
| continue; | |
| } | |
| if (languageAndScriptToCountryCodes.containsKey(scriptLocale)) { | |
| output.writeln(''' | |
| }'''); | |
| } | |
| output.writeln(''' | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments); | |
| }'''); | |
| break; | |
| } | |
| } | |
| } | |
| output.writeln(''' | |
| }'''); | |
| if (hasCountryCode) { | |
| output.writeln(''' | |
| switch (locale.countryCode) {'''); | |
| for (final LocaleInfo locale in languageToLocales[language]!) { | |
| if (locale.originalString == language) { | |
| continue; | |
| } | |
| assert(locale.length > 1); | |
| if (locale.countryCode == null) { | |
| continue; | |
| } | |
| final String countryCode = locale.countryCode!; | |
| output.writeln( | |
| ''' | |
| case '$countryCode': | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''', | |
| ); | |
| } | |
| output.writeln(''' | |
| }'''); | |
| } | |
| output.writeln(''' | |
| return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments); | |
| }'''); | |
| } | |
| } | |
| output.writeln(''' | |
| } | |
| assert(false, '$factoryName() called for unsupported locale "\$locale"'); | |
| return null; | |
| }'''); | |
| return output.toString(); | |
| } | |
| /// Returns the appropriate type for getters with the given attributes. | |
| /// | |
| /// Typically "String", but some (e.g. "timeOfDayFormat") return enums. | |
| /// | |
| /// Used by [generateGetter] below. | |
| String generateType(Map<String, dynamic>? attributes) { | |
| final bool optional = attributes?.containsKey('optional') ?? false; | |
| final String type = switch (attributes?['x-flutter-type']) { | |
| 'icuShortTimePattern' => 'TimeOfDayFormat', | |
| 'scriptCategory' => 'ScriptCategory', | |
| _ => 'String', | |
| }; | |
| return type + (optional ? '?' : ''); | |
| } | |
| /// Returns the appropriate name for getters with the given attributes. | |
| /// | |
| /// Typically this is the key unmodified, but some have parameters, and | |
| /// the GlobalMaterialLocalizations class does the substitution, and for | |
| /// those we have to therefore provide an alternate name. | |
| /// | |
| /// Used by [generateGetter] below. | |
| String generateKey(String key, Map<String, dynamic>? attributes) { | |
| if (attributes != null) { | |
| if (attributes.containsKey('parameters')) { | |
| return '${key}Raw'; | |
| } | |
| switch (attributes['x-flutter-type'] as String?) { | |
| case 'icuShortTimePattern': | |
| return '${key}Raw'; | |
| } | |
| } | |
| if (key == 'datePickerDateOrder') { | |
| return 'datePickerDateOrderString'; | |
| } | |
| if (key == 'datePickerDateTimeOrder') { | |
| return 'datePickerDateTimeOrderString'; | |
| } | |
| return key; | |
| } | |
| const Map<String, String> _icuTimeOfDayToEnum = <String, String>{ | |
| 'HH:mm': 'TimeOfDayFormat.HH_colon_mm', | |
| 'HH.mm': 'TimeOfDayFormat.HH_dot_mm', | |
| "HH 'h' mm": 'TimeOfDayFormat.frenchCanadian', | |
| 'HH:mm น.': 'TimeOfDayFormat.HH_colon_mm', | |
| 'H:mm': 'TimeOfDayFormat.H_colon_mm', | |
| 'h:mm a': 'TimeOfDayFormat.h_colon_mm_space_a', | |
| 'a h:mm': 'TimeOfDayFormat.a_space_h_colon_mm', | |
| 'ah:mm': 'TimeOfDayFormat.a_space_h_colon_mm', | |
| }; | |
| const Map<String, String> _scriptCategoryToEnum = <String, String>{ | |
| 'English-like': 'ScriptCategory.englishLike', | |
| 'dense': 'ScriptCategory.dense', | |
| 'tall': 'ScriptCategory.tall', | |
| }; | |
| /// Returns the literal that describes the value returned by getters | |
| /// with the given attributes. | |
| /// | |
| /// This handles cases like the value being a literal `null`, an enum, and so | |
| /// on. The default is to treat the value as a string and escape it and quote | |
| /// it. | |
| /// | |
| /// Used by [generateGetter] below. | |
| String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInfo locale) { | |
| if (value == null) { | |
| return null; | |
| } | |
| // cupertino_en.arb doesn't use x-flutter-type. | |
| if (attributes != null) { | |
| switch (attributes['x-flutter-type'] as String?) { | |
| case 'icuShortTimePattern': | |
| if (!_icuTimeOfDayToEnum.containsKey(value)) { | |
| throw Exception( | |
| '"$value" is not one of the ICU short time patterns supported ' | |
| 'by the material library. Here is the list of supported ' | |
| 'patterns:\n ${_icuTimeOfDayToEnum.keys.join('\n ')}', | |
| ); | |
| } | |
| return _icuTimeOfDayToEnum[value]; | |
| case 'scriptCategory': | |
| if (!_scriptCategoryToEnum.containsKey(value)) { | |
| throw Exception( | |
| '"$value" is not one of the scriptCategory values supported ' | |
| 'by the material library. Here is the list of supported ' | |
| 'values:\n ${_scriptCategoryToEnum.keys.join('\n ')}', | |
| ); | |
| } | |
| return _scriptCategoryToEnum[value]; | |
| } | |
| } | |
| return generateEncodedString(locale.languageCode, value); | |
| } | |
| /// Combines [generateType], [generateKey], and [generateValue] to return | |
| /// the source of getters for the GlobalMaterialLocalizations subclass. | |
| /// The locale is the locale for which the getter is being generated. | |
| String generateGetter( | |
| String key, | |
| String? value, | |
| Map<String, dynamic>? attributes, | |
| LocaleInfo locale, | |
| ) { | |
| final String type = generateType(attributes); | |
| key = generateKey(key, attributes); | |
| final String? generatedValue = generateValue(value, attributes, locale); | |
| return ''' | |
| @override | |
| $type get $key => $generatedValue;'''; | |
| } | |
| void main(List<String> rawArgs) { | |
| checkCwdIsRepoRoot('gen_localizations'); | |
| final GeneratorOptions options = parseArgs(rawArgs); | |
| // filenames are assumed to end in "prefix_lc.arb" or "prefix_lc_cc.arb", where prefix | |
| // is the 2nd command line argument, lc is a language code and cc is the country | |
| // code. In most cases both codes are just two characters. | |
| final directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n')); | |
| final widgetsFilenameRE = RegExp(r'widgets_(\w+)\.arb$'); | |
| final materialFilenameRE = RegExp(r'material_(\w+)\.arb$'); | |
| final cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$'); | |
| try { | |
| validateEnglishLocalizations(File(path.join(directory.path, 'widgets_en.arb'))); | |
| validateEnglishLocalizations(File(path.join(directory.path, 'material_en.arb'))); | |
| validateEnglishLocalizations(File(path.join(directory.path, 'cupertino_en.arb'))); | |
| } on ValidationError catch (exception) { | |
| exitWithError('$exception'); | |
| } | |
| // Only rewrite material_kn.arb and cupertino_en.arb if overwriting the | |
| // Material and Cupertino localizations files. | |
| if (options.writeToFile) { | |
| // Encodes the material_kn.arb file and the cupertino_en.arb files before | |
| // generating localizations. This prevents a subset of Emacs users from | |
| // crashing when opening up the Flutter source code. | |
| // See https://github.com/flutter/flutter/issues/36704 for more context. | |
| encodeKnArbFiles(directory); | |
| } | |
| precacheLanguageAndRegionTags(); | |
| // Maps of locales to resource key/value pairs for Widgets ARBs. | |
| final widgetsLocaleToResources = <LocaleInfo, Map<String, String>>{}; | |
| // Maps of locales to resource key/attributes pairs for Widgets ARBs. | |
| // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes | |
| final widgetsLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{}; | |
| // Maps of locales to resource key/value pairs for Material ARBs. | |
| final materialLocaleToResources = <LocaleInfo, Map<String, String>>{}; | |
| // Maps of locales to resource key/attributes pairs for Material ARBs. | |
| // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes | |
| final materialLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{}; | |
| // Maps of locales to resource key/value pairs for Cupertino ARBs. | |
| final cupertinoLocaleToResources = <LocaleInfo, Map<String, String>>{}; | |
| // Maps of locales to resource key/attributes pairs for Cupertino ARBs. | |
| // https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes | |
| final cupertinoLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{}; | |
| loadMatchingArbsIntoBundleMaps( | |
| directory: directory, | |
| filenamePattern: widgetsFilenameRE, | |
| localeToResources: widgetsLocaleToResources, | |
| localeToResourceAttributes: widgetsLocaleToResourceAttributes, | |
| ); | |
| loadMatchingArbsIntoBundleMaps( | |
| directory: directory, | |
| filenamePattern: materialFilenameRE, | |
| localeToResources: materialLocaleToResources, | |
| localeToResourceAttributes: materialLocaleToResourceAttributes, | |
| ); | |
| loadMatchingArbsIntoBundleMaps( | |
| directory: directory, | |
| filenamePattern: cupertinoFilenameRE, | |
| localeToResources: cupertinoLocaleToResources, | |
| localeToResourceAttributes: cupertinoLocaleToResourceAttributes, | |
| ); | |
| try { | |
| validateLocalizations( | |
| widgetsLocaleToResources, | |
| widgetsLocaleToResourceAttributes, | |
| removeUndefined: options.removeUndefined, | |
| ); | |
| validateLocalizations( | |
| materialLocaleToResources, | |
| materialLocaleToResourceAttributes, | |
| removeUndefined: options.removeUndefined, | |
| ); | |
| validateLocalizations( | |
| cupertinoLocaleToResources, | |
| cupertinoLocaleToResourceAttributes, | |
| removeUndefined: options.removeUndefined, | |
| ); | |
| } on ValidationError catch (exception) { | |
| exitWithError('$exception'); | |
| } | |
| if (options.removeUndefined) { | |
| removeUndefinedLocalizations(widgetsLocaleToResources); | |
| removeUndefinedLocalizations(materialLocaleToResources); | |
| removeUndefinedLocalizations(cupertinoLocaleToResources); | |
| } | |
| final String? widgetsLocalizations = options.writeToFile || !options.cupertinoOnly | |
| ? generateArbBasedLocalizationSubclasses( | |
| localeToResources: widgetsLocaleToResources, | |
| localeToResourceAttributes: widgetsLocaleToResourceAttributes, | |
| generatedClassPrefix: 'WidgetsLocalization', | |
| baseClass: 'GlobalWidgetsLocalizations', | |
| generateHeader: generateWidgetsHeader, | |
| generateConstructor: generateWidgetsConstructor, | |
| generateConstructorForCountrySubClass: generateWidgetsConstructorForCountrySubclass, | |
| factoryName: widgetsFactoryName, | |
| factoryDeclaration: widgetsFactoryDeclaration, | |
| callsFactoryWithConst: true, | |
| factoryArguments: widgetsFactoryArguments, | |
| supportedLanguagesConstant: widgetsSupportedLanguagesConstant, | |
| supportedLanguagesDocMacro: widgetsSupportedLanguagesDocMacro, | |
| ) | |
| : null; | |
| final materialGeneratedClasses = <String>{}; | |
| final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly | |
| ? generateArbBasedLocalizationSubclasses( | |
| localeToResources: materialLocaleToResources, | |
| localeToResourceAttributes: materialLocaleToResourceAttributes, | |
| generatedClassPrefix: 'MaterialLocalization', | |
| baseClass: 'GlobalMaterialLocalizations', | |
| generateHeader: generateMaterialHeader, | |
| generateConstructor: generateMaterialConstructor, | |
| factoryName: materialFactoryName, | |
| factoryDeclaration: materialFactoryDeclaration, | |
| callsFactoryWithConst: false, | |
| factoryArguments: materialFactoryArguments, | |
| supportedLanguagesConstant: materialSupportedLanguagesConstant, | |
| supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro, | |
| deprecatedReplacementPackage: 'package:material_ui/material_ui.dart', | |
| generatedClassNames: materialGeneratedClasses, | |
| ) | |
| : null; | |
| final cupertinoGeneratedClasses = <String>{}; | |
| final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly | |
| ? generateArbBasedLocalizationSubclasses( | |
| localeToResources: cupertinoLocaleToResources, | |
| localeToResourceAttributes: cupertinoLocaleToResourceAttributes, | |
| generatedClassPrefix: 'CupertinoLocalization', | |
| baseClass: 'GlobalCupertinoLocalizations', | |
| generateHeader: generateCupertinoHeader, | |
| generateConstructor: generateCupertinoConstructor, | |
| factoryName: cupertinoFactoryName, | |
| factoryDeclaration: cupertinoFactoryDeclaration, | |
| callsFactoryWithConst: false, | |
| factoryArguments: cupertinoFactoryArguments, | |
| supportedLanguagesConstant: cupertinoSupportedLanguagesConstant, | |
| supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro, | |
| deprecatedReplacementPackage: 'package:cupertino_ui/cupertino_ui.dart', | |
| generatedClassNames: cupertinoGeneratedClasses, | |
| ) | |
| : null; | |
| if (options.writeToFile) { | |
| final widgetsLocalizationsFile = File( | |
| path.join(directory.path, 'generated_widgets_localizations.dart'), | |
| ); | |
| widgetsLocalizationsFile.writeAsStringSync(widgetsLocalizations!, flush: true); | |
| final materialLocalizationsFile = File( | |
| path.join(directory.path, 'generated_material_localizations.dart'), | |
| ); | |
| materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true); | |
| final cupertinoLocalizationsFile = File( | |
| path.join(directory.path, 'generated_cupertino_localizations.dart'), | |
| ); | |
| cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true); | |
| final fixDataDir = Directory(path.join(directory.parent.parent.path, 'fix_data')); | |
| fixDataDir.createSync(recursive: true); | |
| File(path.join(fixDataDir.path, 'fix_material_localizations.yaml')).writeAsStringSync( | |
| generateLocalizationFixData( | |
| baseClass: 'GlobalMaterialLocalizations', | |
| supportedLanguagesConstant: materialSupportedLanguagesConstant, | |
| factoryName: materialFactoryName, | |
| generatedClassNames: materialGeneratedClasses, | |
| replacementPackageUri: 'package:material_ui/material_ui.dart', | |
| ), | |
| flush: true, | |
| ); | |
| File(path.join(fixDataDir.path, 'fix_cupertino_localizations.yaml')).writeAsStringSync( | |
| generateLocalizationFixData( | |
| baseClass: 'GlobalCupertinoLocalizations', | |
| supportedLanguagesConstant: cupertinoSupportedLanguagesConstant, | |
| factoryName: cupertinoFactoryName, | |
| generatedClassNames: cupertinoGeneratedClasses, | |
| replacementPackageUri: 'package:cupertino_ui/cupertino_ui.dart', | |
| ), | |
| flush: true, | |
| ); | |
| } else { | |
| if (options.cupertinoOnly) { | |
| stdout.write(cupertinoLocalizations); | |
| } else if (options.materialOnly) { | |
| stdout.write(materialLocalizations); | |
| } else if (options.widgetsOnly) { | |
| stdout.write(widgetsLocalizations); | |
| } else { | |
| stdout.write(widgetsLocalizations); | |
| stdout.write(materialLocalizations); | |
| stdout.write(cupertinoLocalizations); | |
| } | |
| } | |
| } | |
| String generateLocalizationFixData({ | |
| required String baseClass, | |
| required String supportedLanguagesConstant, | |
| required String factoryName, | |
| required Set<String> generatedClassNames, | |
| required String replacementPackageUri, | |
| }) { | |
| final buffer = StringBuffer(''' | |
| # Copyright 2014 The Flutter Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # THIS FILE IS GENERATED. DO NOT EDIT BY HAND. | |
| # | |
| # This file is generated by: | |
| # dart dev/tools/localization/bin/gen_localizations.dart --overwrite | |
| # For details regarding the *Flutter Fix* feature, see | |
| # https://flutter.dev/to/flutter-fix | |
| # Every fix must be tested. See the | |
| # flutter/packages/flutter_localizations/test_fixes/README.md file for | |
| # instructions on testing these data driven fixes. | |
| # For documentation about this file format, see | |
| # https://dart.dev/go/data-driven-fixes. | |
| version: 1 | |
| transforms: | |
| - title: "Migrate to '$replacementPackageUri'" | |
| date: 2026-09-17 | |
| element: | |
| uris: ['flutter_localizations.dart'] | |
| class: '$baseClass' | |
| changes: | |
| - kind: 'replacedBy' | |
| newElement: | |
| uris: ['$replacementPackageUri'] | |
| class: '$baseClass' | |
| - title: "Migrate to '$replacementPackageUri'" | |
| date: 2026-09-17 | |
| element: | |
| uris: ['flutter_localizations.dart'] | |
| variable: '$supportedLanguagesConstant' | |
| changes: | |
| - kind: 'replacedBy' | |
| newElement: | |
| uris: ['$replacementPackageUri'] | |
| variable: '$supportedLanguagesConstant' | |
| - title: "Migrate to '$replacementPackageUri'" | |
| date: 2026-09-17 | |
| element: | |
| uris: ['flutter_localizations.dart'] | |
| function: '$factoryName' | |
| changes: | |
| - kind: 'replacedBy' | |
| newElement: | |
| uris: ['$replacementPackageUri'] | |
| function: '$factoryName' | |
| '''); | |
| for (final className in generatedClassNames) { | |
| buffer.write(''' | |
| - title: "Migrate to '$replacementPackageUri'" | |
| date: 2026-09-17 | |
| element: | |
| uris: ['flutter_localizations.dart'] | |
| class: '$className' | |
| changes: | |
| - kind: 'replacedBy' | |
| newElement: | |
| uris: ['$replacementPackageUri'] | |
| class: '$className' | |
| '''); | |
| } | |
| return buffer.toString(); | |
| } |