[gen-l10n] Fix untranslated messages (#68553)
diff --git a/dev/tools/localization/bin/gen_l10n.dart b/dev/tools/localization/bin/gen_l10n.dart deleted file mode 100644 index bbd0a60..0000000 --- a/dev/tools/localization/bin/gen_l10n.dart +++ /dev/null
@@ -1,29 +0,0 @@ -// 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'; - -/// Runs `flutter generate_localizations with arguments passed in. -/// -/// This script exists as a legacy entrypoint, since existing users of -/// gen_l10n tool used to call -/// `dart ${FLUTTER}/dev/tools/localizations/bin/gen_l10n.dart <options>` to -/// generate their Flutter project's localizations resources. -/// -/// Now, the appropriate way to use this tool is to either define an `l10n.yaml` -/// file in the Flutter project repository, or call -/// `flutter generate_localizations <options>`, since the code has moved -/// into `flutter_tools`. -Future<void> main(List<String> rawArgs) async { - final ProcessResult result = await Process.run( - 'flutter', - <String>[ - 'generate_localizations', - ...rawArgs, - ], - ); - - stdout.write(result.stdout); - stderr.write(result.stderr); -}
diff --git a/packages/flutter_tools/lib/src/build_system/targets/localizations.dart b/packages/flutter_tools/lib/src/build_system/targets/localizations.dart index b3d4093..35703f7 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/localizations.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/localizations.dart
@@ -62,13 +62,10 @@ useDeferredLoading: options.deferredLoading ?? false, useSyntheticPackage: options.useSyntheticPackage ?? true, areResourceAttributesRequired: options.areResourceAttributesRequired ?? false, + untranslatedMessagesFile: options?.untranslatedMessagesFile?.toFilePath(), ) ..loadResources() - ..writeOutputFiles() - ..outputUnimplementedMessages( - options?.untranslatedMessagesFile?.toFilePath(), - logger, - ); + ..writeOutputFiles(logger); } on L10nException catch (e) { logger.printError(e.message); throw Exception();
diff --git a/packages/flutter_tools/lib/src/commands/generate_localizations.dart b/packages/flutter_tools/lib/src/commands/generate_localizations.dart index 6df5f5e..a665ec8 100644 --- a/packages/flutter_tools/lib/src/commands/generate_localizations.dart +++ b/packages/flutter_tools/lib/src/commands/generate_localizations.dart
@@ -206,10 +206,10 @@ useSyntheticPackage: useSyntheticPackage, projectPathString: projectPathString, areResourceAttributesRequired: areResourceAttributesRequired, + untranslatedMessagesFile: untranslatedMessagesFile, ) ..loadResources() - ..writeOutputFiles() - ..outputUnimplementedMessages(untranslatedMessagesFile, globals.logger); + ..writeOutputFiles(globals.logger); } on L10nException catch (e) { throwToolExit(e.message); }
diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart index 5713ce3..f120320 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
@@ -520,6 +520,10 @@ /// classes. String _generatedLocalizationsFile; + /// A generated file that will contain the list of messages for each locale + /// that do not have a translation yet. + File _untranslatedMessagesFile; + /// The file that contains the list of inputs and outputs for generating /// localizations. File _inputsAndOutputsListFile; @@ -554,6 +558,7 @@ bool useSyntheticPackage = true, String projectPathString, bool areResourceAttributesRequired = false, + String untranslatedMessagesFile, }) { _useSyntheticPackage = useSyntheticPackage; setProjectDir(projectPathString); @@ -564,6 +569,7 @@ setPreferredSupportedLocales(preferredSupportedLocale); _setHeader(headerString, headerFile); _setUseDeferredLoading(useDeferredLoading); + _setUntranslatedMessagesFile(untranslatedMessagesFile); className = classNameString; _setInputsAndOutputsListFile(inputsAndOutputsListPath); _areResourceAttributesRequired = areResourceAttributesRequired; @@ -763,6 +769,16 @@ _useDeferredLoading = useDeferredLoading; } + void _setUntranslatedMessagesFile(String untranslatedMessagesFileString) { + if (untranslatedMessagesFileString == null || untranslatedMessagesFileString.isEmpty) { + return; + } + + _untranslatedMessagesFile = _fs.file( + globals.fs.path.join(untranslatedMessagesFileString), + ); + } + void _setInputsAndOutputsListFile(String inputsAndOutputsListPath) { if (inputsAndOutputsListPath == null) { return; @@ -1017,7 +1033,7 @@ .replaceAll('@(delegateClass)', delegateClass); } - void writeOutputFiles() { + void writeOutputFiles(Logger logger) { // First, generate the string contents of all necessary files. _generateCode(); @@ -1054,6 +1070,20 @@ }); baseOutputFile.writeAsStringSync(_generatedLocalizationsFile); + + if (_untranslatedMessagesFile != null) { + _generateUntranslatedMessagesFile(logger); + } else if (_unimplementedMessages.isNotEmpty) { + _unimplementedMessages.forEach((LocaleInfo locale, List<String> messages) { + logger.printStatus('"$locale": ${messages.length} untranslated message(s).'); + }); + logger.printStatus( + 'To see a detailed report, use the --untranslated-messages-file \n' + 'option in the tool to generate a JSON format file containing \n' + 'all messages that need to be translated.' + ); + } + if (_inputsAndOutputsListFile != null) { _outputFileList.add(baseOutputFile.absolute.path); @@ -1071,34 +1101,21 @@ } } - void outputUnimplementedMessages(String untranslatedMessagesFile, Logger logger) { + void _generateUntranslatedMessagesFile(Logger logger) { if (logger == null) { throw L10nException( 'Logger must be defined when generating untranslated messages file.' ); } - if (untranslatedMessagesFile == null || untranslatedMessagesFile == '') { - _unimplementedMessages.forEach((LocaleInfo locale, List<String> messages) { - logger.printStatus('"$locale": ${messages.length} untranslated message(s).'); - }); - logger.printStatus( - 'To see a detailed report, use the --untranslated-messages-file \n' - 'option in the tool to generate a JSON format file containing \n' - 'all messages that need to be translated.' - ); - } else { - _writeUnimplementedMessagesFile(untranslatedMessagesFile); - } - } - - void _writeUnimplementedMessagesFile(String untranslatedMessagesFile) { if (_unimplementedMessages.isEmpty) { + _untranslatedMessagesFile.writeAsStringSync('{}'); + if (_inputsAndOutputsListFile != null) { + _outputFileList.add(_untranslatedMessagesFile.absolute.path); + } return; } - final File unimplementedMessageTranslationsFile = _fs.file(untranslatedMessagesFile); - String resultingFile = '{\n'; int count = 0; final int numberOfLocales = _unimplementedMessages.length; @@ -1122,6 +1139,9 @@ }); resultingFile += '}\n'; - unimplementedMessageTranslationsFile.writeAsStringSync(resultingFile); + _untranslatedMessagesFile.writeAsStringSync(resultingFile); + if (_inputsAndOutputsListFile != null) { + _outputFileList.add(_untranslatedMessagesFile.absolute.path); + } } }
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart index 218d3bb..969a0f3 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart
@@ -66,11 +66,11 @@ useSyntheticPackage: false, projectPathString: '/', areResourceAttributesRequired: true, + untranslatedMessagesFile: 'untranslated', ), ).called(1); verify(mockLocalizationsGenerator.loadResources()).called(1); - verify(mockLocalizationsGenerator.writeOutputFiles()).called(1); - verify(mockLocalizationsGenerator.outputUnimplementedMessages('untranslated', logger)).called(1); + verify(mockLocalizationsGenerator.writeOutputFiles(logger)).called(1); }); testUsingContext('generateLocalizations throws exception on missing flutter: generate: true flag', () async {
diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart index 8761eba..22b58ed 100644 --- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
@@ -48,6 +48,11 @@ { "title": "Título" }'''; +const String twoEsMessageArbFileString = ''' +{ + "title": "Título", + "subtitle": "Subtitular" +}'''; const String singleZhMessageArbFileString = ''' { "title": "标题" @@ -232,7 +237,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { throw TestFailure('Unexpected failure during test setup: ${e.message}'); } on Exception catch (e) { @@ -428,7 +433,7 @@ classNameString: defaultClassNameString, ) ..loadResources() - ..writeOutputFiles(); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -465,7 +470,7 @@ fail('Using app_en_CA_foo.arb should fail as it is not a valid locale.'); }); - test('correctly creates an unimplemented messages file', () { + test('correctly creates an untranslated messages file (useSyntheticPackage = true)', () { fs.currentDirectory.childDirectory('lib').childDirectory('l10n') ..createSync(recursive: true) ..childFile(defaultTemplateArbFileName).writeAsStringSync(twoMessageArbFileString) @@ -481,13 +486,10 @@ templateArbFileName: defaultTemplateArbFileName, outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, + untranslatedMessagesFile: fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'), ) ..loadResources() - ..writeOutputFiles() - ..outputUnimplementedMessages( - fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'), - BufferLogger.test(), - ); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -506,6 +508,139 @@ expect(unimplementedOutputString, contains('subtitle')); }); + test('correctly creates an untranslated messages file (useSyntheticPackage = false)', () { + fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true) + ..childFile(defaultTemplateArbFileName).writeAsStringSync(twoMessageArbFileString) + ..childFile(esArbFileName).writeAsStringSync(singleEsMessageArbFileString); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + outputPathString: defaultL10nPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + useSyntheticPackage: false, + untranslatedMessagesFile: fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'), + ) + ..loadResources() + ..writeOutputFiles(BufferLogger.test()); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } + + final File unimplementedOutputFile = fs.file( + fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'), + ); + final String unimplementedOutputString = unimplementedOutputFile.readAsStringSync(); + try { + // Since ARB file is essentially JSON, decoding it should not fail. + json.decode(unimplementedOutputString); + } on Exception { + fail('Parsing arb file should not fail'); + } + expect(unimplementedOutputString, contains('es')); + expect(unimplementedOutputString, contains('subtitle')); + }); + + test( + 'untranslated messages suggestion is printed when translation is missing', + () { + final BufferLogger testLogger = BufferLogger.test(); + fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true) + ..childFile(defaultTemplateArbFileName).writeAsStringSync(twoMessageArbFileString) + ..childFile(esArbFileName).writeAsStringSync(singleEsMessageArbFileString); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + outputPathString: defaultL10nPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ) + ..loadResources() + ..writeOutputFiles(testLogger); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } + + expect(testLogger.statusText, contains('To see a detailed report, use the --untranslated-messages-file')); + }, + ); + + test( + 'unimplemented messages suggestion is not printed when all messages ' + 'are fully translated', + () { + final BufferLogger testLogger = BufferLogger.test(); + fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true) + ..childFile(defaultTemplateArbFileName).writeAsStringSync(twoMessageArbFileString) + ..childFile(esArbFileName).writeAsStringSync(twoMessageArbFileString); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + outputPathString: defaultL10nPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + ) + ..loadResources() + ..writeOutputFiles(testLogger); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } + + expect(testLogger.statusText, ''); + }, + ); + + test('untranslated messages file included in generated JSON list of outputs', () { + _standardFlutterDirectoryL10nSetup(fs); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + inputsAndOutputsListPath: syntheticL10nPackagePath, + untranslatedMessagesFile: fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'), + ) + ..loadResources() + ..writeOutputFiles(BufferLogger.test()); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } + + final File inputsAndOutputsList = fs.file( + fs.path.join(syntheticL10nPackagePath, 'gen_l10n_inputs_and_outputs.json'), + ); + expect(inputsAndOutputsList.existsSync(), isTrue); + final Map<String, dynamic> jsonResult = json.decode( + inputsAndOutputsList.readAsStringSync(), + ) as Map<String, dynamic>; + expect(jsonResult.containsKey('outputs'), isTrue); + final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>; + expect(outputList, contains(contains('unimplemented_message_translations.json'))); + }); + test( 'uses inputPathString as outputPathString when the outputPathString is ' 'null while not using the synthetic package option', @@ -524,7 +659,7 @@ useSyntheticPackage: false, ) ..loadResources() - ..writeOutputFiles(); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -567,7 +702,7 @@ useSyntheticPackage: false, ) ..loadResources() - ..writeOutputFiles(); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -599,7 +734,7 @@ useSyntheticPackage: false, ) ..loadResources() - ..writeOutputFiles(); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -627,7 +762,7 @@ inputsAndOutputsListPath: syntheticL10nPackagePath, ) ..loadResources() - ..writeOutputFiles(); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -1074,6 +1209,7 @@ test('message without placeholders - should generate code comment with description and template message translation', () { _standardFlutterDirectoryL10nSetup(fs); final LocalizationsGenerator generator = LocalizationsGenerator(fs); + final BufferLogger testLogger = BufferLogger.test(); try { generator.initialize( inputPathString: defaultL10nPathString, @@ -1083,7 +1219,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(testLogger); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -1116,9 +1252,9 @@ l10nDirectory.childFile(esArbFileName) .writeAsStringSync(singleEsMessageArbFileString); - - final LocalizationsGenerator generator = LocalizationsGenerator(fs); + final BufferLogger testLogger = BufferLogger.test(); + try { generator.initialize( inputPathString: defaultL10nPathString, @@ -1128,7 +1264,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(testLogger); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -1171,6 +1307,7 @@ }'''); final LocalizationsGenerator generator = LocalizationsGenerator(fs); + final BufferLogger testLogger = BufferLogger.test(); try { generator.initialize( inputPathString: defaultL10nPathString, @@ -1180,7 +1317,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(testLogger); } on Exception catch (e) { final L10nException exception = e as L10nException; print(exception.message); @@ -1220,7 +1357,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -1253,7 +1390,7 @@ preferredSupportedLocale: preferredSupportedLocale, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -1283,7 +1420,7 @@ useDeferredLoading: true, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on Exception catch (e) { fail('Generating output files should not fail: $e'); } @@ -1328,7 +1465,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('asdf')); expect(e.message, contains('springStartDate')); @@ -1367,7 +1504,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('the "format" attribute needs to be set')); return; @@ -1405,7 +1542,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('asdf')); expect(e.message, contains('progress')); @@ -1442,7 +1579,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); return; @@ -1475,7 +1612,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('Check to see if the plural message is in the proper ICU syntax format')); return; @@ -1504,7 +1641,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('Resource attribute "@helloWorlds" was not found')); return; @@ -1536,7 +1673,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } 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')); @@ -1572,7 +1709,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('app_en.arb')); expect(e.message, contains('FormatException')); @@ -1608,7 +1745,7 @@ areResourceAttributesRequired: true, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('Resource attribute "@title" was not found')); return; @@ -1644,7 +1781,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('Invalid ARB resource name')); return; @@ -1676,7 +1813,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('Invalid ARB resource name')); return; @@ -1707,7 +1844,7 @@ classNameString: defaultClassNameString, ); generator.loadResources(); - generator.writeOutputFiles(); + generator.writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { expect(e.message, contains('Invalid ARB resource name')); return; @@ -1731,7 +1868,7 @@ classNameString: defaultClassNameString, ) ..loadResources() - ..writeOutputFiles(); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); } @@ -1767,7 +1904,7 @@ classNameString: defaultClassNameString, ) ..loadResources() - ..writeOutputFiles(); + ..writeOutputFiles(BufferLogger.test()); } on L10nException catch (e) { fail('Generating output should not fail: \n${e.message}'); }
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index d49e250..824387e 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -1444,7 +1444,7 @@ await residentRunner.runSourceGenerators(); expect(testLogger.errorText, isEmpty); - expect(testLogger.statusText, contains('use the --untranslated-messages-file')); + expect(testLogger.statusText, isEmpty); })); testUsingContext('ResidentRunner can run source generation - generation fails', () => testbed.run(() async {