diff --git a/dev/tools/localization/bin/gen_l10n.dart b/dev/tools/localization/bin/gen_l10n.dart index 768b5f0d9de..195b860c800 100644 --- a/dev/tools/localization/bin/gen_l10n.dart +++ b/dev/tools/localization/bin/gen_l10n.dart @@ -105,6 +105,21 @@ void main(List arguments) { 'Note that this flag does not affect other platforms such as mobile or ' 'desktop.', ); + parser.addOption( + 'gen-inputs-and-outputs-list', + valueHelp: 'path-to-output-directory', + help: 'When specified, the tool generates a JSON file containing the ' + 'tool\'s inputs and outputs named gen_l10n_inputs_and_outputs.json.' + '\n\n' + 'This can be useful for keeping track of which files of the Flutter ' + 'project were used when generating the latest set of localizations. ' + 'For example, the Flutter tool\'s build system uses this file to ' + 'keep track of when to call gen_l10n during hot reload.\n\n' + 'The value of this option is the directory where the JSON file will be ' + 'generated.' + '\n\n' + 'When null, the JSON file will not be generated.' + ); final argslib.ArgResults results = parser.parse(arguments); if (results['help'] == true) { @@ -124,6 +139,7 @@ void main(List arguments) { final String headerString = results['header'] as String; final String headerFile = results['header-file'] as String; final bool useDeferredLoading = results['use-deferred-loading'] as bool; + final String inputsAndOutputsListPath = results['gen-inputs-and-outputs-list'] as String; const local.LocalFileSystem fs = local.LocalFileSystem(); final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(fs); @@ -140,9 +156,10 @@ void main(List arguments) { headerString: headerString, headerFile: headerFile, useDeferredLoading: useDeferredLoading, + inputsAndOutputsListPath: inputsAndOutputsListPath, ) ..loadResources() - ..writeOutputFile() + ..writeOutputFiles() ..outputUnimplementedMessages(untranslatedMessagesFile); } on FileSystemException catch (e) { exitWithError(e.message); diff --git a/dev/tools/localization/gen_l10n.dart b/dev/tools/localization/gen_l10n.dart index 0aba4b853c6..dfab847ec73 100644 --- a/dev/tools/localization/gen_l10n.dart +++ b/dev/tools/localization/gen_l10n.dart @@ -418,11 +418,13 @@ class LocalizationsGenerator { /// This file is specified with the [initialize] method. File templateArbFile; - /// The file to write the generated localizations and localizations delegate - /// classes to. + /// The file to write the generated abstract localizations and + /// localizations delegate classes to. Separate localizations + /// files will also be generated for each language using this + /// filename as a prefix and the locale as the suffix. /// /// This file is specified with the [initialize] method. - File outputFile; + File baseOutputFile; /// The class name to be used for the localizations class in [outputFile]. /// @@ -491,6 +493,12 @@ class LocalizationsGenerator { /// classes. String _generatedLocalizationsFile; + /// The file that contains the list of inputs and outputs for generating + /// localizations. + File _inputsAndOutputsListFile; + List _inputFileList; + List _outputFileList; + /// Initializes [inputDirectory], [outputDirectory], [templateArbFile], /// [outputFile] and [className]. /// @@ -509,15 +517,17 @@ class LocalizationsGenerator { String headerString, String headerFile, bool useDeferredLoading = false, + String inputsAndOutputsListPath, }) { setInputDirectory(inputPathString); setOutputDirectory(outputPathString ?? inputPathString); setTemplateArbFile(templateArbFileName); - setOutputFile(outputFileString); + setBaseOutputFile(outputFileString); setPreferredSupportedLocales(preferredSupportedLocaleString); _setHeader(headerString, headerFile); _setUseDeferredLoading(useDeferredLoading); className = classNameString; + _setInputsAndOutputsListFile(inputsAndOutputsListPath); } static bool _isNotReadable(FileStat fileStat) { @@ -581,10 +591,10 @@ class LocalizationsGenerator { /// Sets the reference [File] for the localizations delegate [outputFile]. @visibleForTesting - void setOutputFile(String outputFileString) { + void setBaseOutputFile(String outputFileString) { if (outputFileString == null) throw L10nException('outputFileString argument cannot be null'); - outputFile = _fs.file(path.join(outputDirectory.path, outputFileString)); + baseOutputFile = _fs.file(path.join(outputDirectory.path, outputFileString)); } static bool _isValidClassName(String className) { @@ -664,6 +674,17 @@ class LocalizationsGenerator { _useDeferredLoading = useDeferredLoading; } + void _setInputsAndOutputsListFile(String inputsAndOutputsListPath) { + if (inputsAndOutputsListPath == null) + return; + + _inputsAndOutputsListFile = _fs.file( + path.join(inputsAndOutputsListPath, 'gen_l10n_inputs_and_outputs.json'), + ); + _inputFileList = []; + _outputFileList = []; + } + static bool _isValidGetterAndMethodName(String name) { // Public Dart method name must not start with an underscore if (name[0] == '_') @@ -697,6 +718,11 @@ class LocalizationsGenerator { } _allBundles = AppResourceBundleCollection(inputDirectory); + if (_inputsAndOutputsListFile != null) { + _inputFileList.addAll(_allBundles.bundles.map((AppResourceBundle bundle) { + return bundle.file.absolute.path; + })); + } final List allLocales = List.from(_allBundles.locales); for (final LocaleInfo preferredLocale in preferredSupportedLocales) { @@ -781,8 +807,9 @@ class LocalizationsGenerator { } // Generate the AppLocalizations class, its LocalizationsDelegate subclass, - // and all AppLocalizations subclasses for every locale. - void generateCode() { + // and all AppLocalizations subclasses for every locale. This method by + // itself does not generate the output files. + void _generateCode() { bool isBaseClassLocale(LocaleInfo locale, String language) { return locale.languageCode == language && locale.countryCode == null @@ -800,7 +827,7 @@ class LocalizationsGenerator { } final String directory = path.basename(outputDirectory.path); - final String outputFileName = path.basename(outputFile.path); + final String outputFileName = path.basename(baseOutputFile.path); final Iterable supportedLocalesCode = supportedLocales.map((LocaleInfo locale) { final String languageCode = locale.languageCode; @@ -892,9 +919,9 @@ class LocalizationsGenerator { .replaceAll('@(delegateClass)', delegateClass); } - void writeOutputFile() { + void writeOutputFiles() { // First, generate the string contents of all necessary files. - generateCode(); + _generateCode(); // Since all validity checks have passed up to this point, // write the contents into the directory. @@ -913,8 +940,27 @@ class LocalizationsGenerator { // Generate the required files for localizations. _languageFileMap.forEach((File file, String contents) { file.writeAsStringSync(contents); + if (_inputsAndOutputsListFile != null) { + _outputFileList.add(file.absolute.path); + } }); - outputFile.writeAsStringSync(_generatedLocalizationsFile); + + baseOutputFile.writeAsStringSync(_generatedLocalizationsFile); + if (_inputsAndOutputsListFile != null) { + _outputFileList.add(baseOutputFile.absolute.path); + + // Generate a JSON file containing the inputs and outputs of the gen_l10n script. + if (!_inputsAndOutputsListFile.existsSync()) { + _inputsAndOutputsListFile.createSync(recursive: true); + } + + _inputsAndOutputsListFile.writeAsStringSync( + json.encode( { + 'inputs': _inputFileList, + 'outputs': _outputFileList, + }), + ); + } } void outputUnimplementedMessages(String untranslatedMessagesFile) { diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart index 21a14f8c124..8748a221c1f 100644 --- a/dev/tools/test/localization/gen_l10n_test.dart +++ b/dev/tools/test/localization/gen_l10n_test.dart @@ -77,7 +77,7 @@ void main() { } fail( - 'Attempting to set LocalizationsGenerator.setInputDirectory should fail if the ' + 'LocalizationsGenerator.setInputDirectory should fail if the ' 'directory does not exist.' ); }); @@ -93,8 +93,8 @@ void main() { } fail( - 'Attempting to set LocalizationsGenerator.setInputDirectory should fail if the ' - 'the input string is null.' + 'LocalizationsGenerator.setInputDirectory should fail if the ' + 'input string is null.' ); }); @@ -109,8 +109,8 @@ void main() { } fail( - 'Attempting to set LocalizationsGenerator.setOutputDirectory should fail if the ' - 'the input string is null.' + 'LocalizationsGenerator.setOutputDirectory should fail if the ' + 'input string is null.' ); }); @@ -124,8 +124,8 @@ void main() { } fail( - 'Attempting to set LocalizationsGenerator.setTemplateArbFile should fail if the ' - 'the inputDirectory is not specified.' + 'LocalizationsGenerator.setTemplateArbFile should fail if the ' + 'inputDirectory is not specified.' ); }); @@ -140,8 +140,8 @@ void main() { } fail( - 'Attempting to set LocalizationsGenerator.setTemplateArbFile should fail if the ' - 'the templateArbFileName passed in is null.' + 'LocalizationsGenerator.setTemplateArbFile should fail if the ' + 'templateArbFileName passed in is null.' ); }); @@ -156,24 +156,24 @@ void main() { } fail( - 'Attempting to set LocalizationsGenerator.setTemplateArbFile should fail if the ' - 'the input string is null.' + 'LocalizationsGenerator.setTemplateArbFile should fail if the ' + 'input string is null.' ); }); - test('setOutputFile fails if input string is null', () { + test('setBaseOutputFile fails if input string is null', () { _standardFlutterDirectoryL10nSetup(fs); final LocalizationsGenerator generator = LocalizationsGenerator(fs); try { - generator.setOutputFile(null); + generator.setBaseOutputFile(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.' + 'LocalizationsGenerator.setBaseOutputFile should fail if the ' + 'input string is null.' ); }); @@ -188,8 +188,8 @@ void main() { } fail( - 'Attempting to set LocalizationsGenerator.className should fail if the ' - 'the input string is null.' + 'LocalizationsGenerator.className should fail if the ' + 'input string is null.' ); }); @@ -202,7 +202,7 @@ void main() { generator.setInputDirectory(defaultL10nPathString); generator.setOutputDirectory(defaultL10nPathString); generator.setTemplateArbFile(defaultTemplateArbFileName); - generator.setOutputFile(defaultOutputFileString); + generator.setBaseOutputFile(defaultOutputFileString); } on L10nException catch (e) { throw TestFailure('Unexpected failure during test setup: ${e.message}'); } @@ -216,8 +216,8 @@ void main() { return; } fail( - 'Attempting to set LocalizationsGenerator.className should fail if the ' - 'the input string is not a valid Dart class name.' + 'LocalizationsGenerator.className should fail if the ' + 'input string is not a valid Dart class name.' ); }); @@ -229,8 +229,8 @@ void main() { return; } fail( - 'Attempting to set LocalizationsGenerator.className should fail if the ' - 'the input string is not a valid public Dart class name.' + 'LocalizationsGenerator.className should fail if the ' + 'input string is not a valid public Dart class name.' ); }); @@ -242,8 +242,8 @@ void main() { return; } fail( - 'Attempting to set LocalizationsGenerator.className should fail if the ' - 'the input string is not a valid public Dart class name.' + 'LocalizationsGenerator.className should fail if the ' + 'input string is not a valid public Dart class name.' ); }); @@ -255,8 +255,8 @@ void main() { return; } fail( - 'Attempting to set LocalizationsGenerator.className should fail if the ' - 'the input string is not a valid public Dart class name.' + 'LocalizationsGenerator.className should fail if the ' + 'input string is not a valid public Dart class name.' ); }); }); @@ -326,7 +326,7 @@ void main() { classNameString: defaultClassNameString, ) ..loadResources() - ..generateCode() + ..writeOutputFiles() ..outputUnimplementedMessages(path.join('lib', 'l10n', 'unimplemented_message_translations.json')); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); @@ -359,7 +359,7 @@ void main() { classNameString: defaultClassNameString, ) ..loadResources() - ..writeOutputFile(); + ..writeOutputFiles(); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -397,7 +397,7 @@ void main() { classNameString: defaultClassNameString, ) ..loadResources() - ..writeOutputFile(); + ..writeOutputFiles(); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -424,7 +424,7 @@ void main() { classNameString: defaultClassNameString, ) ..loadResources() - ..writeOutputFile(); + ..writeOutputFiles(); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -436,6 +436,45 @@ void main() { expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); }); + test('creates list of inputs and outputs when file path is specified', () { + _standardFlutterDirectoryL10nSetup(fs); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + inputsAndOutputsListPath: defaultL10nPathString, + ) + ..loadResources() + ..writeOutputFiles(); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } + + final File inputsAndOutputsList = fs + .directory('lib') + .childDirectory('l10n') + .childFile('gen_l10n_inputs_and_outputs.json'); + expect(inputsAndOutputsList.existsSync(), isTrue); + + final Map jsonResult = json.decode(inputsAndOutputsList.readAsStringSync()) as Map; + expect(jsonResult.containsKey('inputs'), isTrue); + final List inputList = jsonResult['inputs'] as List; + expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_en.arb'))); + expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_es.arb'))); + + expect(jsonResult.containsKey('outputs'), isTrue); + final List outputList = jsonResult['outputs'] as List; + expect(outputList, contains(fs.path.absolute('lib', 'l10n', 'output-localization-file.dart'))); + expect(outputList, contains(fs.path.absolute('lib', 'l10n', 'output-localization-file_en.dart'))); + expect(outputList, contains(fs.path.absolute('lib', 'l10n', 'output-localization-file_es.dart'))); + }); + test('setting both a headerString and a headerFile should fail', () { fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true) @@ -710,15 +749,9 @@ void main() { fail('Setting language and locales should not fail: \n${e.message}'); } - if (Platform.isWindows) { - expect(generator.arbPathStrings.first, r'lib\l10n\app_en.arb'); - expect(generator.arbPathStrings.elementAt(1), r'lib\l10n\app_es.arb'); - expect(generator.arbPathStrings.elementAt(2), r'lib\l10n\app_zh.arb'); - } else { - expect(generator.arbPathStrings.first, 'lib/l10n/app_en.arb'); - expect(generator.arbPathStrings.elementAt(1), 'lib/l10n/app_es.arb'); - expect(generator.arbPathStrings.elementAt(2), 'lib/l10n/app_zh.arb'); - } + expect(generator.arbPathStrings.first, path.join('lib', 'l10n', 'app_en.arb')); + expect(generator.arbPathStrings.elementAt(1), path.join('lib', 'l10n', 'app_es.arb')); + expect(generator.arbPathStrings.elementAt(2), path.join('lib', 'l10n', 'app_zh.arb')); }); test('correctly parses @@locale property in arb file', () { @@ -904,7 +937,7 @@ void main() { }); }); - group('generateCode', () { + group('writeOutputFiles', () { test('should generate a file per language', () { const String singleEnCaMessageArbFileString = ''' { @@ -924,7 +957,7 @@ void main() { classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFile(); + generator.writeOutputFiles(); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -957,7 +990,7 @@ void main() { preferredSupportedLocaleString: preferredSupportedLocaleString, ); generator.loadResources(); - generator.writeOutputFile(); + generator.writeOutputFiles(); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -988,7 +1021,7 @@ import 'output-localization-file_zh.dart'; useDeferredLoading: true, ); generator.loadResources(); - generator.writeOutputFile(); + generator.writeOutputFiles(); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -1033,7 +1066,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('asdf')); expect(e.message, contains('springStartDate')); @@ -1072,7 +1105,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('the "format" attribute needs to be set')); return; @@ -1110,7 +1143,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('asdf')); expect(e.message, contains('progress')); @@ -1147,7 +1180,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); return; @@ -1180,7 +1213,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); return; @@ -1209,7 +1242,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('Resource attribute "@helloWorlds" was not found')); return; @@ -1241,7 +1274,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('is not properly formatted')); expect(e.message, contains('Ensure that it is a map with string valued keys')); @@ -1274,7 +1307,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on FormatException catch (e) { expect(e.message, contains('Unexpected character')); return; @@ -1306,7 +1339,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('Resource attribute "@title" was not found')); return; @@ -1342,7 +1375,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('Invalid ARB resource name')); return; @@ -1374,7 +1407,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('Invalid ARB resource name')); return; @@ -1406,7 +1439,7 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e classNameString: defaultClassNameString, ); generator.loadResources(); - generator.generateCode(); + generator.writeOutputFiles(); } on L10nException catch (e) { expect(e.message, contains('Invalid ARB resource name')); return;