Unverified Commit 37e66b21 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

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
parent 066b5a16
......@@ -423,6 +423,7 @@ Future<void> _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);
......
// 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<void> main(List<String> 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', <String>['pub', 'get']);
if (pubGetResult.exitCode != 0) {
stderr.write(pubGetResult.stderr);
exit(1);
}
final ProcessResult generateFromArbResult = await Process.run('flutter', <String>[
'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);
}
}
......@@ -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<String> genMethodParameters(Map<String, dynamic> bundle, String key, String type) {
final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
if (attributesMap != null && attributesMap.containsKey('placeholders')) {
......@@ -193,7 +189,7 @@ String genSimpleMethod(Map<String, dynamic> bundle, String key) {
final Map<String, dynamic> attributesMap = bundle['@$key'] as Map<String, dynamic>;
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<String, dynamic> 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<void> main(List<String> 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_<locale>.dart` and `messages_all.dart`)
/// will reside here.
///
/// This directory is specified with the [initialize] method.
Directory l10nDirectory;
/// 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;
/// The file to write the generated localizations and localizations delegate
/// classes to.
///
/// This file is specified with the [initialize] method.
File outputFile;
/// 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<String> arbFilenames = <String>[];
final String arbPathString = results['arb-dir'] as String;
final String outputFileString = results['output-localization-file'] as String;
/// The supported language codes as found in the arb files located in
/// [l10nDirectory].
final Set<String> supportedLanguageCodes = <String>{};
/// The supported locales as found in the arb files located in
/// [l10nDirectory].
final Set<LocaleInfo> supportedLocales = <LocaleInfo>{};
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 class methods that will be generated in the localizations class
/// based on messages found in the template arb file.
final List<String> classMethods = <String>[];
/// Initializes [l10nDirectory], [templateArbFile], [outputFile] and [className].
///
/// Throws an [L10nException] when a provided configuration is not allowed
/// 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())
exitWithError(
throw FileSystemException(
"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(
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] == '-')
exitWithError(
if (templateArbFileStatModeString[0] == '-' && templateArbFileStatModeString[3] == '-')
throw FileSystemException(
"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"
);
final List<String> arbFilenames = <String>[];
final Set<String> supportedLanguageCodes = <String>{};
final Set<LocaleInfo> supportedLocales = <LocaleInfo>{};
}
for (FileSystemEntity entity in l10nDirectory.listSync().toList()..sort(sortFilesByPath)) {
final String entityPath = entity.path;
/// 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));
}
if (FileSystemEntity.isFileSync(entityPath)) {
final RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$');
if (arbFilenameRE.hasMatch(entityPath)) {
final File arbFile = File(entityPath);
final Map<String, dynamic> arbContents = json.decode(arbFile.readAsStringSync()) as Map<String, dynamic>;
String localeString = arbContents['@@locale'] as String;
@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<File> fileSystemEntityList = l10nDirectory
.listSync()
.whereType<File>()
.toList();
final List<LocaleInfo> localeInfoList = <LocaleInfo>[];
for (File file in fileSystemEntityList) {
final String filePath = file.path;
if (arbFilenameRE.hasMatch(filePath)) {
final Map<String, dynamic> 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);
}
}
final List<String> classMethods = <String>[];
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<String, dynamic> bundle;
try {
bundle = json.decode(templateArbFile.readAsStringSync()) as Map<String, dynamic>;
bundle = json.decode(templateArbFile.readAsStringSync());
} on FileSystemException catch (e) {
exitWithError('Unable to read input arb file: $e');
throw FileSystemException('Unable to read input arb file: $e');
} on FormatException catch (e) {
exitWithError('Unable to parse arb file: $e');
throw FormatException('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()) {
final List<String> sortedArbKeys = bundle.keys.toList()..sort();
for (String key in sortedArbKeys) {
if (key.startsWith('@'))
continue;
if (!_isValidGetterAndMethodName(key))
exitWithError(
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] as String))
if (pluralValueRE.hasMatch(bundle[key]))
classMethods.add(genPluralMethod(bundle, key));
else
classMethods.add(genSimpleMethod(bundle, key));
}
}
/// 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', stringsClassName)
.replaceAll('@className', className)
.replaceAll('@classMethods', classMethods.join('\n'))
.replaceAll('@importFile', _importFilePath(arbPathString, outputFileString))
.replaceAll('@importFile', '$directory/$outputFileName')
.replaceAll('@supportedLocales', genSupportedLocaleProperty(supportedLocales))
.replaceAll('@supportedLanguageCodes', supportedLanguageCodes.toList().join(', '))
);
final ProcessResult pubGetResult = await Process.run('flutter', <String>['pub', 'get']);
if (pubGetResult.exitCode != 0) {
stderr.write(pubGetResult.stderr);
exit(1);
}
}
final ProcessResult generateFromArbResult = await Process.run('flutter', <String>[
'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);
}
class L10nException implements Exception {
L10nException(this.message);
final String message;
}
......@@ -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
// 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<T>() => test_package.TypeMatcher<T>();
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');
}
}
// 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: <Object>[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: <Object>[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<AppLocalizations>'));
});
});
}
......@@ -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
```
......
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