From 37e66b21798c1507039e36769f12ea7b6cc0a43a Mon Sep 17 00:00:00 2001 From: Shi-Hao Hong Date: Fri, 6 Dec 2019 11:35:08 -0800 Subject: [PATCH] gen_l10n.dart tool testing (#44856) * Add tests to gen_l10n.dart tool * Separate out LocalizationsGenerator class to improve testability of code * Add testing dependencies to dev/tools * Integrate dev/tools testing to flutter CI * Restructure dev/tools folder for testing * Fix license headers --- dev/bots/test.dart | 1 + dev/tools/localization/bin/gen_l10n.dart | 101 +++ dev/tools/localization/gen_l10n.dart | 389 ++++++---- dev/tools/pubspec.yaml | 34 +- dev/tools/test/common.dart | 27 + .../test/localization/gen_l10n_test.dart | 716 ++++++++++++++++++ examples/stocks/lib/i18n/regenerate.md | 2 +- 7 files changed, 1111 insertions(+), 159 deletions(-) create mode 100644 dev/tools/localization/bin/gen_l10n.dart create mode 100644 dev/tools/test/common.dart create mode 100644 dev/tools/test/localization/gen_l10n_test.dart diff --git a/dev/bots/test.dart b/dev/bots/test.dart index d3678316b7..cf9af11156 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -423,6 +423,7 @@ Future _runFrameworkTests() async { await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'), tableData: bigqueryApi?.tabledata); await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'), tableData: bigqueryApi?.tabledata); await _pubRunTest(path.join(flutterRoot, 'dev', 'snippets'), tableData: bigqueryApi?.tabledata); + await _pubRunTest(path.join(flutterRoot, 'dev', 'tools'), tableData: bigqueryApi?.tabledata); await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'), tableData: bigqueryApi?.tabledata); await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'), tableData: bigqueryApi?.tabledata); await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'), tableData: bigqueryApi?.tabledata); diff --git a/dev/tools/localization/bin/gen_l10n.dart b/dev/tools/localization/bin/gen_l10n.dart new file mode 100644 index 0000000000..642c5d309a --- /dev/null +++ b/dev/tools/localization/bin/gen_l10n.dart @@ -0,0 +1,101 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart' as argslib; +import 'package:file/local.dart' as local; +import 'package:path/path.dart' as path; + +import '../gen_l10n.dart'; +import '../localizations_utils.dart'; + +Future main(List arguments) async { + final argslib.ArgParser parser = argslib.ArgParser(); + parser.addFlag( + 'help', + defaultsTo: false, + negatable: false, + help: 'Print this help message.', + ); + parser.addOption( + 'arb-dir', + defaultsTo: path.join('lib', 'l10n'), + help: 'The directory where all localization files should reside. For ' + 'example, the template and translated arb files should be located here. ' + 'Also, the generated output messages Dart files for each locale and the ' + 'generated localizations classes will be created here.', + ); + parser.addOption( + 'template-arb-file', + defaultsTo: 'app_en.arb', + help: 'The template arb file that will be used as the basis for ' + 'generating the Dart localization and messages files.', + ); + parser.addOption( + 'output-localization-file', + defaultsTo: 'app_localizations.dart', + help: 'The filename for the output localization and localizations ' + 'delegate classes.', + ); + parser.addOption( + 'output-class', + defaultsTo: 'AppLocalizations', + help: 'The Dart class name to use for the output localization and ' + 'localizations delegate classes.', + ); + + final argslib.ArgResults results = parser.parse(arguments); + if (results['help'] == true) { + print(parser.usage); + exit(0); + } + + final String arbPathString = results['arb-dir']; + final String outputFileString = results['output-localization-file']; + final String templateArbFileName = results['template-arb-file']; + final String classNameString = results['output-class']; + + const local.LocalFileSystem fs = local.LocalFileSystem(); + final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs); + try { + localizationsGenerator + ..initialize( + l10nDirectoryPath: arbPathString, + templateArbFileName: templateArbFileName, + outputFileString: outputFileString, + classNameString: classNameString, + ) + ..parseArbFiles() + ..generateClassMethods() + ..generateOutputFile(); + } on FileSystemException catch (e) { + exitWithError(e.message); + } on FormatException catch (e) { + exitWithError(e.message); + } on L10nException catch (e) { + exitWithError(e.message); + } + + final ProcessResult pubGetResult = await Process.run('flutter', ['pub', 'get']); + if (pubGetResult.exitCode != 0) { + stderr.write(pubGetResult.stderr); + exit(1); + } + + final ProcessResult generateFromArbResult = await Process.run('flutter', [ + 'pub', + 'run', + 'intl_translation:generate_from_arb', + '--output-dir=${localizationsGenerator.l10nDirectory.path}', + '--no-use-deferred-loading', + localizationsGenerator.outputFile.path, + ...localizationsGenerator.arbFilenames, + ]); + 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 0d954cddf5..2cc57f8b8f 100644 --- a/dev/tools/localization/gen_l10n.dart +++ b/dev/tools/localization/gen_l10n.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:args/args.dart' as argslib; +import 'package:file/file.dart' as file; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'localizations_utils.dart'; @@ -149,10 +149,6 @@ const String pluralMethodTemplate = ''' } '''; -int sortFilesByPath (FileSystemEntity a, FileSystemEntity b) { - return a.path.compareTo(b.path); -} - List genMethodParameters(Map bundle, String key, String type) { final Map attributesMap = bundle['@$key'] as Map; if (attributesMap != null && attributesMap.containsKey('placeholders')) { @@ -193,7 +189,7 @@ String genSimpleMethod(Map bundle, String key) { final Map attributesMap = bundle['@$key'] as Map; if (attributesMap == null) - exitWithError( + throw L10nException( 'Resource attribute "@$key" was not found. Please ensure that each ' 'resource id has a corresponding resource attribute.' ); @@ -238,7 +234,7 @@ String genPluralMethod(Map bundle, String key) { ...genIntlMethodArgs(bundle, key), ]; - for(String pluralKey in pluralIds.keys) { + for (String pluralKey in pluralIds.keys) { final RegExp expRE = RegExp('($pluralKey){([^}]+)}'); final RegExpMatch match = expRE.firstMatch(message); if (match != null && match.groupCount == 2) { @@ -289,6 +285,19 @@ bool _isValidClassName(String className) { return true; } +bool _isNotReadable(FileStat fileStat) { + final String rawStatString = fileStat.modeString(); + // Removes potential prepended permission bits, such as '(suid)' and '(guid)'. + final String statString = rawStatString.substring(rawStatString.length - 9); + return !(statString[0] == 'r' || statString[3] == 'r' || statString[6] == 'r'); +} +bool _isNotWritable(FileStat fileStat) { + final String rawStatString = fileStat.modeString(); + // Removes potential prepended permission bits, such as '(suid)' and '(guid)'. + final String statString = rawStatString.substring(rawStatString.length - 9); + return !(statString[1] == 'w' || statString[4] == 'w' || statString[7] == 'w'); +} + bool _isValidGetterAndMethodName(String name) { // Dart getter and method name cannot contain non-alphanumeric symbols if (name.contains(RegExp(r'[^a-zA-Z\d]'))) @@ -302,184 +311,250 @@ bool _isValidGetterAndMethodName(String name) { return true; } -bool _isDirectoryReadableAndWritable(String statString) { - if (statString[0] == '-' || statString[1] == '-') - return false; - return true; -} +/// 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 { + /// Creates an instance of the localizations generator class. + /// + /// It takes in a [FileSystem] representation that the class will act upon. + LocalizationsGenerator(this._fs); -String _importFilePath(String path, String fileName) { - final String replaceLib = path.replaceAll('lib/', ''); - return '$replaceLib/$fileName'; -} + static RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$'); + static RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$'); + static RegExp pluralValueRE = RegExp(r'^\s*\{[\w\s,]*,\s*plural\s*,'); -Future main(List arguments) async { - final argslib.ArgParser parser = argslib.ArgParser(); - parser.addFlag( - 'help', - defaultsTo: false, - negatable: false, - help: 'Print this help message.', - ); - parser.addOption( - 'arb-dir', - defaultsTo: path.join('lib', 'l10n'), - help: 'The directory where all localization files should reside. For ' - 'example, the template and translated arb files should be located here. ' - 'Also, the generated output messages Dart files for each locale and the ' - 'generated localizations classes will be created here.', - ); - parser.addOption( - 'template-arb-file', - defaultsTo: 'app_en.arb', - help: 'The template arb file that will be used as the basis for ' - 'generating the Dart localization and messages files.', - ); - parser.addOption( - 'output-localization-file', - defaultsTo: 'app_localizations.dart', - help: 'The filename for the output localization and localizations ' - 'delegate classes.', - ); - parser.addOption( - 'output-class', - defaultsTo: 'AppLocalizations', - help: 'The Dart class name to use for the output localization and ' - 'localizations delegate classes.', - ); + final file.FileSystem _fs; - final argslib.ArgResults results = parser.parse(arguments); - if (results['help'] == true) { - print(parser.usage); - exit(0); - } + /// The reference to the project's l10n directory. + /// + /// It is assumed that all input files (e.g. [templateArbFile], arb files + /// for translated messages) and output files (e.g. The localizations + /// [outputFile], `messages_.dart` and `messages_all.dart`) + /// will reside here. + /// + /// This directory is specified with the [initialize] method. + Directory l10nDirectory; - final String arbPathString = results['arb-dir'] as String; - final String outputFileString = results['output-localization-file'] as String; + /// The input arb file which defines all of the messages that will be + /// exported by the generated class that's written to [outputFile]. + /// + /// This file is specified with the [initialize] method. + File templateArbFile; - final Directory l10nDirectory = Directory(arbPathString); - final File templateArbFile = File(path.join(l10nDirectory.path, results['template-arb-file'] as String)); - final File outputFile = File(path.join(l10nDirectory.path, outputFileString)); - final String stringsClassName = results['output-class'] as String; + /// The file to write the generated localizations and localizations delegate + /// classes to. + /// + /// This file is specified with the [initialize] method. + File outputFile; - if (!l10nDirectory.existsSync()) - exitWithError( - "The 'arb-dir' directory, $l10nDirectory, does not exist.\n" - 'Make sure that the correct path was provided.' - ); - final String l10nDirectoryStatModeString = l10nDirectory.statSync().modeString(); - if (!_isDirectoryReadableAndWritable(l10nDirectoryStatModeString)) - exitWithError( - "The 'arb-dir' directory, $l10nDirectory, doesn't allow reading and writing.\n" - 'Please ensure that the user has read and write permissions.' - ); - final String templateArbFileStatModeString = templateArbFile.statSync().modeString(); - if (templateArbFileStatModeString[0] == '-') - exitWithError( - "The 'template-arb-file', $templateArbFile, is not readable.\n" - 'Please ensure that the user has read permissions.' - ); - if (!_isValidClassName(stringsClassName)) - exitWithError( - "The 'output-class', $stringsClassName, is not valid Dart class name.\n" - ); + /// The class name to be used for the localizations class in [outputFile]. + /// + /// For example, if 'AppLocalizations' is passed in, a class named + /// AppLocalizations will be used for localized message lookups. + /// + /// The class name is specified with the [initialize] method. + String get className => _className; + String _className; + /// Sets the [className] for the localizations and localizations delegate + /// classes. + /// The list of all arb files in [l10nDirectory]. final List arbFilenames = []; + + /// The supported language codes as found in the arb files located in + /// [l10nDirectory]. final Set supportedLanguageCodes = {}; + + /// The supported locales as found in the arb files located in + /// [l10nDirectory]. final Set supportedLocales = {}; - for (FileSystemEntity entity in l10nDirectory.listSync().toList()..sort(sortFilesByPath)) { - final String entityPath = entity.path; + /// The class methods that will be generated in the localizations class + /// based on messages found in the template arb file. + final List classMethods = []; - if (FileSystemEntity.isFileSync(entityPath)) { - final RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$'); - if (arbFilenameRE.hasMatch(entityPath)) { - final File arbFile = File(entityPath); - final Map arbContents = json.decode(arbFile.readAsStringSync()) as Map; - String localeString = arbContents['@@locale'] as String; + /// Initializes [l10nDirectory], [templateArbFile], [outputFile] and [className]. + /// + /// 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. + void initialize({ + String l10nDirectoryPath, + String templateArbFileName, + String outputFileString, + String classNameString, + }) { + setL10nDirectory(l10nDirectoryPath); + setTemplateArbFile(templateArbFileName); + setOutputFile(outputFileString); + className = classNameString; + } + /// Sets the reference [Directory] for [l10nDirectory]. + @visibleForTesting + void setL10nDirectory(String arbPathString) { + if (arbPathString == null) + throw L10nException('arbPathString argument cannot be null'); + l10nDirectory = _fs.directory(arbPathString); + if (!l10nDirectory.existsSync()) + throw FileSystemException( + "The 'arb-dir' directory, $l10nDirectory, does not exist.\n" + 'Make sure that the correct path was provided.' + ); + + final FileStat fileStat = l10nDirectory.statSync(); + if (_isNotReadable(fileStat) || _isNotWritable(fileStat)) + throw FileSystemException( + "The 'arb-dir' directory, $l10nDirectory, doesn't allow reading and writing.\n" + 'Please ensure that the user has read and write permissions.' + ); + } + + /// Sets the reference [File] for [templateArbFile]. + @visibleForTesting + void setTemplateArbFile(String templateArbFileName) { + if (templateArbFileName == null) + throw L10nException('templateArbFileName argument cannot be null'); + if (l10nDirectory == null) + throw L10nException('l10nDirectory cannot be null when setting template arb file'); + + templateArbFile = _fs.file(path.join(l10nDirectory.path, templateArbFileName)); + final String templateArbFileStatModeString = templateArbFile.statSync().modeString(); + if (templateArbFileStatModeString[0] == '-' && templateArbFileStatModeString[3] == '-') + throw FileSystemException( + "The 'template-arb-file', $templateArbFile, is not readable.\n" + 'Please ensure that the user has read permissions.' + ); + } + + /// Sets the reference [File] for the localizations delegate [outputFile]. + @visibleForTesting + void setOutputFile(String outputFileString) { + if (outputFileString == null) + throw L10nException('outputFileString argument cannot be null'); + outputFile = _fs.file(path.join(l10nDirectory.path, outputFileString)); + } + + @visibleForTesting + set className(String classNameString) { + if (classNameString == null) + throw L10nException('classNameString argument cannot be null'); + if (!_isValidClassName(classNameString)) + throw L10nException( + "The 'output-class', $classNameString, is not a valid Dart class name.\n" + ); + _className = classNameString; + } + + /// Scans [l10nDirectory] for arb files and parses them for language and locale + /// information. + void parseArbFiles() { + final List fileSystemEntityList = l10nDirectory + .listSync() + .whereType() + .toList(); + final List localeInfoList = []; + + for (File file in fileSystemEntityList) { + final String filePath = file.path; + if (arbFilenameRE.hasMatch(filePath)) { + final Map arbContents = json.decode(file.readAsStringSync()); + String localeString = arbContents['@@locale']; if (localeString == null) { - final RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$'); - final RegExpMatch arbFileMatch = arbFilenameLocaleRE.firstMatch(entityPath); + final RegExpMatch arbFileMatch = arbFilenameLocaleRE.firstMatch(filePath); if (arbFileMatch == null) { - exitWithError( + throw L10nException( "The following .arb file's locale could not be determined: \n" - '$entityPath \n' + '$filePath \n' "Make sure that the locale is specified in the '@@locale' " - 'property or as part of the filename (ie. file_en.arb)' + 'property or as part of the filename (e.g. file_en.arb)' ); } - localeString = arbFilenameLocaleRE.firstMatch(entityPath)[1]; + localeString = arbFilenameLocaleRE.firstMatch(filePath)[1]; } - arbFilenames.add(entityPath); + arbFilenames.add(filePath); final LocaleInfo localeInfo = LocaleInfo.fromString(localeString); - if (supportedLocales.contains(localeInfo)) - exitWithError( + 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.' ); - supportedLocales.add(localeInfo); - supportedLanguageCodes.add('\'${localeInfo.languageCode}\''); + localeInfoList.add(localeInfo); } } + + localeInfoList.sort(); + supportedLocales.addAll(localeInfoList); + supportedLanguageCodes.addAll(localeInfoList.map((LocaleInfo localeInfo) { + return '\'${localeInfo.languageCode}\''; + })); + } + + /// 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 bundle; + try { + bundle = json.decode(templateArbFile.readAsStringSync()); + } 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 sortedArbKeys = bundle.keys.toList()..sort(); + for (String key in sortedArbKeys) { + if (key.startsWith('@')) + continue; + if (!_isValidGetterAndMethodName(key)) + throw L10nException( + 'Invalid key format: $key \n It has to be in camel case, cannot start ' + 'with a number, and cannot contain non-alphanumeric characters.' + ); + if (pluralValueRE.hasMatch(bundle[key])) + classMethods.add(genPluralMethod(bundle, key)); + else + classMethods.add(genSimpleMethod(bundle, key)); + } } - final List classMethods = []; - - Map bundle; - try { - bundle = json.decode(templateArbFile.readAsStringSync()) as Map; - } on FileSystemException catch (e) { - exitWithError('Unable to read input arb file: $e'); - } on FormatException catch (e) { - exitWithError('Unable to parse arb file: $e'); - } - - final RegExp pluralValueRE = RegExp(r'^\s*\{[\w\s,]*,\s*plural\s*,'); - - for (String key in bundle.keys.toList()..sort()) { - if (key.startsWith('@')) - continue; - if (!_isValidGetterAndMethodName(key)) - exitWithError( - 'Invalid key format: $key \n It has to be in camel case, cannot start ' - 'with a number, and cannot contain non-alphanumeric characters.' - ); - if (pluralValueRE.hasMatch(bundle[key] as String)) - classMethods.add(genPluralMethod(bundle, key)); - else - classMethods.add(genSimpleMethod(bundle, key)); - } - - outputFile.writeAsStringSync( - defaultFileTemplate - .replaceAll('@className', stringsClassName) - .replaceAll('@classMethods', classMethods.join('\n')) - .replaceAll('@importFile', _importFilePath(arbPathString, outputFileString)) - .replaceAll('@supportedLocales', genSupportedLocaleProperty(supportedLocales)) - .replaceAll('@supportedLanguageCodes', supportedLanguageCodes.toList().join(', ')) - ); - - final ProcessResult pubGetResult = await Process.run('flutter', ['pub', 'get']); - if (pubGetResult.exitCode != 0) { - stderr.write(pubGetResult.stderr); - exit(1); - } - - final ProcessResult generateFromArbResult = await Process.run('flutter', [ - 'pub', - 'pub', - 'run', - 'intl_translation:generate_from_arb', - '--output-dir=${l10nDirectory.path}', - '--no-use-deferred-loading', - outputFile.path, - ...arbFilenames, - ]); - if (generateFromArbResult.exitCode != 0) { - stderr.write(generateFromArbResult.stderr); - exit(1); + /// Generates a file that contains the localizations class and the + /// LocalizationsDelegate class. + void generateOutputFile() { + 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(', ')) + ); } } + +class L10nException implements Exception { + L10nException(this.message); + + final String message; +} diff --git a/dev/tools/pubspec.yaml b/dev/tools/pubspec.yaml index 6254d8afe7..09f0177fcc 100644 --- a/dev/tools/pubspec.yaml +++ b/dev/tools/pubspec.yaml @@ -29,12 +29,44 @@ dependencies: typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: + test: 1.9.4 test_api: 0.2.11 mockito: 4.1.1 + analyzer: 0.38.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + coverage: 0.13.3+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + front_end: 0.1.27 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + glob: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http_multi_server: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + io: 0.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + js: 0.6.1+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + kernel: 0.3.27 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + logging: 0.11.3+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + mime: 0.9.6+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + multi_server_socket: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + node_interop: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + node_io: 1.0.1+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + node_preamble: 1.4.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + package_config: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + package_resolver: 1.0.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pool: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pub_semver: 1.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf: 0.7.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_packages_handler: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_static: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_web_socket: 0.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_map_stack_trace: 1.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_maps: 0.10.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stack_trace: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.2.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + watcher: 0.9.7+13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + yaml: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: a9e9 +# PUBSPEC CHECKSUM: 3590 diff --git a/dev/tools/test/common.dart b/dev/tools/test/common.dart new file mode 100644 index 0000000000..4060c06fda --- /dev/null +++ b/dev/tools/test/common.dart @@ -0,0 +1,27 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; +import 'package:test/test.dart' as test_package show TypeMatcher; + +export 'package:test/test.dart' hide TypeMatcher, isInstanceOf; + +// Defines a 'package:test' shim. +// TODO(ianh): Remove this file once https://github.com/dart-lang/matcher/issues/98 is fixed + +/// A matcher that compares the type of the actual value to the type argument T. +Matcher isInstanceOf() => test_package.TypeMatcher(); + +void tryToDelete(Directory directory) { + // This should not be necessary, but it turns out that + // on Windows it's common for deletions to fail due to + // bogus (we think) "access denied" errors. + try { + directory.deleteSync(recursive: true); + } on FileSystemException catch (error) { + print('Failed to delete ${directory.path}: $error'); + } +} diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart new file mode 100644 index 0000000000..872bec7617 --- /dev/null +++ b/dev/tools/test/localization/gen_l10n_test.dart @@ -0,0 +1,716 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:path/path.dart' as path; + +import '../../localization/gen_l10n.dart'; +import '../../localization/localizations_utils.dart'; + +import '../common.dart'; + +final String defaultArbPathString = path.join('lib', 'l10n'); +const String defaultTemplateArbFileName = 'app_en_US.arb'; +const String defaultOutputFileString = 'output-localization-file'; +const String defaultClassNameString = 'AppLocalizations'; +const String singleMessageArbFileString = '''{ + "title": "Stocks", + "@title": { + "description": "Title for the Stocks application" + } +}'''; + +const String esArbFileName = 'app_es.arb'; +const String singleEsMessageArbFileString = '''{ + "title": "Acciones" +}'''; + +void _standardFlutterDirectoryL10nSetup(FileSystem fs) { + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(singleMessageArbFileString); + l10nDirectory.childFile(esArbFileName) + .writeAsStringSync(singleEsMessageArbFileString); +} + +void main() { + MemoryFileSystem fs; + + setUp(() { + fs = MemoryFileSystem( + style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix + ); + }); + + group('LocalizationsGenerator setters:', () { + test('happy path', () { + _standardFlutterDirectoryL10nSetup(fs); + expect(() { + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + }, returnsNormally); + }); + + test('setL10nDirectory fails if the directory does not exist', () { + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.setL10nDirectory('lib'); + } on FileSystemException catch (e) { + expect(e.message, contains('Make sure that the correct path was provided')); + return; + } + + fail( + 'Attempting to set LocalizationsGenerator.setL10nDirectory should fail if the ' + 'directory does not exist.' + ); + }); + + test('setL10nDirectory fails if input string is null', () { + _standardFlutterDirectoryL10nSetup(fs); + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.setL10nDirectory(null); + } on L10nException catch (e) { + expect(e.message, contains('cannot be null')); + return; + } + + fail( + 'Attempting to set LocalizationsGenerator.setL10nDirectory should fail if the ' + 'the input string is null.' + ); + }); + + test('setTemplateArbFile fails if l10nDirectory is null', () { + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.setTemplateArbFile(defaultTemplateArbFileName); + } on L10nException catch (e) { + expect(e.message, contains('cannot be null')); + return; + } + + fail( + 'Attempting to set LocalizationsGenerator.setTemplateArbFile should fail if the ' + 'the l10nDirectory is null.' + ); + }); + + test('setTemplateArbFile fails if templateArbFileName is null', () { + _standardFlutterDirectoryL10nSetup(fs); + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.setTemplateArbFile(null); + } on L10nException catch (e) { + expect(e.message, contains('cannot be null')); + return; + } + + fail( + 'Attempting to set LocalizationsGenerator.setTemplateArbFile should fail if the ' + 'the l10nDirectory is null.' + ); + }); + + test('setTemplateArbFile fails if input string is null', () { + _standardFlutterDirectoryL10nSetup(fs); + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.setTemplateArbFile(null); + } on L10nException catch (e) { + expect(e.message, contains('cannot be null')); + return; + } + + fail( + 'Attempting to set LocalizationsGenerator.setTemplateArbFile should fail if the ' + 'the input string is null.' + ); + }); + + test('setOutputFile fails if input string is null', () { + _standardFlutterDirectoryL10nSetup(fs); + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.setOutputFile(null); + } on L10nException catch (e) { + expect(e.message, contains('cannot be null')); + return; + } + + fail( + 'Attempting to set LocalizationsGenerator.setOutputFile should fail if the ' + 'the input string is null.' + ); + }); + + test('setting className fails if input string is null', () { + _standardFlutterDirectoryL10nSetup(fs); + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.className = null; + } on L10nException catch (e) { + expect(e.message, contains('cannot be null')); + return; + } + + fail( + 'Attempting to set LocalizationsGenerator.className should fail if the ' + 'the input string is null.' + ); + }); + + group('className should only take valid Dart class names:', () { + LocalizationsGenerator generator; + setUp(() { + _standardFlutterDirectoryL10nSetup(fs); + generator = LocalizationsGenerator(fs); + try { + generator.setL10nDirectory(defaultArbPathString); + generator.setTemplateArbFile(defaultTemplateArbFileName); + generator.setOutputFile(defaultOutputFileString); + } on L10nException catch (e) { + throw TestFailure('Unexpected failure during test setup: ${e.message}'); + } + }); + + test('fails on string with spaces', () { + try { + generator.className = 'String with spaces'; + } on L10nException catch (e) { + expect(e.message, contains('is not a valid Dart class name')); + return; + } + fail( + 'Attempting to set LocalizationsGenerator.className should fail if the ' + 'the input string is not a valid Dart class name.' + ); + }); + + test('fails on non-alphanumeric symbols', () { + try { + generator.className = 'TestClass@123'; + } on L10nException catch (e) { + expect(e.message, contains('is not a valid Dart class name')); + return; + } + fail( + 'Attempting to set LocalizationsGenerator.className should fail if the ' + 'the input string is not a valid Dart class name.' + ); + }); + + test('fails on camel-case', () { + try { + generator.className = 'camelCaseClassName'; + } on L10nException catch (e) { + expect(e.message, contains('is not a valid Dart class name')); + return; + } + fail( + 'Attempting to set LocalizationsGenerator.className should fail if the ' + 'the input string is not a valid Dart class name.' + ); + }); + + test('fails when starting with a number', () { + try { + generator.className = '123ClassName'; + } on L10nException catch (e) { + expect(e.message, contains('is not a valid Dart class name')); + return; + } + fail( + 'Attempting to set LocalizationsGenerator.className should fail if the ' + 'the input string is not a valid Dart class name.' + ); + }); + }); + }); + + group('LocalizationsGenerator.parseArbFiles:', () { + test('correctly initializes supportedLocales and supportedLanguageCodes properties', () { + _standardFlutterDirectoryL10nSetup(fs); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + } on L10nException catch (e) { + fail('Setting language and locales should not fail: \n$e'); + } + + expect(generator.supportedLocales.contains(LocaleInfo.fromString('en_US')), true); + expect(generator.supportedLocales.contains(LocaleInfo.fromString('es')), true); + }); + + test('correctly parses @@locale property in arb file', () { + const String arbFileWithEnLocale = '''{ + "@@locale": "en", + "title": "Stocks", + "@title": { + "description": "Title for the Stocks application" + } +}'''; + + const String arbFileWithZhLocale = '''{ + "@@locale": "zh", + "title": "Stocks", + "@title": { + "description": "Title for the Stocks application" + } +}'''; + + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile('first_file.arb') + .writeAsStringSync(arbFileWithEnLocale); + l10nDirectory.childFile('second_file.arb') + .writeAsStringSync(arbFileWithZhLocale); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: 'first_file.arb', + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + } on L10nException catch (e) { + fail('Setting language and locales should not fail: \n$e'); + } + + expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true); + expect(generator.supportedLocales.contains(LocaleInfo.fromString('zh')), true); + }); + + test('correctly parses @@locale property in arb file', () { + const String arbFileWithEnLocale = '''{ + "@@locale": "en", + "title": "Stocks", + "@title": { + "description": "Title for the Stocks application" + } +}'''; + + const String arbFileWithZhLocale = '''{ + "@@locale": "zh", + "title": "Stocks", + "@title": { + "description": "Title for the Stocks application" + } +}'''; + + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile('app_es.arb') + .writeAsStringSync(arbFileWithEnLocale); + l10nDirectory.childFile('app_am.arb') + .writeAsStringSync(arbFileWithZhLocale); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: 'app_es.arb', + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + } on L10nException catch (e) { + fail('Setting language and locales should not fail: \n$e'); + } + + // @@locale property should hold higher priority + expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true); + expect(generator.supportedLocales.contains(LocaleInfo.fromString('zh')), true); + // filename should not be used since @@locale is specified + expect(generator.supportedLocales.contains(LocaleInfo.fromString('es')), false); + expect(generator.supportedLocales.contains(LocaleInfo.fromString('am')), false); + }); + + test('throws when arb file\'s locale could not be determined', () { + fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true) + ..childFile('app.arb') + .writeAsStringSync(singleMessageArbFileString); + try { + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: 'app.arb', + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + } on L10nException catch (e) { + expect(e.message, contains('locale could not be determined')); + return; + } + fail( + 'Since locale is not specified, setting languages and locales ' + 'should fail' + ); + }); + test('throws when the same locale is detected more than once', () { + const String secondMessageArbFileString = '''{ + "market": "MARKET", + "@market": { + "description": "Label for the Market tab" + } +}'''; + + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile('app_en.arb') + .writeAsStringSync(singleMessageArbFileString); + l10nDirectory.childFile('app2_en.arb') + .writeAsStringSync(secondMessageArbFileString); + + try { + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: 'app_en.arb', + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + } on L10nException catch (e) { + expect(e.message, contains('Multiple arb files with the same locale detected')); + return; + } + + fail( + 'Since en locale is specified twice, setting languages and locales ' + 'should fail' + ); + }); + }); + + group('LocalizationsGenerator.generateClassMethods:', () { + test('correctly generates a simple message with getter:', () { + _standardFlutterDirectoryL10nSetup(fs); + 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 get title { + return Intl.message( + r'Stocks', + locale: _localeName, + name: 'title', + desc: r'Title for the Stocks application' + ); + } +'''); + }); + + test('correctly generates simple message method with parameters', () { + const String singleSimpleMessageWithPlaceholderArbFileString = '''{ + "itemNumber": "Item {value}", + "@itemNumber": { + "description": "Item placement in list.", + "placeholders": { + "value": { + "example": "1" + } + } + } +}'''; + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(singleSimpleMessageWithPlaceholderArbFileString); + + 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 itemNumber(Object value) { + return Intl.message( + r\'Item \$value\', + locale: _localeName, + name: 'itemNumber', + desc: r\'Item placement in list.\', + args: [value] + ); + } +'''); + }); + + test('correctly generates a plural message:', () { + const String singlePluralMessageArbFileString = '''{ + "helloWorlds": "{count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}", + "@helloWorlds": { + "placeholders": { + "count": {} + } + } +}'''; + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(singlePluralMessageArbFileString); + + 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 helloWorlds(int count) { + return Intl.plural( + count, + locale: _localeName, + name: 'helloWorlds', + args: [count], + zero: 'Hello', + one: 'Hello World', + two: 'Hello two worlds', + few: 'Hello \$count worlds', + many: 'Hello all \$count worlds', + other: 'Hello other \$count worlds' + ); + } +''' + ); + }); + + test('should throw when failing to parse the arb file:', () { + const String arbFileWithTrailingComma = '''{ + "title": "Stocks", + "@title": { + "description": "Title for the Stocks application" + }, +}'''; + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(arbFileWithTrailingComma); + + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + generator.generateClassMethods(); + } on FormatException catch (e) { + expect(e.message, contains('Unexpected character')); + return; + } + + fail( + 'should fail with a FormatException due to a trailing comma in the ' + 'arb file.' + ); + }); + + test('should throw when resource is is missing resource attribute:', () { + const String arbFileWithMissingResourceAttribute = '''{ + "title": "Stocks" +}'''; + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(arbFileWithMissingResourceAttribute); + + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + generator.generateClassMethods(); + } on L10nException catch (e) { + expect(e.message, contains('Resource attribute "@title" was not found')); + return; + } + + fail( + 'should fail with a FormatException due to a trailing comma in the ' + 'arb file.' + ); + }); + + group('checks for method/getter formatting', () { + test('cannot contain non-alphanumeric symbols', () { + const String nonAlphaNumericArbFile = '''{ + "title!!": "Stocks", + "@title!!": { + "description": "Title for the Stocks application" + } + }'''; + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(nonAlphaNumericArbFile); + + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + generator.generateClassMethods(); + } on L10nException catch (e) { + expect(e.message, contains('Invalid key format')); + return; + } + + fail('should fail due to non-alphanumeric character.'); + }); + + test('must start with lowercase character', () { + const String nonAlphaNumericArbFile = '''{ + "Title": "Stocks", + "@Title": { + "description": "Title for the Stocks application" + } + }'''; + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(nonAlphaNumericArbFile); + + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + generator.generateClassMethods(); + } on L10nException catch (e) { + expect(e.message, contains('Invalid key format')); + return; + } + + fail('should fail since key starts with a non-lowercase.'); + }); + + test('cannot start with a number', () { + const String nonAlphaNumericArbFile = '''{ + "123title": "Stocks", + "@123title": { + "description": "Title for the Stocks application" + } + }'''; + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(nonAlphaNumericArbFile); + + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + generator.generateClassMethods(); + } on L10nException catch (e) { + expect(e.message, contains('Invalid key format')); + return; + } + + fail('should fail since key starts with a number.'); + }); + }); + }); + + group('LocalizationsGenerator.generateOutputFile:', () { + test('correctly generates the localizations classes:', () { + _standardFlutterDirectoryL10nSetup(fs); + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.initialize( + l10nDirectoryPath: defaultArbPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ); + generator.parseArbFiles(); + generator.generateClassMethods(); + generator.generateOutputFile(); + } on Exception catch (e) { + fail('Generating output localization file should succeed: \n$e'); + } + + final String outputFileString = generator.outputFile.readAsStringSync(); + expect(outputFileString, contains('class AppLocalizations')); + expect(outputFileString, contains('class _AppLocalizationsDelegate extends LocalizationsDelegate')); + }); + }); +} diff --git a/examples/stocks/lib/i18n/regenerate.md b/examples/stocks/lib/i18n/regenerate.md index 331a433505..e75b945154 100644 --- a/examples/stocks/lib/i18n/regenerate.md +++ b/examples/stocks/lib/i18n/regenerate.md @@ -15,7 +15,7 @@ for more info. `messages_all.dart`, and `stock_strings.dart` with the following command: ```dart -dart ${FLUTTER_PATH}/dev/tools/localization/gen_l10n.dart --arb-dir=lib/i18n \ +dart ${FLUTTER_PATH}/dev/tools/localization/bin/gen_l10n.dart --arb-dir=lib/i18n \ --template-arb-file=stocks_en_US.arb --output-localization-file=stock_strings.dart \ --output-class=StockStrings ```