Unverified Commit 13e1336e authored by Hans Muller's avatar Hans Muller Committed by GitHub

Generate message lookup in gen_l10n (#50733)

parent 9b3754d5
...@@ -63,9 +63,8 @@ Future<void> main(List<String> arguments) async { ...@@ -63,9 +63,8 @@ Future<void> main(List<String> arguments) async {
exit(0); exit(0);
} }
final String flutterRoot = Platform.environment['FLUTTER_ROOT']; await precacheLanguageAndRegionTags();
final String flutterBin = Platform.isWindows ? 'flutter.bat' : 'flutter';
final String flutterPath = flutterRoot == null ? flutterBin : path.join(flutterRoot, 'bin', flutterBin);
final String arbPathString = results['arb-dir'] as String; final String arbPathString = results['arb-dir'] as String;
final String outputFileString = results['output-localization-file'] as String; final String outputFileString = results['output-localization-file'] as String;
final String templateArbFileName = results['template-arb-file'] as String; final String templateArbFileName = results['template-arb-file'] as String;
...@@ -74,6 +73,7 @@ Future<void> main(List<String> arguments) async { ...@@ -74,6 +73,7 @@ Future<void> main(List<String> arguments) async {
const local.LocalFileSystem fs = local.LocalFileSystem(); const local.LocalFileSystem fs = local.LocalFileSystem();
final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs); final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs);
try { try {
localizationsGenerator localizationsGenerator
..initialize( ..initialize(
...@@ -83,9 +83,8 @@ Future<void> main(List<String> arguments) async { ...@@ -83,9 +83,8 @@ Future<void> main(List<String> arguments) async {
classNameString: classNameString, classNameString: classNameString,
preferredSupportedLocaleString: preferredSupportedLocaleString, preferredSupportedLocaleString: preferredSupportedLocaleString,
) )
..parseArbFiles() ..loadResources()
..generateClassMethods() ..writeOutputFile();
..generateOutputFile();
} on FileSystemException catch (e) { } on FileSystemException catch (e) {
exitWithError(e.message); exitWithError(e.message);
} on FormatException catch (e) { } on FormatException catch (e) {
...@@ -93,24 +92,4 @@ Future<void> main(List<String> arguments) async { ...@@ -93,24 +92,4 @@ Future<void> main(List<String> arguments) async {
} on L10nException catch (e) { } on L10nException catch (e) {
exitWithError(e.message); 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);
}
} }
...@@ -13,255 +13,78 @@ import 'gen_l10n_templates.dart'; ...@@ -13,255 +13,78 @@ import 'gen_l10n_templates.dart';
import 'gen_l10n_types.dart'; import 'gen_l10n_types.dart';
import 'localizations_utils.dart'; import 'localizations_utils.dart';
// The set of date formats that can be automatically localized. List<String> generateMethodParameters(Message message) {
// assert(message.placeholders.isNotEmpty);
// The localizations generation tool makes use of the intl library's final Placeholder countPlaceholder = message.isPlural ? message.getCountPlaceholder() : null;
// 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) {
return message.placeholders.map((Placeholder placeholder) { return message.placeholders.map((Placeholder placeholder) {
final String arg = placeholder.name; final String type = placeholder == countPlaceholder ? 'int' : placeholder.type;
return placeholder.requiresFormatting ? '${arg}String' : arg; return '$type ${placeholder.name}';
}).toList(); }).toList();
} }
String generateDateFormattingLogic(Message message) { String generateDateFormattingLogic(Message message) {
if (message.placeholders.isEmpty) if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting)
return ''; 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 DateFormat ${placeholder.name}DateFormat = DateFormat.${placeholder.format}(_localeName);
final String ${placeholder.name}String = ${placeholder.name}DateFormat.format(${placeholder.name});
''');
}
return result.toString();
}
String generateNumberFormattingLogic(Message message) {
if (message.placeholders.isEmpty)
return '';
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.'
);
}
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('''
final NumberFormat ${placeholder.name}NumberFormat = NumberFormat.${placeholder.format}(
locale: _localeName,${optionalParametersString.toString()}
);
final String ${placeholder.name}String = ${placeholder.name}NumberFormat.format(${placeholder.name});
''');
} else { final Iterable<String> formatStatements = message.placeholders
result.write(''' .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 NumberFormat ${placeholder.name}NumberFormat = NumberFormat.${placeholder.format}(_localeName); return formatStatements.isEmpty ? '@(none)' : formatStatements.join('');
final String ${placeholder.name}String = ${placeholder.name}NumberFormat.format(${placeholder.name});
''');
}
}
return result.toString();
} }
String genSimpleMethod(Message message) { String generateNumberFormattingLogic(Message message) {
String genSimpleMethodMessage() { if (message.placeholders.isEmpty || !message.placeholdersRequireFormatting) {
String messageValue = message.value; return '@(none)';
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) { final Iterable<String> formatStatements = message.placeholders
return simpleMethodTemplate .where((Placeholder placeholder) => placeholder.isNumber)
.replaceAll('@(methodName)', message.resourceId) .map((Placeholder placeholder) {
.replaceAll('@(methodParameters)', genMethodParameters().join(', ')) if (!placeholder.hasValidNumberFormat) {
.replaceAll('@(message)', genSimpleMethodMessage()) throw L10nException(
.replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n ')); 'Number format ${placeholder.format} for the ${placeholder.name} '
} 'placeholder does not have a corresponding NumberFormat constructor.\n'
'Check the intl library\'s NumberFormat class constructors for allowed '
'number formats.'
);
}
final Iterable<String> parameters =
placeholder.optionalParameters.map<String>((OptionalParameter parameter) {
return '${parameter.name}: ${parameter.value}';
},
);
return numberFormatTemplate
.replaceAll('@(placeholder)', placeholder.name)
.replaceAll('@(format)', placeholder.format)
.replaceAll('@(parameters)', parameters.join(', \n'));
});
return getterMethodTemplate return formatStatements.isEmpty ? '@(none)' : formatStatements.join('');
.replaceAll('@(methodName)', message.resourceId)
.replaceAll('@(message)', genSimpleMethodMessage())
.replaceAll('@(intlMethodArgs)', generateIntlMethodArgs(message).join(',\n '));
} }
String generatePluralMethod(Message message) { String generatePluralMethod(Message message) {
if (message.placeholders.isEmpty) { if (message.placeholders.isEmpty) {
throw L10nException( throw L10nException(
...@@ -277,15 +100,6 @@ String generatePluralMethod(Message message) { ...@@ -277,15 +100,6 @@ String generatePluralMethod(Message message) {
for (final Placeholder placeholder in message.placeholders) for (final Placeholder placeholder in message.placeholders)
easyMessage = easyMessage.replaceAll('{${placeholder.name}}', '#${placeholder.name}#'); 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(); final Placeholder countPlaceholder = message.getCountPlaceholder();
if (countPlaceholder == null) { if (countPlaceholder == null) {
throw L10nException( throw L10nException(
...@@ -295,67 +109,154 @@ String generatePluralMethod(Message message) { ...@@ -295,67 +109,154 @@ String generatePluralMethod(Message message) {
); );
} }
final List<String> intlMethodArgs = <String>[ const Map<String, String> pluralIds = <String, String>{
countPlaceholder.name, '=0': 'zero',
'locale: _localeName', '=1': 'one',
...generateIntlMethodArgs(message), '=2': 'two',
]; 'few': 'few',
'many': 'many',
'other': 'other'
};
final List<String> pluralLogicArgs = <String>[];
for (final String pluralKey in pluralIds.keys) { for (final String pluralKey in pluralIds.keys) {
final RegExp expRE = RegExp('($pluralKey){([^}]+)}'); final RegExp expRE = RegExp('($pluralKey)\\s*{([^}]+)}');
final RegExpMatch match = expRE.firstMatch(easyMessage); final RegExpMatch match = expRE.firstMatch(easyMessage);
if (match != null && match.groupCount == 2) { if (match != null && match.groupCount == 2) {
String argValue = match.group(2); String argValue = match.group(2);
for (final Placeholder placeholder in message.placeholders) { for (final Placeholder placeholder in message.placeholders) {
if (placeholder.requiresFormatting) { if (placeholder != countPlaceholder && placeholder.requiresFormatting) {
argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}'); argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}');
} else { } else {
argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}'); argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}');
} }
} }
intlMethodArgs.add("${pluralIds[pluralKey]}: '$argValue'"); pluralLogicArgs.add(" ${pluralIds[pluralKey]}: '$argValue'");
}
}
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'";
} }
List<String> generatePluralMethodParameters([String type]) { if (message.isPlural) {
return message.placeholders.map((Placeholder placeholder) { return generatePluralMethod(message);
final String placeholderType = placeholder == countPlaceholder ? 'int' : (type ?? placeholder.type);
return '$placeholderType ${placeholder.name}';
}).toList();
} }
if (message.placeholdersRequireFormatting) { if (message.placeholdersRequireFormatting) {
return pluralFormatMethodTemplate return formatMethodTemplate
.replaceAll('@(methodName)', message.resourceId) .replaceAll('@(name)', message.resourceId)
.replaceAll('@(methodParameters)', generatePluralMethodParameters().join(', ')) .replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) .replaceAll('@(dateFormatting)', generateDateFormattingLogic(message))
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) .replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message))
.replaceAll('@(innerMethodParameters)', generatePluralMethodParameters('Object').join(', ')) .replaceAll('@(message)', generateMessage())
.replaceAll('@(innerMethodArgs)', generateInnerMethodArgs(message).join(', ')) .replaceAll('@(none)\n', '');
.replaceAll('@(intlMethodArgs)', intlMethodArgs.join(',\n '));
} }
return pluralMethodTemplate if (message.placeholders.isNotEmpty) {
.replaceAll('@(methodName)', message.resourceId) return methodTemplate
.replaceAll('@(methodParameters)', generatePluralMethodParameters().join(', ')) .replaceAll('@(name)', message.resourceId)
.replaceAll('@(dateFormatting)', generateDateFormattingLogic(message)) .replaceAll('@(parameters)', generateMethodParameters(message).join(', '))
.replaceAll('@(numberFormatting)', generateNumberFormattingLogic(message)) .replaceAll('@(message)', generateMessage());
.replaceAll('@(intlMethodArgs)', intlMethodArgs.join(',\n ')); }
return getterTemplate
.replaceAll('@(name)', message.resourceId)
.replaceAll('@(message)', generateMessage());
}
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 ');
} }
/// The localizations generation class used to generate the localizations
/// classes, as well as all pertinent Dart files required to internationalize a
/// Flutter application.
class LocalizationsGenerator { class LocalizationsGenerator {
/// Creates an instance of the localizations generator class. /// Creates an instance of the localizations generator class.
/// ///
/// It takes in a [FileSystem] representation that the class will act upon. /// It takes in a [FileSystem] representation that the class will act upon.
LocalizationsGenerator(this._fs); LocalizationsGenerator(this._fs);
static RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$');
static RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$');
final file.FileSystem _fs; final file.FileSystem _fs;
Iterable<Message> _allMessages;
AppResourceBundleCollection _allBundles;
/// The reference to the project's l10n directory. /// The reference to the project's l10n directory.
/// ///
...@@ -404,7 +305,9 @@ class LocalizationsGenerator { ...@@ -404,7 +305,9 @@ class LocalizationsGenerator {
List<LocaleInfo> _preferredSupportedLocales; List<LocaleInfo> _preferredSupportedLocales;
/// The list of all arb path strings in [l10nDirectory]. /// 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 /// The supported language codes as found in the arb files located in
/// [l10nDirectory]. /// [l10nDirectory].
...@@ -414,10 +317,6 @@ class LocalizationsGenerator { ...@@ -414,10 +317,6 @@ class LocalizationsGenerator {
/// [l10nDirectory]. /// [l10nDirectory].
final Set<LocaleInfo> supportedLocales = <LocaleInfo>{}; 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]. /// Initializes [l10nDirectory], [templateArbFile], [outputFile] and [className].
/// ///
/// Throws an [L10nException] when a provided configuration is not allowed /// Throws an [L10nException] when a provided configuration is not allowed
...@@ -531,7 +430,9 @@ class LocalizationsGenerator { ...@@ -531,7 +430,9 @@ class LocalizationsGenerator {
/// will take priority over the other locales. /// will take priority over the other locales.
@visibleForTesting @visibleForTesting
void setPreferredSupportedLocales(String inputLocales) { 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>; final List<dynamic> preferredLocalesStringList = json.decode(inputLocales) as List<dynamic>;
_preferredSupportedLocales = preferredLocalesStringList.map((dynamic localeString) { _preferredSupportedLocales = preferredLocalesStringList.map((dynamic localeString) {
if (localeString.runtimeType != String) { if (localeString.runtimeType != String) {
...@@ -542,70 +443,6 @@ class LocalizationsGenerator { ...@@ -542,70 +443,6 @@ class LocalizationsGenerator {
} }
} }
/// 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) { static bool _isValidGetterAndMethodName(String name) {
// Public Dart method name must not start with an underscore // Public Dart method name must not start with an underscore
if (name[0] == '_') if (name[0] == '_')
...@@ -622,83 +459,78 @@ class LocalizationsGenerator { ...@@ -622,83 +459,78 @@ class LocalizationsGenerator {
return true; return true;
} }
static String _genSupportedLocaleProperty(Set<LocaleInfo> supportedLocales) { // Load _allMessages from templateArbFile and _allBundles from all of the ARB
const String prefix = 'static const List<Locale> supportedLocales = <Locale>[\n Locale('; // files in l10nDirectory. Also initialized: supportedLocales.
const String suffix = '),\n ];'; void loadResources() {
final AppResourceBundle templateBundle = AppResourceBundle(templateArbFile);
String resultingProperty = prefix; _allMessages = templateBundle.resourceIds.map((String id) => Message(templateBundle.resources, id));
for (final LocaleInfo locale in supportedLocales) { for (final String resourceId in templateBundle.resourceIds)
final String languageCode = locale.languageCode; if (!_isValidGetterAndMethodName(resourceId)) {
final String countryCode = locale.countryCode; throw L10nException(
'Invalid ARB resource name "$resourceId" in $templateArbFile.\n'
'Resources names must be valid Dart method names: they have to be '
'camel case, cannot start with a number or underscore, and cannot '
'contain non-alphanumeric characters.'
);
}
resultingProperty += "'$languageCode'"; _allBundles = AppResourceBundleCollection(l10nDirectory);
if (countryCode != null)
resultingProperty += ", '$countryCode'";
resultingProperty += '),\n Locale(';
}
resultingProperty = resultingProperty.substring(0, resultingProperty.length - '),\n Locale('.length);
resultingProperty += suffix;
return resultingProperty; final List<LocaleInfo> allLocales = List<LocaleInfo>.from(_allBundles.locales);
} for (final LocaleInfo preferredLocale in preferredSupportedLocales) {
final int index = allLocales.indexOf(preferredLocale);
/// Generates the methods for the localizations class. if (index == -1) {
///
/// 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)) {
throw L10nException( throw L10nException(
'Invalid key format: $key \n It has to be in camel case, cannot start ' "The preferred supported locale, '$preferredLocale', cannot be "
'with a number or underscore, and cannot contain non-alphanumeric characters.' '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);
final Message message = Message(bundle, key); allLocales.insertAll(0, preferredSupportedLocales);
if (message.isPlural)
classMethods.add(generatePluralMethod(message));
else
classMethods.add(genSimpleMethod(message));
} }
supportedLocales.addAll(allLocales);
} }
/// Generates a file that contains the localizations class and the // Generate the AppLocalizations class, its LocalizationsDelegate subclass.
/// LocalizationsDelegate class. String generateCode() {
void generateOutputFile() {
final String directory = path.basename(l10nDirectory.path); final String directory = path.basename(l10nDirectory.path);
final String outputFileName = path.basename(outputFile.path); final String outputFileName = path.basename(outputFile.path);
outputFile.writeAsStringSync(
defaultFileTemplate final Iterable<String> supportedLocalesCode = supportedLocales.map((LocaleInfo locale) {
.replaceAll('@(className)', className) final String country = locale.countryCode;
.replaceAll('@(classMethods)', classMethods.join('\n')) final String countryArg = country == null ? '' : ', $country';
.replaceAll('@(importFile)', '$directory/$outputFileName') return 'Locale(\'${locale.languageCode}$countryArg\')';
.replaceAll('@(supportedLocales)', _genSupportedLocaleProperty(supportedLocales)) });
.replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.toList().join(', '))
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());
} }
} }
...@@ -2,80 +2,29 @@ ...@@ -2,80 +2,29 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
const String getterMethodTemplate = ''' const String fileTemplate = '''
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 = '''
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.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 /// Callers can lookup localized strings with an instance of @(class) returned
/// by `@(className).of(context)`. /// by `@(class).of(context)`.
/// ///
/// Applications need to include `@(className).delegate()` in their app's /// Applications need to include `@(class).delegate()` in their app\'s
/// localizationDelegates list, and the locales they support in the app's /// localizationDelegates list, and the locales they support in the app\'s
/// supportedLocales list. For example: /// supportedLocales list. For example:
/// ///
/// ``` /// ```
/// import '@(importFile)'; /// import '@(importFile)';
/// ///
/// return MaterialApp( /// return MaterialApp(
/// localizationsDelegates: @(className).localizationsDelegates, /// localizationsDelegates: @(class).localizationsDelegates,
/// supportedLocales: @(className).supportedLocales, /// supportedLocales: @(class).supportedLocales,
/// home: MyApplicationHome(), /// home: MyApplicationHome(),
/// ); /// );
/// ``` /// ```
...@@ -113,26 +62,18 @@ import 'messages_all.dart'; ...@@ -113,26 +62,18 @@ import 'messages_all.dart';
/// Select and expand the newly-created Localizations item then, for each /// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale /// 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 /// 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. /// property.
abstract class @(class) {
// ignore_for_file: unnecessary_brace_in_string_interps @(class)(String locale) : assert(locale != null), _localeName = intl.Intl.canonicalizedLocale(locale.toString());
class @(className) {
@(className)(Locale locale) : _localeName = Intl.canonicalizedLocale(locale.toString());
final String _localeName; final String _localeName;
static Future<@(className)> load(Locale locale) { static @(class) of(BuildContext context) {
return initializeMessages(locale.toString()) return Localizations.of<@(class)>(context, @(class));
.then<@(className)>((_) => @(className)(locale));
}
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 /// A list of this localizations delegate along with the default localizations
/// delegates. /// delegates.
...@@ -152,21 +93,104 @@ class @(className) { ...@@ -152,21 +93,104 @@ class @(className) {
]; ];
/// A list of this localizations delegate's supported locales. /// 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)> { class _@(class)Delegate extends LocalizationsDelegate<@(class)> {
const _@(className)Delegate(); const _@(class)Delegate();
@override @override
Future<@(className)> load(Locale locale) => @(className).load(locale); Future<@(class)> load(Locale locale) {
return SynchronousFuture<@(class)>(@(lookupName)(locale));
}
@override @override
bool isSupported(Locale locale) => <String>[@(supportedLanguageCodes)].contains(locale.languageCode); bool isSupported(Locale locale) => <String>[@(supportedLanguageCodes)].contains(locale.languageCode);
@override @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)();
}''';
...@@ -2,12 +2,149 @@ ...@@ -2,12 +2,149 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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 { class L10nException implements Exception {
L10nException(this.message); L10nException(this.message);
final String 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 { class OptionalParameter {
const OptionalParameter(this.name, this.value) : assert(name != null), assert(value != null); const OptionalParameter(this.name, this.value) : assert(name != null), assert(value != null);
...@@ -15,6 +152,39 @@ class OptionalParameter { ...@@ -15,6 +152,39 @@ class OptionalParameter {
final Object value; 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 { class Placeholder {
Placeholder(this.resourceId, this.name, Map<String, dynamic> attributes) Placeholder(this.resourceId, this.name, Map<String, dynamic> attributes)
: assert(resourceId != null), : assert(resourceId != null),
...@@ -33,7 +203,10 @@ class Placeholder { ...@@ -33,7 +203,10 @@ class Placeholder {
bool get requiresFormatting => <String>['DateTime', 'double', 'int', 'num'].contains(type); bool get requiresFormatting => <String>['DateTime', 'double', 'int', 'num'].contains(type);
bool get isNumber => <String>['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 isDate => 'DateTime' == type;
bool get hasValidDateFormat => _validDateFormats.contains(format);
static String _stringAttribute( static String _stringAttribute(
String resourceId, String resourceId,
...@@ -75,6 +248,21 @@ class Placeholder { ...@@ -75,6 +248,21 @@ class Placeholder {
} }
} }
// 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 { class Message {
Message(Map<String, dynamic> bundle, this.resourceId) Message(Map<String, dynamic> bundle, this.resourceId)
: assert(bundle != null), : assert(bundle != null),
...@@ -169,3 +357,90 @@ class Message { ...@@ -169,3 +357,90 @@ class Message {
}).toList(); }).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)';
}
}
...@@ -25,7 +25,6 @@ const String singleMessageArbFileString = ''' ...@@ -25,7 +25,6 @@ const String singleMessageArbFileString = '''
"description": "Title for the application" "description": "Title for the application"
} }
}'''; }''';
const String esArbFileName = 'app_es.arb'; const String esArbFileName = 'app_es.arb';
const String singleEsMessageArbFileString = ''' const String singleEsMessageArbFileString = '''
{ {
...@@ -48,10 +47,11 @@ void _standardFlutterDirectoryL10nSetup(FileSystem fs) { ...@@ -48,10 +47,11 @@ void _standardFlutterDirectoryL10nSetup(FileSystem fs) {
void main() { void main() {
MemoryFileSystem fs; MemoryFileSystem fs;
setUp(() { setUp(() async {
fs = MemoryFileSystem( fs = MemoryFileSystem(
style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix
); );
await precacheLanguageAndRegionTags();
}); });
group('Setters', () { group('Setters', () {
...@@ -233,7 +233,7 @@ void main() { ...@@ -233,7 +233,7 @@ void main() {
}); });
}); });
group('parseArbFiles', () { group('loadResources', () {
test('correctly initializes supportedLocales and supportedLanguageCodes properties', () { test('correctly initializes supportedLocales and supportedLanguageCodes properties', () {
_standardFlutterDirectoryL10nSetup(fs); _standardFlutterDirectoryL10nSetup(fs);
...@@ -246,7 +246,7 @@ void main() { ...@@ -246,7 +246,7 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
fail('Setting language and locales should not fail: \n$e'); fail('Setting language and locales should not fail: \n$e');
} }
...@@ -275,7 +275,7 @@ void main() { ...@@ -275,7 +275,7 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
fail('Setting language and locales should not fail: \n$e'); fail('Setting language and locales should not fail: \n$e');
} }
...@@ -306,7 +306,7 @@ void main() { ...@@ -306,7 +306,7 @@ void main() {
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
preferredSupportedLocaleString: preferredSupportedLocaleString, preferredSupportedLocaleString: preferredSupportedLocaleString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
fail('Setting language and locales should not fail: \n$e'); fail('Setting language and locales should not fail: \n$e');
} }
...@@ -340,7 +340,7 @@ void main() { ...@@ -340,7 +340,7 @@ void main() {
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
preferredSupportedLocaleString: preferredSupportedLocaleString, preferredSupportedLocaleString: preferredSupportedLocaleString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
expect( expect(
e.message, e.message,
...@@ -381,7 +381,7 @@ void main() { ...@@ -381,7 +381,7 @@ void main() {
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
preferredSupportedLocaleString: preferredSupportedLocaleString, preferredSupportedLocaleString: preferredSupportedLocaleString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
expect( expect(
e.message, e.message,
...@@ -417,7 +417,7 @@ void main() { ...@@ -417,7 +417,7 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
fail('Setting language and locales should not fail: \n$e'); fail('Setting language and locales should not fail: \n$e');
} }
...@@ -468,7 +468,7 @@ void main() { ...@@ -468,7 +468,7 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
fail('Setting language and locales should not fail: \n$e'); fail('Setting language and locales should not fail: \n$e');
} }
...@@ -512,7 +512,7 @@ void main() { ...@@ -512,7 +512,7 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
fail('Setting language and locales should not fail: \n$e'); fail('Setting language and locales should not fail: \n$e');
} }
...@@ -538,7 +538,7 @@ void main() { ...@@ -538,7 +538,7 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('locale could not be determined')); expect(e.message, contains('locale could not be determined'));
return; return;
...@@ -572,9 +572,9 @@ void main() { ...@@ -572,9 +572,9 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
} on L10nException catch (e) { } 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; return;
} }
...@@ -585,11 +585,12 @@ void main() { ...@@ -585,11 +585,12 @@ void main() {
}); });
}); });
group('generateClassMethods', () { group('generateCode', () {
group('DateTime tests', () { group('DateTime tests', () {
test('throws an exception when improperly formatted date is passed in', () { test('throws an exception when improperly formatted date is passed in', () {
const String singleDateMessageArbFileString = ''' const String singleDateMessageArbFileString = '''
{ {
"@@locale": "en",
"springBegins": "Spring begins on {springStartDate}", "springBegins": "Spring begins on {springStartDate}",
"@springBegins": { "@springBegins": {
"description": "The first day of spring", "description": "The first day of spring",
...@@ -614,8 +615,8 @@ void main() { ...@@ -614,8 +615,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('asdf')); expect(e.message, contains('asdf'));
expect(e.message, contains('springStartDate')); expect(e.message, contains('springStartDate'));
...@@ -652,8 +653,8 @@ void main() { ...@@ -652,8 +653,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('the "format" attribute needs to be set')); expect(e.message, contains('the "format" attribute needs to be set'));
return; return;
...@@ -662,199 +663,6 @@ void main() { ...@@ -662,199 +663,6 @@ void main() {
fail('Improper date formatting should throw an exception'); 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', () { test('throws an exception when improperly formatted number is passed in', () {
const String singleDateMessageArbFileString = ''' const String singleDateMessageArbFileString = '''
{ {
...@@ -882,8 +690,8 @@ void main() { ...@@ -882,8 +690,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('asdf')); expect(e.message, contains('asdf'));
expect(e.message, contains('progress')); expect(e.message, contains('progress'));
...@@ -918,8 +726,8 @@ void main() { ...@@ -918,8 +726,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format'));
return; return;
...@@ -950,8 +758,8 @@ void main() { ...@@ -950,8 +758,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format'));
return; return;
...@@ -978,8 +786,8 @@ void main() { ...@@ -978,8 +786,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('Resource attribute "@helloWorlds" was not found')); expect(e.message, contains('Resource attribute "@helloWorlds" was not found'));
return; return;
...@@ -1009,8 +817,8 @@ void main() { ...@@ -1009,8 +817,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('is not properly formatted')); expect(e.message, contains('is not properly formatted'));
expect(e.message, contains('Ensure that it is a map with string valued keys')); expect(e.message, contains('Ensure that it is a map with string valued keys'));
...@@ -1041,8 +849,8 @@ void main() { ...@@ -1041,8 +849,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on FormatException catch (e) { } on FormatException catch (e) {
expect(e.message, contains('Unexpected character')); expect(e.message, contains('Unexpected character'));
return; return;
...@@ -1072,8 +880,8 @@ void main() { ...@@ -1072,8 +880,8 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('Resource attribute "@title" was not found')); expect(e.message, contains('Resource attribute "@title" was not found'));
return; return;
...@@ -1089,11 +897,11 @@ void main() { ...@@ -1089,11 +897,11 @@ void main() {
test('cannot contain non-alphanumeric symbols', () { test('cannot contain non-alphanumeric symbols', () {
const String nonAlphaNumericArbFile = ''' const String nonAlphaNumericArbFile = '''
{ {
"title!!": "Stocks", "title!!": "Stocks",
"@title!!": { "@title!!": {
"description": "Title for the Stocks application" "description": "Title for the Stocks application"
} }
}'''; }''';
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
..createSync(recursive: true); ..createSync(recursive: true);
l10nDirectory.childFile(defaultTemplateArbFileName) l10nDirectory.childFile(defaultTemplateArbFileName)
...@@ -1107,10 +915,10 @@ void main() { ...@@ -1107,10 +915,10 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('Invalid key format')); expect(e.message, contains('Invalid ARB resource name'));
return; return;
} }
...@@ -1120,11 +928,11 @@ void main() { ...@@ -1120,11 +928,11 @@ void main() {
test('must start with lowercase character', () { test('must start with lowercase character', () {
const String nonAlphaNumericArbFile = ''' const String nonAlphaNumericArbFile = '''
{ {
"Title": "Stocks", "Title": "Stocks",
"@Title": { "@Title": {
"description": "Title for the Stocks application" "description": "Title for the Stocks application"
} }
}'''; }''';
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
..createSync(recursive: true); ..createSync(recursive: true);
l10nDirectory.childFile(defaultTemplateArbFileName) l10nDirectory.childFile(defaultTemplateArbFileName)
...@@ -1138,10 +946,10 @@ void main() { ...@@ -1138,10 +946,10 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('Invalid key format')); expect(e.message, contains('Invalid ARB resource name'));
return; return;
} }
...@@ -1151,11 +959,11 @@ void main() { ...@@ -1151,11 +959,11 @@ void main() {
test('cannot start with a number', () { test('cannot start with a number', () {
const String nonAlphaNumericArbFile = ''' const String nonAlphaNumericArbFile = '''
{ {
"123title": "Stocks", "123title": "Stocks",
"@123title": { "@123title": {
"description": "Title for the Stocks application" "description": "Title for the Stocks application"
} }
}'''; }''';
final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
..createSync(recursive: true); ..createSync(recursive: true);
l10nDirectory.childFile(defaultTemplateArbFileName) l10nDirectory.childFile(defaultTemplateArbFileName)
...@@ -1169,10 +977,10 @@ void main() { ...@@ -1169,10 +977,10 @@ void main() {
outputFileString: defaultOutputFileString, outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString, classNameString: defaultClassNameString,
); );
generator.parseArbFiles(); generator.loadResources();
generator.generateClassMethods(); generator.generateCode();
} on L10nException catch (e) { } on L10nException catch (e) {
expect(e.message, contains('Invalid key format')); expect(e.message, contains('Invalid ARB resource name'));
return; return;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment