Generate message lookup in gen_l10n (#50733)
diff --git a/dev/tools/localization/bin/gen_l10n.dart b/dev/tools/localization/bin/gen_l10n.dart index 1c61a95..9bfcd79 100644 --- a/dev/tools/localization/bin/gen_l10n.dart +++ b/dev/tools/localization/bin/gen_l10n.dart
@@ -63,9 +63,8 @@ exit(0); } - final String flutterRoot = Platform.environment['FLUTTER_ROOT']; - final String flutterBin = Platform.isWindows ? 'flutter.bat' : 'flutter'; - final String flutterPath = flutterRoot == null ? flutterBin : path.join(flutterRoot, 'bin', flutterBin); + await precacheLanguageAndRegionTags(); + final String arbPathString = results['arb-dir'] as String; final String outputFileString = results['output-localization-file'] as String; final String templateArbFileName = results['template-arb-file'] as String; @@ -74,6 +73,7 @@ const local.LocalFileSystem fs = local.LocalFileSystem(); final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs); + try { localizationsGenerator ..initialize( @@ -83,9 +83,8 @@ classNameString: classNameString, preferredSupportedLocaleString: preferredSupportedLocaleString, ) - ..parseArbFiles() - ..generateClassMethods() - ..generateOutputFile(); + ..loadResources() + ..writeOutputFile(); } on FileSystemException catch (e) { exitWithError(e.message); } on FormatException catch (e) { @@ -93,24 +92,4 @@ } on L10nException catch (e) { exitWithError(e.message); } - - final ProcessResult pubGetResult = await Process.run(flutterPath, <String>['pub', 'get']); - if (pubGetResult.exitCode != 0) { - stderr.write(pubGetResult.stderr); - exit(1); - } - - final ProcessResult generateFromArbResult = await Process.run(flutterPath, <String>[ - 'pub', - 'run', - 'intl_translation:generate_from_arb', - '--output-dir=${localizationsGenerator.l10nDirectory.path}', - '--no-use-deferred-loading', - localizationsGenerator.outputFile.path, - ...localizationsGenerator.arbPathStrings, - ]); - if (generateFromArbResult.exitCode != 0) { - stderr.write(generateFromArbResult.stderr); - exit(1); - } }
diff --git a/dev/tools/localization/gen_l10n.dart b/dev/tools/localization/gen_l10n.dart index 751ba61..2102947 100644 --- a/dev/tools/localization/gen_l10n.dart +++ b/dev/tools/localization/gen_l10n.dart
@@ -13,255 +13,78 @@ import 'gen_l10n_types.dart'; import 'localizations_utils.dart'; -// The set of date formats that can be automatically localized. -// -// The localizations generation tool makes use of the intl library's -// DateFormat class to properly format dates based on the locale, the -// desired format, as well as the passed in [DateTime]. For example, using -// DateFormat.yMMMMd("en_US").format(DateTime.utc(1996, 7, 10)) results -// in the string "July 10, 1996". -// -// Since the tool generates code that uses DateFormat's constructor, it is -// necessary to verify that the constructor exists, or the -// tool will generate code that may cause a compile-time error. -// -// See also: -// -// * <https://pub.dev/packages/intl> -// * <https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html> -// * <https://api.dartlang.org/stable/2.7.0/dart-core/DateTime-class.html> -const Set<String> allowableDateFormats = <String>{ - 'd', - 'E', - 'EEEE', - 'LLL', - 'LLLL', - 'M', - 'Md', - 'MEd', - 'MMM', - 'MMMd', - 'MMMEd', - 'MMMM', - 'MMMMd', - 'MMMMEEEEd', - 'QQQ', - 'QQQQ', - 'y', - 'yM', - 'yMd', - 'yMEd', - 'yMMM', - 'yMMMd', - 'yMMMEd', - 'yMMMM', - 'yMMMMd', - 'yMMMMEEEEd', - 'yQQQ', - 'yQQQQ', - 'H', - 'Hm', - 'Hms', - 'j', - 'jm', - 'jms', - 'jmv', - 'jmz', - 'jv', - 'jz', - 'm', - 'ms', - 's', -}; - -// The set of number formats that can be automatically localized. -// -// The localizations generation tool makes use of the intl library's -// NumberFormat class to properly format numbers based on the locale, the -// desired format, as well as the passed in number. For example, using -// DateFormat.compactLong("en_US").format(1200000) results -// in the string "1.2 million". -// -// Since the tool generates code that uses NumberFormat's constructor, it is -// necessary to verify that the constructor exists, or the -// tool will generate code that may cause a compile-time error. -// -// See also: -// -// * <https://pub.dev/packages/intl> -// * <https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html> -const Set<String> allowableNumberFormats = <String>{ - 'compact', - 'compactCurrency', - 'compactSimpleCurrency', - 'compactLong', - 'currency', - 'decimalPattern', - 'decimalPercentPattern', - 'percentPattern', - 'scientificPattern', - 'simpleCurrency', -}; - -// The names of the NumberFormat factory constructors which have named -// parameters rather than positional parameters. -// -// This helps the tool correctly generate number formmatting code correctly. -// -// Example of code that uses named parameters: -// final NumberFormat format = NumberFormat.compact( -// locale: _localeName, -// ); -// -// Example of code that uses positional parameters: -// final NumberFormat format = NumberFormat.scientificPattern(_localeName); -const Set<String> numberFormatsWithNamedParameters = <String>{ - 'compact', - 'compactCurrency', - 'compactSimpleCurrency', - 'compactLong', - 'currency', - 'decimalPercentPattern', - 'simpleCurrency', -}; - -List<String> generateIntlMethodArgs(Message message) { - final List<String> methodArgs = <String>["name: '${message.resourceId}'"]; - if (message.description != null) - methodArgs.add('desc: ${generateString(message.description)}'); - if (message.placeholders.isNotEmpty) { - final String args = message.placeholders.map<String>((Placeholder placeholder) { - return placeholder.name; - }).join(', '); - methodArgs.add('args: <Object>[$args]'); - } - return methodArgs; -} - -List<String> generateInnerMethodArgs(Message message) { +List<String> generateMethodParameters(Message message) { + assert(message.placeholders.isNotEmpty); + final Placeholder countPlaceholder = message.isPlural ? message.getCountPlaceholder() : null; return message.placeholders.map((Placeholder placeholder) { - final String arg = placeholder.name; - return placeholder.requiresFormatting ? '${arg}String' : arg; + final String type = placeholder == countPlaceholder ? 'int' : placeholder.type; + return '$type ${placeholder.name}'; }).toList(); } String generateDateFormattingLogic(Message message) { - if (message.placeholders.isEmpty) - return ''; + if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) + return '@(none)'; - final StringBuffer result = StringBuffer(); - for (final Placeholder placeholder in message.placeholders) { - if (!placeholder.isDate) - continue; - 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 (!allowableDateFormats.contains(placeholder.format)) { - 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.' - ); - } - result.write(''' + 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); + }); - final DateFormat ${placeholder.name}DateFormat = DateFormat.${placeholder.format}(_localeName); - final String ${placeholder.name}String = ${placeholder.name}DateFormat.format(${placeholder.name}); -'''); - } - return result.toString(); + return formatStatements.isEmpty ? '@(none)' : formatStatements.join(''); } String generateNumberFormattingLogic(Message message) { - if (message.placeholders.isEmpty) - return ''; + if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) { + return '@(none)'; + } - final StringBuffer result = StringBuffer(); - for (final Placeholder placeholder in message.placeholders) { - if (!placeholder.isNumber) - continue; - if (!allowableNumberFormats.contains(placeholder.format)) { - 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> 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}'; + }, ); - } - if (numberFormatsWithNamedParameters.contains(placeholder.format)) { - final StringBuffer optionalParametersString = StringBuffer(); - for (final OptionalParameter parameter in placeholder.optionalParameters) - optionalParametersString.write('\n ${parameter.name}: ${parameter.value},'); - result.write(''' + return numberFormatTemplate + .replaceAll('@(placeholder)', placeholder.name) + .replaceAll('@(format)', placeholder.format) + .replaceAll('@(parameters)', parameters.join(', \n')); + }); - final NumberFormat ${placeholder.name}NumberFormat = NumberFormat.${placeholder.format}( - locale: _localeName,${optionalParametersString.toString()} - ); - final String ${placeholder.name}String = ${placeholder.name}NumberFormat.format(${placeholder.name}); -'''); - - } else { - result.write(''' - - final NumberFormat ${placeholder.name}NumberFormat = NumberFormat.${placeholder.format}(_localeName); - final String ${placeholder.name}String = ${placeholder.name}NumberFormat.format(${placeholder.name}); -'''); - } - } - return result.toString(); + return formatStatements.isEmpty ? '@(none)' : formatStatements.join(''); } -String genSimpleMethod(Message message) { - String genSimpleMethodMessage() { - String messageValue = message.value; - for (final Placeholder placeholder in message.placeholders) { - messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}}'); - } - final String generatedMessage = generateString(messageValue); // "r'...'" - return generatedMessage.startsWith('r') ? generatedMessage.substring(1) : generatedMessage; - } - - List<String> genMethodParameters([String type]) { - return message.placeholders.map((Placeholder placeholder) { - return '${type ?? placeholder.type} ${placeholder.name}'; - }).toList(); - } - - if (message.placeholdersRequireFormatting) { - return formatMethodTemplate - .replaceAll('@(methodName)', message.resourceId) - .replaceAll('@(methodParameters)', genMethodParameters().join(', ')) - .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) - .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) - .replaceAll('@(message)', genSimpleMethodMessage()) - .replaceAll('@(innerMethodParameters)', genMethodParameters('Object').join(', ')) - .replaceAll('@(innerMethodArgs)', generateInnerMethodArgs(message).join(', ')) - .replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n ')); - } - - if (message.placeholders.isNotEmpty) { - return simpleMethodTemplate - .replaceAll('@(methodName)', message.resourceId) - .replaceAll('@(methodParameters)', genMethodParameters().join(', ')) - .replaceAll('@(message)', genSimpleMethodMessage()) - .replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n ')); - } - - return getterMethodTemplate - .replaceAll('@(methodName)', message.resourceId) - .replaceAll('@(message)', genSimpleMethodMessage()) - .replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n ')); -} - - String generatePluralMethod(Message message) { if (message.placeholders.isEmpty) { throw L10nException( @@ -277,15 +100,6 @@ for (final Placeholder placeholder in message.placeholders) easyMessage = easyMessage.replaceAll('{${placeholder.name}}', '#${placeholder.name}#'); - const Map<String, String> pluralIds = <String, String>{ - '=0': 'zero', - '=1': 'one', - '=2': 'two', - 'few': 'few', - 'many': 'many', - 'other': 'other' - }; - final Placeholder countPlaceholder = message.getCountPlaceholder(); if (countPlaceholder == null) { throw L10nException( @@ -295,67 +109,154 @@ ); } - final List<String> intlMethodArgs = <String>[ - countPlaceholder.name, - 'locale: _localeName', - ...generateIntlMethodArgs(message), - ]; + const Map<String, String> pluralIds = <String, String>{ + '=0': 'zero', + '=1': 'one', + '=2': 'two', + 'few': 'few', + 'many': 'many', + 'other': 'other' + }; + final List<String> pluralLogicArgs = <String>[]; for (final String pluralKey in pluralIds.keys) { - final RegExp expRE = RegExp('($pluralKey){([^}]+)}'); + final RegExp expRE = RegExp('($pluralKey)\\s*{([^}]+)}'); final RegExpMatch match = expRE.firstMatch(easyMessage); if (match != null && match.groupCount == 2) { String argValue = match.group(2); for (final Placeholder placeholder in message.placeholders) { - if (placeholder.requiresFormatting) { + if (placeholder != countPlaceholder && placeholder.requiresFormatting) { argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}'); } else { argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}'); } } - intlMethodArgs.add("${pluralIds[pluralKey]}: '$argValue'"); + pluralLogicArgs.add(" ${pluralIds[pluralKey]}: '$argValue'"); } } - List<String> generatePluralMethodParameters([String type]) { - return message.placeholders.map((Placeholder placeholder) { - final String placeholderType = placeholder == countPlaceholder ? 'int' : (type ?? placeholder.type); - return '$placeholderType ${placeholder.name}'; - }).toList(); + 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() { + String messageValue = bundle.translationFor(message); + 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}}'); + } + } + return "'$messageValue'"; + } + + if (message.isPlural) { + return generatePluralMethod(message); } if (message.placeholdersRequireFormatting) { - return pluralFormatMethodTemplate - .replaceAll('@(methodName)', message.resourceId) - .replaceAll('@(methodParameters)', generatePluralMethodParameters().join(', ')) + return formatMethodTemplate + .replaceAll('@(name)', message.resourceId) + .replaceAll('@(parameters)', generateMethodParameters(message).join(', ')) .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) - .replaceAll('@(innerMethodParameters)', generatePluralMethodParameters('Object').join(', ')) - .replaceAll('@(innerMethodArgs)', generateInnerMethodArgs(message).join(', ')) - .replaceAll('@(intlMethodArgs)', intlMethodArgs.join(',\n ')); + .replaceAll('@(message)', generateMessage()) + .replaceAll('@(none)\n', ''); } - return pluralMethodTemplate - .replaceAll('@(methodName)', message.resourceId) - .replaceAll('@(methodParameters)', generatePluralMethodParameters().join(', ')) - .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) - .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) - .replaceAll('@(intlMethodArgs)', intlMethodArgs.join(',\n ')); + 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()); } -/// The localizations generation class used to generate the localizations -/// classes, as well as all pertinent Dart files required to internationalize a -/// Flutter application. +String generateClass(String className, AppResourceBundle bundle, Iterable<Message> messages) { + final LocaleInfo locale = bundle.locale; + + String baseClassName = className; + if (locale.countryCode != null) { + baseClassName = '$className${LocaleInfo.fromString(locale.languageCode).camelCase()}'; + } + + final Iterable<String> methods = messages + .where((Message message) => bundle.translationFor(message) != null) + .map((Message message) => generateMethod(message, bundle)); + + return classTemplate + .replaceAll('@(language)', describeLocale(locale.toString())) + .replaceAll('@(baseClass)', baseClassName) + .replaceAll('@(class)', '$className${locale.camelCase()}') + .replaceAll('@(localeName)', locale.toString()) + .replaceAll('@(methods)', methods.join('\n\n')); +} + +String generateBaseClassMethod(Message message) { + final String comment = message.description ?? 'No description provided in @${message.resourceId}'; + if (message.placeholders.isNotEmpty) { + return baseClassMethodTemplate + .replaceAll('@(comment)', comment) + .replaceAll('@(name)', message.resourceId) + .replaceAll('@(parameters)', generateMethodParameters(message).join(', ')); + } + return baseClassGetterTemplate + .replaceAll('@(comment)', comment) + .replaceAll('@(name)', message.resourceId); +} + +String generateLookupBody(AppResourceBundleCollection allBundles, String className) { + final Iterable<String> switchClauses = allBundles.languages.map((String language) { + final Iterable<LocaleInfo> locales = allBundles.localesForLanguage(language); + if (locales.length == 1) { + return switchClauseTemplate + .replaceAll('@(case)', language) + .replaceAll('@(class)', '$className${locales.first.camelCase()}'); + } + + final Iterable<LocaleInfo> localesWithCountryCodes = locales.where((LocaleInfo locale) => locale.countryCode != null); + return countryCodeSwitchTemplate + .replaceAll('@(languageCode)', language) + .replaceAll('@(class)', '$className${LocaleInfo.fromString(language).camelCase()}') + .replaceAll('@(switchClauses)', localesWithCountryCodes.map((LocaleInfo locale) { + return switchClauseTemplate + .replaceAll('@(case)', locale.countryCode) + .replaceAll('@(class)', '$className${locale.camelCase()}'); + }).join('\n ')); + }); + return switchClauses.join('\n '); +} + 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); - static RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$'); - static RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$'); - final file.FileSystem _fs; + Iterable<Message> _allMessages; + AppResourceBundleCollection _allBundles; + /// The reference to the project's l10n directory. /// @@ -404,7 +305,9 @@ List<LocaleInfo> _preferredSupportedLocales; /// The list of all arb path strings in [l10nDirectory]. - final List<String> arbPathStrings = <String>[]; + List<String> get arbPathStrings { + return _allBundles.bundles.map((AppResourceBundle bundle) => bundle.file.path).toList(); + } /// The supported language codes as found in the arb files located in /// [l10nDirectory]. @@ -414,10 +317,6 @@ /// [l10nDirectory]. final Set<LocaleInfo> supportedLocales = <LocaleInfo>{}; - /// The class methods that will be generated in the localizations class - /// based on messages found in the template arb file. - final List<String> classMethods = <String>[]; - /// Initializes [l10nDirectory], [templateArbFile], [outputFile] and [className]. /// /// Throws an [L10nException] when a provided configuration is not allowed @@ -531,7 +430,9 @@ /// will take priority over the other locales. @visibleForTesting void setPreferredSupportedLocales(String inputLocales) { - if (inputLocales != null) { + if (inputLocales == null || inputLocales.trim().isEmpty) { + _preferredSupportedLocales = const <LocaleInfo>[]; + } else { final List<dynamic> preferredLocalesStringList = json.decode(inputLocales) as List<dynamic>; _preferredSupportedLocales = preferredLocalesStringList.map((dynamic localeString) { if (localeString.runtimeType != String) { @@ -542,70 +443,6 @@ } } - /// Scans [l10nDirectory] for arb files and parses them for language and locale - /// information. - void parseArbFiles() { - final List<File> fileSystemEntityList = l10nDirectory - .listSync() - .whereType<File>() - .toList(); - final List<LocaleInfo> localeInfoList = <LocaleInfo>[]; - - for (final File file in fileSystemEntityList) { - final String filePath = file.path; - if (arbFilenameRE.hasMatch(filePath)) { - final Map<String, dynamic> arbContents = json.decode(file.readAsStringSync()) as Map<String, dynamic>; - String localeString = arbContents['@@locale'] as String; - if (localeString == null) { - final RegExpMatch arbFileMatch = arbFilenameLocaleRE.firstMatch(filePath); - if (arbFileMatch == null) { - throw L10nException( - "The following .arb file's locale could not be determined: \n" - '$filePath \n' - "Make sure that the locale is specified in the '@@locale' " - 'property or as part of the filename (e.g. file_en.arb)' - ); - } - - localeString = arbFilenameLocaleRE.firstMatch(filePath)[1]; - } - - arbPathStrings.add(filePath); - final LocaleInfo localeInfo = LocaleInfo.fromString(localeString); - if (localeInfoList.contains(localeInfo)) - throw L10nException( - 'Multiple arb files with the same locale detected. \n' - 'Ensure that there is exactly one arb file for each locale.' - ); - localeInfoList.add(localeInfo); - } - } - - arbPathStrings.sort(); - localeInfoList.sort(); - supportedLanguageCodes.addAll(localeInfoList.map((LocaleInfo localeInfo) { - return "'${localeInfo.languageCode}'"; - })); - - if (preferredSupportedLocales != null) { - for (final LocaleInfo preferredLocale in preferredSupportedLocales) { - if (!localeInfoList.contains(preferredLocale)) { - throw L10nException( - "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 if there is no intent to support ' - 'it.' - ); - } - - localeInfoList.removeWhere((LocaleInfo localeInfo) => localeInfo == preferredLocale); - } - localeInfoList.insertAll(0, preferredSupportedLocales); - } - supportedLocales.addAll(localeInfoList); - } - static bool _isValidGetterAndMethodName(String name) { // Public Dart method name must not start with an underscore if (name[0] == '_') @@ -622,83 +459,78 @@ return true; } - static String _genSupportedLocaleProperty(Set<LocaleInfo> supportedLocales) { - const String prefix = 'static const List<Locale> supportedLocales = <Locale>[\n Locale('; - const String suffix = '),\n ];'; - - String resultingProperty = prefix; - for (final LocaleInfo locale in supportedLocales) { - final String languageCode = locale.languageCode; - final String countryCode = locale.countryCode; - - resultingProperty += "'$languageCode'"; - if (countryCode != null) - resultingProperty += ", '$countryCode'"; - resultingProperty += '),\n Locale('; - } - resultingProperty = resultingProperty.substring(0, resultingProperty.length - '),\n Locale('.length); - resultingProperty += suffix; - - return resultingProperty; - } - - /// Generates the methods for the localizations class. - /// - /// The method parses [templateArbFile] and uses its resource ids as the - /// Dart method and getter names. It then uses each resource id's - /// corresponding resource value to figure out how to define these getters. - /// - /// For example, a message with plurals will be handled differently from - /// a simple, singular message. - /// - /// 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. - /// - /// Throws a [FormatException] when parsing the arb file is unsuccessful. - void generateClassMethods() { - Map<String, dynamic> bundle; - try { - bundle = json.decode(templateArbFile.readAsStringSync()) as Map<String, dynamic>; - } on FileSystemException catch (e) { - throw FileSystemException('Unable to read input arb file: $e'); - } on FormatException catch (e) { - throw FormatException('Unable to parse arb file: $e'); - } - - final List<String> sortedArbKeys = bundle.keys.toList()..sort(); - for (final String key in sortedArbKeys) { - if (key.startsWith('@')) - continue; - if (!_isValidGetterAndMethodName(key)) { + // Load _allMessages from templateArbFile and _allBundles from all of the ARB + // files in l10nDirectory. Also initialized: supportedLocales. + void loadResources() { + final AppResourceBundle templateBundle = AppResourceBundle(templateArbFile); + _allMessages = templateBundle.resourceIds.map((String id) => Message(templateBundle.resources, id)); + for (final String resourceId in templateBundle.resourceIds) + if (!_isValidGetterAndMethodName(resourceId)) { throw L10nException( - 'Invalid key format: $key \n It has to be in camel case, cannot start ' - 'with a number or underscore, and cannot contain non-alphanumeric characters.' + '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.' ); } - final Message message = Message(bundle, key); - if (message.isPlural) - classMethods.add(generatePluralMethod(message)); - else - classMethods.add(genSimpleMethod(message)); + _allBundles = AppResourceBundleCollection(l10nDirectory); + + final List<LocaleInfo> allLocales = List<LocaleInfo>.from(_allBundles.locales); + for (final LocaleInfo preferredLocale in preferredSupportedLocales) { + final int index = allLocales.indexOf(preferredLocale); + if (index == -1) { + throw L10nException( + "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.' + ); + } + allLocales.removeAt(index); + allLocales.insertAll(0, preferredSupportedLocales); } + supportedLocales.addAll(allLocales); } - /// Generates a file that contains the localizations class and the - /// LocalizationsDelegate class. - void generateOutputFile() { + // Generate the AppLocalizations class, its LocalizationsDelegate subclass. + String generateCode() { final String directory = path.basename(l10nDirectory.path); final String outputFileName = path.basename(outputFile.path); - outputFile.writeAsStringSync( - defaultFileTemplate - .replaceAll('@(className)', className) - .replaceAll('@(classMethods)', classMethods.join('\n')) - .replaceAll('@(importFile)', '$directory/$outputFileName') - .replaceAll('@(supportedLocales)', _genSupportedLocaleProperty(supportedLocales)) - .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.toList().join(', ')) + + final Iterable<String> supportedLocalesCode = supportedLocales.map((LocaleInfo locale) { + final String country = locale.countryCode; + final String countryArg = country == null ? '' : ', $country'; + return 'Locale(\'${locale.languageCode}$countryArg\')'; + }); + + final Set<String> supportedLanguageCodes = Set<String>.from( + _allBundles.locales.map<String>((LocaleInfo locale) => '\'${locale.languageCode}\'') ); + + final StringBuffer allMessagesClasses = StringBuffer(); + final List<LocaleInfo> allLocales = _allBundles.locales.toList()..sort(); + for (final LocaleInfo locale in allLocales) { + allMessagesClasses.writeln(); + allMessagesClasses.writeln( + generateClass(className, _allBundles.bundleFor(locale), _allMessages) + ); + } + + final String lookupBody = generateLookupBody(_allBundles, className); + + return fileTemplate + .replaceAll('@(class)', className) + .replaceAll('@(methods)', _allMessages.map(generateBaseClassMethod).join('\n')) + .replaceAll('@(importFile)', '$directory/$outputFileName') + .replaceAll('@(supportedLocales)', supportedLocalesCode.join(',\n ')) + .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', ')) + .replaceAll('@(allMessagesClasses)', allMessagesClasses.toString().trim()) + .replaceAll('@(lookupName)', '_lookup$className') + .replaceAll('@(lookupBody)', lookupBody); + } + + void writeOutputFile() { + outputFile.writeAsStringSync(generateCode()); } }
diff --git a/dev/tools/localization/gen_l10n_templates.dart b/dev/tools/localization/gen_l10n_templates.dart index 4caa5ca..328be9a 100644 --- a/dev/tools/localization/gen_l10n_templates.dart +++ b/dev/tools/localization/gen_l10n_templates.dart
@@ -2,80 +2,29 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -const String getterMethodTemplate = ''' - String get @(methodName) { - return Intl.message( - @(message), - locale: _localeName, - @(intlMethodArgs) - ); - } -'''; - -const String simpleMethodTemplate = ''' - String @(methodName)(@(methodParameters)) { - return Intl.message( - @(message), - locale: _localeName, - @(intlMethodArgs) - ); - } -'''; - -const String formatMethodTemplate = ''' - String @(methodName)(@(methodParameters)) {@(dateFormatting)@(numberFormatting) - String @(methodName)(@(innerMethodParameters)) { - return Intl.message( - @(message), - locale: _localeName, - @(intlMethodArgs) - ); - } - return @(methodName)(@(innerMethodArgs)); - } -'''; - -const String pluralMethodTemplate = ''' - String @(methodName)(@(methodParameters)) {@(dateFormatting)@(numberFormatting) - return Intl.plural( - @(intlMethodArgs) - ); - } -'''; - -const String pluralFormatMethodTemplate = ''' - String @(methodName)(@(methodParameters)) {@(dateFormatting)@(numberFormatting) - String @(methodName)(@(innerMethodParameters)) { - return Intl.plural( - @(intlMethodArgs) - ); - } - return @(methodName)(@(innerMethodArgs)); - } -'''; - -const String defaultFileTemplate = ''' +const String fileTemplate = ''' import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:intl/intl.dart'; +import 'package:intl/intl.dart' as intl; -import 'messages_all.dart'; +// ignore_for_file: unnecessary_brace_in_string_interps -/// Callers can lookup localized strings with an instance of @(className) returned -/// by `@(className).of(context)`. +/// Callers can lookup localized strings with an instance of @(class) returned +/// by `@(class).of(context)`. /// -/// Applications need to include `@(className).delegate()` in their app's -/// localizationDelegates list, and the locales they support in the app's +/// Applications need to include `@(class).delegate()` in their app\'s +/// localizationDelegates list, and the locales they support in the app\'s /// supportedLocales list. For example: /// /// ``` /// import '@(importFile)'; /// /// return MaterialApp( -/// localizationsDelegates: @(className).localizationsDelegates, -/// supportedLocales: @(className).supportedLocales, +/// localizationsDelegates: @(class).localizationsDelegates, +/// supportedLocales: @(class).supportedLocales, /// home: MyApplicationHome(), /// ); /// ``` @@ -113,26 +62,18 @@ /// Select and expand the newly-created Localizations item then, for each /// locale your application supports, add a new item and select the locale /// you wish to add from the pop-up menu in the Value field. This list should -/// be consistent with the languages listed in the @(className).supportedLocales +/// be consistent with the languages listed in the @(class).supportedLocales /// property. - -// ignore_for_file: unnecessary_brace_in_string_interps - -class @(className) { - @(className)(Locale locale) : _localeName = Intl.canonicalizedLocale(locale.toString()); +abstract class @(class) { + @(class)(String locale) : assert(locale != null), _localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String _localeName; - static Future<@(className)> load(Locale locale) { - return initializeMessages(locale.toString()) - .then<@(className)>((_) => @(className)(locale)); + static @(class) of(BuildContext context) { + return Localizations.of<@(class)>(context, @(class)); } - static @(className) of(BuildContext context) { - return Localizations.of<@(className)>(context, @(className)); - } - - static const LocalizationsDelegate<@(className)> delegate = _@(className)Delegate(); + static const LocalizationsDelegate<@(class)> delegate = _@(class)Delegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -152,21 +93,104 @@ ]; /// A list of this localizations delegate's supported locales. - @(supportedLocales) + static const List<Locale> supportedLocales = <Locale>[ + @(supportedLocales) + ]; -@(classMethods) -} +@(methods)} -class _@(className)Delegate extends LocalizationsDelegate<@(className)> { - const _@(className)Delegate(); +class _@(class)Delegate extends LocalizationsDelegate<@(class)> { + const _@(class)Delegate(); @override - Future<@(className)> load(Locale locale) => @(className).load(locale); + Future<@(class)> load(Locale locale) { + return SynchronousFuture<@(class)>(@(lookupName)(locale)); + } @override bool isSupported(Locale locale) => <String>[@(supportedLanguageCodes)].contains(locale.languageCode); @override - bool shouldReload(_@(className)Delegate old) => false; + bool shouldReload(_@(class)Delegate old) => false; +} + +@(allMessagesClasses) + +@(class) @(lookupName)(Locale locale) { + switch(locale.languageCode) { + @(lookupBody) + } + assert(false, '@(class).delegate failed to load unsupported locale "\$locale"'); + return null; } '''; + +const String numberFormatTemplate = ''' + final intl.NumberFormat @(placeholder)NumberFormat = intl.NumberFormat.@(format)( + locale: _localeName, + @(parameters) + ); + final String @(placeholder)String = @(placeholder)NumberFormat.format(@(placeholder)); +'''; + +const String dateFormatTemplate = ''' + final intl.DateFormat @(placeholder)DateFormat = intl.DateFormat.@(format)(_localeName); + final String @(placeholder)String = @(placeholder)DateFormat.format(@(placeholder)); +'''; + +const String getterTemplate = ''' + @override + String get @(name) => @(message);'''; + +const String methodTemplate = ''' + @override + String @(name)(@(parameters)) { + return @(message); + }'''; + +const String formatMethodTemplate = ''' + @override + String @(name)(@(parameters)) { +@(dateFormatting) +@(numberFormatting) + return @(message); + }'''; + +const String pluralMethodTemplate = ''' + @override + String @(name)(@(parameters)) { +@(dateFormatting) +@(numberFormatting) + return intl.Intl.pluralLogic( + @(count), + locale: _localeName, +@(pluralLogicArgs), + ); + }'''; + +const String classTemplate = ''' +/// The translations for @(language) (`@(localeName)`). +class @(class) extends @(baseClass) { + @(class)([String locale = '@(localeName)']) : super(locale); + +@(methods) +}'''; + +const String baseClassGetterTemplate = ''' + // @(comment) + String get @(name); +'''; + +const String baseClassMethodTemplate = ''' + // @(comment) + String @(name)(@(parameters)); +'''; + +const String switchClauseTemplate = '''case '@(case)': return @(class)();'''; + +const String countryCodeSwitchTemplate = '''case '@(languageCode)': { + switch (locale.countryCode) { + @(switchClauses) + } + return @(class)(); + }''';
diff --git a/dev/tools/localization/gen_l10n_types.dart b/dev/tools/localization/gen_l10n_types.dart index f00535a..b760dfc 100644 --- a/dev/tools/localization/gen_l10n_types.dart +++ b/dev/tools/localization/gen_l10n_types.dart
@@ -2,12 +2,149 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; +import 'dart:io'; + +import 'localizations_utils.dart'; + +// The set of date formats that can be automatically localized. +// +// The localizations generation tool makes use of the intl library's +// DateFormat class to properly format dates based on the locale, the +// desired format, as well as the passed in [DateTime]. For example, using +// DateFormat.yMMMMd("en_US").format(DateTime.utc(1996, 7, 10)) results +// in the string "July 10, 1996". +// +// Since the tool generates code that uses DateFormat's constructor, it is +// necessary to verify that the constructor exists, or the +// tool will generate code that may cause a compile-time error. +// +// See also: +// +// * <https://pub.dev/packages/intl> +// * <https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html> +// * <https://api.dartlang.org/stable/2.7.0/dart-core/DateTime-class.html> +const Set<String> _validDateFormats = <String>{ + 'd', + 'E', + 'EEEE', + 'LLL', + 'LLLL', + 'M', + 'Md', + 'MEd', + 'MMM', + 'MMMd', + 'MMMEd', + 'MMMM', + 'MMMMd', + 'MMMMEEEEd', + 'QQQ', + 'QQQQ', + 'y', + 'yM', + 'yMd', + 'yMEd', + 'yMMM', + 'yMMMd', + 'yMMMEd', + 'yMMMM', + 'yMMMMd', + 'yMMMMEEEEd', + 'yQQQ', + 'yQQQQ', + 'H', + 'Hm', + 'Hms', + 'j', + 'jm', + 'jms', + 'jmv', + 'jmz', + 'jv', + 'jz', + 'm', + 'ms', + 's', +}; + +// The set of number formats that can be automatically localized. +// +// The localizations generation tool makes use of the intl library's +// NumberFormat class to properly format numbers based on the locale, the +// desired format, as well as the passed in number. For example, using +// DateFormat.compactLong("en_US").format(1200000) results +// in the string "1.2 million". +// +// Since the tool generates code that uses NumberFormat's constructor, it is +// necessary to verify that the constructor exists, or the +// tool will generate code that may cause a compile-time error. +// +// See also: +// +// * <https://pub.dev/packages/intl> +// * <https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html> +const Set<String> _validNumberFormats = <String>{ + 'compact', + 'compactCurrency', + 'compactSimpleCurrency', + 'compactLong', + 'currency', + 'decimalPattern', + 'decimalPercentPattern', + 'percentPattern', + 'scientificPattern', + 'simpleCurrency', +}; + +// The names of the NumberFormat factory constructors which have named +// parameters rather than positional parameters. +// +// This helps the tool correctly generate number formmatting code correctly. +// +// Example of code that uses named parameters: +// final NumberFormat format = NumberFormat.compact( +// locale: _localeName, +// ); +// +// Example of code that uses positional parameters: +// final NumberFormat format = NumberFormat.scientificPattern(_localeName); +const Set<String> _numberFormatsWithNamedParameters = <String>{ + 'compact', + 'compactCurrency', + 'compactSimpleCurrency', + 'compactLong', + 'currency', + 'decimalPercentPattern', + 'simpleCurrency', +}; + class L10nException implements Exception { L10nException(this.message); final String message; } +// One optional named parameter to be used by a NumberFormat. +// +// Some of the NumberFormat factory constructors have optional named parameters. +// For example NumberFormat.compactCurrency has a decimalDigits parameter that +// specifies the number of decimal places to use when formatting. +// +// Optional parameters for NumberFormat placeholders are specified as a +// JSON map value for optionalParameters in a resource's "@" ARB file entry: +// +// "@myResourceId": { +// "placeholders": { +// "myNumberPlaceholder": { +// "type": "double", +// "format": "compactCurrency", +// "optionalParameters": { +// "decimalDigits": 2 +// } +// } +// } +// } class OptionalParameter { const OptionalParameter(this.name, this.value) : assert(name != null), assert(value != null); @@ -15,6 +152,39 @@ final Object value; } +// One message parameter: one placeholder from an @foo entry in the template ARB file. +// +// Placeholders are specified as a JSON map with one entry for each placeholder. +// One placeholder must be specified for each message "{parameter}". +// Each placeholder entry is also a JSON map. If the map is empty, the placeholder +// is assumed to be an Object value whose toString() value will be displayed. +// For example: +// +// "greeting": "{hello} {world}", +// "@greeting": { +// "description": "A message with a two parameters", +// "placeholders": { +// "hello": {}, +// "world": {} +// } +// } +// +// Each placeholder can optionally specify a valid Dart type. If the type +// is NumberFormat or DateFormat then a format which matches one of the +// type's factory constructors can also be specified. In this example the +// date placeholder is to be formated with DateFormat.yMMMMd: +// +// "helloWorldOn": "Hello World on {date}", +// "@helloWorldOn": { +// "description": "A message with a date parameter", +// "placeholders": { +// "date": { +// "type": "DateTime", +// "format": "yMMMMd" +// } +// } +// } +// class Placeholder { Placeholder(this.resourceId, this.name, Map<String, dynamic> attributes) : assert(resourceId != null), @@ -33,7 +203,10 @@ bool get requiresFormatting => <String>['DateTime', 'double', 'int', 'num'].contains(type); bool get isNumber => <String>['double', 'int', 'num'].contains(type); + bool get hasValidNumberFormat => _validNumberFormats.contains(format); + bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format); bool get isDate => 'DateTime' == type; + bool get hasValidDateFormat => _validDateFormats.contains(format); static String _stringAttribute( String resourceId, @@ -75,6 +248,21 @@ } } +// One translation: one pair of foo,@foo entries from the template ARB file. +// +// The template ARB file must contain an entry called @myResourceId for each +// message named myResourceId. The @ entry describes message parameters +// called "placeholders" and can include an optional description. +// Here's a simple example message with no parameters: +// +// "helloWorld": "Hello World", +// "@helloWorld": { +// "description": "The conventional newborn programmer greeting" +// } +// +// The value of this Message is "Hello World". The Message's value is the +// localized string to be shown for the template ARB file's locale. +// The docs for the Placeholder explain how placeholder entries are defined. class Message { Message(Map<String, dynamic> bundle, this.resourceId) : assert(bundle != null), @@ -169,3 +357,90 @@ }).toList(); } } + +// Represents the contents of one ARB file. +class AppResourceBundle { + factory AppResourceBundle(File file) { + assert(file != null); + // Assuming that the caller has verified that the file exists and is readable. + + final Map<String, dynamic> resources = json.decode(file.readAsStringSync()) as Map<String, dynamic>; + String localeString = resources['@@locale'] as String; + if (localeString == null) { + final RegExp filenameRE = RegExp(r'^[^_]*_(\w+)\.arb$'); + final RegExpMatch match = filenameRE.firstMatch(file.path); + localeString = match == null ? null : match[1]; + } + if (localeString == null) { + throw L10nException( + "The following .arb file's locale could not be determined: \n" + '${file.path} \n' + "Make sure that the locale is specified in the file's '@@locale' " + 'property or as part of the filename (e.g. file_en.arb)' + ); + } + + final Iterable<String> ids = resources.keys.where((String key) => !key.startsWith('@')); + return AppResourceBundle._(file, LocaleInfo.fromString(localeString), resources, ids); + } + + const AppResourceBundle._(this.file, this.locale, this.resources, this.resourceIds); + + final File file; + final LocaleInfo locale; + final Map<String, dynamic> resources; + final Iterable<String> resourceIds; + + String translationFor(Message message) => resources[message.resourceId] as String; + + @override + String toString() { + return 'AppResourceBundle($locale, ${file.path})'; + } +} + +// Represents all of the ARB files in [directory] as [AppResourceBundle]s. +class AppResourceBundleCollection { + factory AppResourceBundleCollection(Directory directory) { + assert(directory != null); + // Assuming that the caller has verified that the directory is readable. + + final RegExp filenameRE = RegExp(r'(\w+)\.arb$'); + final Map<LocaleInfo, AppResourceBundle> localeToBundle = <LocaleInfo, AppResourceBundle>{}; + final Map<String, List<LocaleInfo>> languageToLocales = <String, List<LocaleInfo>>{}; + final List<File> files = directory.listSync().whereType<File>().toList()..sort(sortFilesByPath); + for (final File file in files) { + if (filenameRE.hasMatch(file.path)) { + final AppResourceBundle bundle = AppResourceBundle(file); + if (localeToBundle[bundle.locale] != null) { + throw L10nException( + "Multiple arb files with the same '${bundle.locale}' locale detected. \n" + 'Ensure that there is exactly one arb file for each locale.' + ); + } + localeToBundle[bundle.locale] = bundle; + languageToLocales[bundle.locale.languageCode] ??= <LocaleInfo>[]; + languageToLocales[bundle.locale.languageCode].add(bundle.locale); + } + } + return AppResourceBundleCollection._(directory, localeToBundle, languageToLocales); + } + + const AppResourceBundleCollection._(this._directory, this._localeToBundle, this._languageToLocales); + + final Directory _directory; + final Map<LocaleInfo, AppResourceBundle> _localeToBundle; + final Map<String, List<LocaleInfo>> _languageToLocales; + + Iterable<LocaleInfo> get locales => _localeToBundle.keys; + Iterable<AppResourceBundle> get bundles => _localeToBundle.values; + AppResourceBundle bundleFor(LocaleInfo locale) => _localeToBundle[locale]; + + Iterable<String> get languages => _languageToLocales.keys; + Iterable<LocaleInfo> localesForLanguage(String language) => _languageToLocales[language] ?? <LocaleInfo>[]; + + @override + String toString() { + return 'AppResourceBundleCollection(${_directory.path}, ${locales.length} locales)'; + } +}
diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart index 7847b79..e090bc0 100644 --- a/dev/tools/test/localization/gen_l10n_test.dart +++ b/dev/tools/test/localization/gen_l10n_test.dart
@@ -25,7 +25,6 @@ "description": "Title for the application" } }'''; - const String esArbFileName = 'app_es.arb'; const String singleEsMessageArbFileString = ''' { @@ -48,10 +47,11 @@ void main() { MemoryFileSystem fs; - setUp(() { + setUp(() async { fs = MemoryFileSystem( style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix ); + await precacheLanguageAndRegionTags(); }); group('Setters', () { @@ -233,7 +233,7 @@ }); }); - group('parseArbFiles', () { + group('loadResources', () { test('correctly initializes supportedLocales and supportedLanguageCodes properties', () { _standardFlutterDirectoryL10nSetup(fs); @@ -246,7 +246,7 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { fail('Setting language and locales should not fail: \n$e'); } @@ -275,7 +275,7 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { fail('Setting language and locales should not fail: \n$e'); } @@ -306,7 +306,7 @@ classNameString: defaultClassNameString, preferredSupportedLocaleString: preferredSupportedLocaleString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { fail('Setting language and locales should not fail: \n$e'); } @@ -340,7 +340,7 @@ classNameString: defaultClassNameString, preferredSupportedLocaleString: preferredSupportedLocaleString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { expect( e.message, @@ -381,7 +381,7 @@ classNameString: defaultClassNameString, preferredSupportedLocaleString: preferredSupportedLocaleString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { expect( e.message, @@ -417,7 +417,7 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { fail('Setting language and locales should not fail: \n$e'); } @@ -468,7 +468,7 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { fail('Setting language and locales should not fail: \n$e'); } @@ -512,7 +512,7 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { fail('Setting language and locales should not fail: \n$e'); } @@ -538,7 +538,7 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { expect(e.message, contains('locale could not be determined')); return; @@ -572,9 +572,9 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); + generator.loadResources(); } on L10nException catch (e) { - expect(e.message, contains('Multiple arb files with the same locale detected')); + expect(e.message, contains("Multiple arb files with the same 'en' locale detected")); return; } @@ -585,11 +585,12 @@ }); }); - group('generateClassMethods', () { + group('generateCode', () { group('DateTime tests', () { test('throws an exception when improperly formatted date is passed in', () { const String singleDateMessageArbFileString = ''' { + "@@locale": "en", "springBegins": "Spring begins on {springStartDate}", "@springBegins": { "description": "The first day of spring", @@ -614,8 +615,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('asdf')); expect(e.message, contains('springStartDate')); @@ -652,8 +653,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('the "format" attribute needs to be set')); return; @@ -662,199 +663,6 @@ fail('Improper date formatting should throw an exception'); }); - test('correctly generates simple message with date along with other placeholders', () { - const String singleDateMessageArbFileString = ''' -{ - "springGreetings": "Since it's {springStartDate}, it's finally spring! {helloWorld}!", - "@springGreetings": { - "description": "A realization that it's finally the spring season, followed by a greeting.", - "placeholders": { - "springStartDate": { - "type": "DateTime", - "format": "yMMMMEEEEd" - }, - "helloWorld": {} - } - } -}'''; - final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') - ..createSync(recursive: true); - l10nDirectory.childFile(defaultTemplateArbFileName) - .writeAsStringSync(singleDateMessageArbFileString); - - final LocalizationsGenerator generator = LocalizationsGenerator(fs); - try { - generator.initialize( - l10nDirectoryPath: defaultArbPathString, - templateArbFileName: defaultTemplateArbFileName, - outputFileString: defaultOutputFileString, - classNameString: defaultClassNameString, - ); - generator.parseArbFiles(); - generator.generateClassMethods(); - } on Exception catch (e) { - fail('Parsing template arb file should succeed: \n$e'); - } - - expect(generator.classMethods, isNotEmpty); - expect( - generator.classMethods.first, - r''' - String springGreetings(DateTime springStartDate, Object helloWorld) { - final DateFormat springStartDateDateFormat = DateFormat.yMMMMEEEEd(_localeName); - final String springStartDateString = springStartDateDateFormat.format(springStartDate); - - String springGreetings(Object springStartDate, Object helloWorld) { - return Intl.message( - "Since it's ${springStartDate}, it's finally spring! ${helloWorld}!", - locale: _localeName, - name: 'springGreetings', - desc: "A realization that it's finally the spring season, followed by a greeting.", - args: <Object>[springStartDate, helloWorld] - ); - } - return springGreetings(springStartDateString, helloWorld); - } -'''); - }); - }); - - group('Number tests', () { - test('correctly adds optional named parameters to numbers', () { - const Set<String> numberFormatsWithNamedParameters = <String>{ - 'compact', - 'compactCurrency', - 'compactSimpleCurrency', - 'compactLong', - 'currency', - 'decimalPercentPattern', - 'simpleCurrency', - }; - - for (final String numberFormat in numberFormatsWithNamedParameters) { - final String singleNumberMessage = ''' -{ - "courseCompletion": "You have completed {progress} of the course.", - "@courseCompletion": { - "description": "The amount of progress the student has made in their class.", - "placeholders": { - "progress": { - "type": "double", - "format": "$numberFormat", - "optionalParameters": { - "decimalDigits": 2 - } - } - } - } -}'''; - final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') - ..createSync(recursive: true); - l10nDirectory.childFile(defaultTemplateArbFileName) - .writeAsStringSync(singleNumberMessage); - - final LocalizationsGenerator generator = LocalizationsGenerator(fs); - try { - generator.initialize( - l10nDirectoryPath: defaultArbPathString, - templateArbFileName: defaultTemplateArbFileName, - outputFileString: defaultOutputFileString, - classNameString: defaultClassNameString, - ); - generator.parseArbFiles(); - generator.generateClassMethods(); - } on Exception catch (e) { - fail('Parsing template arb file should succeed: \n$e'); - } - - expect(generator.classMethods, isNotEmpty); - expect( - generator.classMethods.first, - ''' - String courseCompletion(double progress) { - final NumberFormat progressNumberFormat = NumberFormat.$numberFormat( - locale: _localeName, - decimalDigits: 2, - ); - final String progressString = progressNumberFormat.format(progress); - - String courseCompletion(Object progress) { - return Intl.message( - 'You have completed \${progress} of the course.', - locale: _localeName, - name: 'courseCompletion', - desc: 'The amount of progress the student has made in their class.', - args: <Object>[progress] - ); - } - return courseCompletion(progressString); - } -''');} - }); - - test('correctly adds optional positional parameters to numbers', () { - const Set<String> numberFormatsWithPositionalParameters = <String>{ - 'decimalPattern', - 'percentPattern', - 'scientificPattern', - }; - - for (final String numberFormat in numberFormatsWithPositionalParameters) { - final String singleNumberMessage = ''' -{ - "courseCompletion": "You have completed {progress} of the course.", - "@courseCompletion": { - "description": "The amount of progress the student has made in their class.", - "placeholders": { - "progress": { - "type": "double", - "format": "$numberFormat" - } - } - } -}'''; - final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') - ..createSync(recursive: true); - l10nDirectory.childFile(defaultTemplateArbFileName) - .writeAsStringSync(singleNumberMessage); - - final LocalizationsGenerator generator = LocalizationsGenerator(fs); - try { - generator.initialize( - l10nDirectoryPath: defaultArbPathString, - templateArbFileName: defaultTemplateArbFileName, - outputFileString: defaultOutputFileString, - classNameString: defaultClassNameString, - ); - generator.parseArbFiles(); - generator.generateClassMethods(); - } on Exception catch (e) { - fail('Parsing template arb file should succeed: \n$e'); - } - - expect(generator.classMethods, isNotEmpty); - expect( - generator.classMethods.first, - ''' - String courseCompletion(double progress) { - final NumberFormat progressNumberFormat = NumberFormat.$numberFormat(_localeName); - final String progressString = progressNumberFormat.format(progress); - - String courseCompletion(Object progress) { - return Intl.message( - 'You have completed \${progress} of the course.', - locale: _localeName, - name: 'courseCompletion', - desc: 'The amount of progress the student has made in their class.', - args: <Object>[progress] - ); - } - return courseCompletion(progressString); - } -'''); - } - }); - test('throws an exception when improperly formatted number is passed in', () { const String singleDateMessageArbFileString = ''' { @@ -882,8 +690,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('asdf')); expect(e.message, contains('progress')); @@ -918,8 +726,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); return; @@ -950,8 +758,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); return; @@ -978,8 +786,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('Resource attribute "@helloWorlds" was not found')); return; @@ -1009,8 +817,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('is not properly formatted')); expect(e.message, contains('Ensure that it is a map with string valued keys')); @@ -1041,8 +849,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on FormatException catch (e) { expect(e.message, contains('Unexpected character')); return; @@ -1072,8 +880,8 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { expect(e.message, contains('Resource attribute "@title" was not found')); return; @@ -1089,11 +897,11 @@ test('cannot contain non-alphanumeric symbols', () { const String nonAlphaNumericArbFile = ''' { - "title!!": "Stocks", - "@title!!": { - "description": "Title for the Stocks application" - } - }'''; + "title!!": "Stocks", + "@title!!": { + "description": "Title for the Stocks application" + } +}'''; final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true); l10nDirectory.childFile(defaultTemplateArbFileName) @@ -1107,10 +915,10 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { - expect(e.message, contains('Invalid key format')); + expect(e.message, contains('Invalid ARB resource name')); return; } @@ -1120,11 +928,11 @@ test('must start with lowercase character', () { const String nonAlphaNumericArbFile = ''' { - "Title": "Stocks", - "@Title": { - "description": "Title for the Stocks application" - } - }'''; + "Title": "Stocks", + "@Title": { + "description": "Title for the Stocks application" + } +}'''; final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true); l10nDirectory.childFile(defaultTemplateArbFileName) @@ -1138,10 +946,10 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { - expect(e.message, contains('Invalid key format')); + expect(e.message, contains('Invalid ARB resource name')); return; } @@ -1151,11 +959,11 @@ test('cannot start with a number', () { const String nonAlphaNumericArbFile = ''' { - "123title": "Stocks", - "@123title": { - "description": "Title for the Stocks application" - } - }'''; + "123title": "Stocks", + "@123title": { + "description": "Title for the Stocks application" + } +}'''; final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true); l10nDirectory.childFile(defaultTemplateArbFileName) @@ -1169,10 +977,10 @@ outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, ); - generator.parseArbFiles(); - generator.generateClassMethods(); + generator.loadResources(); + generator.generateCode(); } on L10nException catch (e) { - expect(e.message, contains('Invalid key format')); + expect(e.message, contains('Invalid ARB resource name')); return; }