| // 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 'package:file/memory.dart'; |
| import 'package:flutter_tools/src/artifacts.dart'; |
| import 'package:flutter_tools/src/base/file_system.dart'; |
| import 'package:flutter_tools/src/base/logger.dart'; |
| import 'package:flutter_tools/src/convert.dart'; |
| import 'package:flutter_tools/src/localizations/gen_l10n.dart'; |
| import 'package:flutter_tools/src/localizations/gen_l10n_types.dart'; |
| import 'package:flutter_tools/src/localizations/localizations_utils.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import '../src/common.dart'; |
| import '../src/fake_process_manager.dart'; |
| |
| const String defaultTemplateArbFileName = 'app_en.arb'; |
| const String defaultOutputFileString = 'output-localization-file.dart'; |
| const String defaultClassNameString = 'AppLocalizations'; |
| const String singleMessageArbFileString = ''' |
| { |
| "title": "Title", |
| "@title": { |
| "description": "Title for the application." |
| } |
| }'''; |
| const String twoMessageArbFileString = ''' |
| { |
| "title": "Title", |
| "@title": { |
| "description": "Title for the application." |
| }, |
| "subtitle": "Subtitle", |
| "@subtitle": { |
| "description": "Subtitle for the application." |
| } |
| }'''; |
| const String esArbFileName = 'app_es.arb'; |
| const String singleEsMessageArbFileString = ''' |
| { |
| "title": "Título" |
| }'''; |
| const String singleZhMessageArbFileString = ''' |
| { |
| "title": "标题" |
| }'''; |
| const String intlImportDartCode = ''' |
| import 'package:intl/intl.dart' as intl; |
| '''; |
| const String foundationImportDartCode = ''' |
| import 'package:flutter/foundation.dart'; |
| '''; |
| |
| 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); |
| fs.file('pubspec.yaml') |
| ..createSync(recursive: true) |
| ..writeAsStringSync(''' |
| flutter: |
| generate: true |
| '''); |
| |
| } |
| |
| void main() { |
| late MemoryFileSystem fs; |
| late BufferLogger logger; |
| late Artifacts artifacts; |
| late ProcessManager processManager; |
| late String defaultL10nPathString; |
| late String syntheticPackagePath; |
| late String syntheticL10nPackagePath; |
| |
| LocalizationsGenerator setupLocalizations( |
| Map<String, String> localeToArbFile, |
| { |
| String? yamlFile, |
| String? outputPathString, |
| String? outputFileString, |
| String? headerString, |
| String? headerFile, |
| String? untranslatedMessagesFile, |
| bool useSyntheticPackage = true, |
| bool isFromYaml = false, |
| bool usesNullableGetter = true, |
| String? inputsAndOutputsListPath, |
| List<String>? preferredSupportedLocales, |
| bool useDeferredLoading = false, |
| bool useEscaping = false, |
| bool areResourceAttributeRequired = false, |
| bool suppressWarnings = false, |
| bool relaxSyntax = false, |
| void Function(Directory)? setup, |
| } |
| ) { |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| for (final String locale in localeToArbFile.keys) { |
| l10nDirectory.childFile('app_$locale.arb') |
| .writeAsStringSync(localeToArbFile[locale]!); |
| } |
| if (setup != null) { |
| setup(l10nDirectory); |
| } |
| return LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: l10nDirectory.path, |
| outputPathString: outputPathString ?? l10nDirectory.path, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: outputFileString ?? defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| headerString: headerString, |
| headerFile: headerFile, |
| logger: logger, |
| untranslatedMessagesFile: untranslatedMessagesFile, |
| useSyntheticPackage: useSyntheticPackage, |
| inputsAndOutputsListPath: inputsAndOutputsListPath, |
| usesNullableGetter: usesNullableGetter, |
| preferredSupportedLocales: preferredSupportedLocales, |
| useDeferredLoading: useDeferredLoading, |
| useEscaping: useEscaping, |
| areResourceAttributesRequired: areResourceAttributeRequired, |
| suppressWarnings: suppressWarnings, |
| useRelaxedSyntax: relaxSyntax, |
| ) |
| ..loadResources() |
| ..writeOutputFiles(isFromYaml: isFromYaml); |
| } |
| |
| String getGeneratedFileContent({String? locale}) { |
| final String fileName = locale == null ? 'output-localization-file.dart' : 'output-localization-file_$locale.dart'; |
| return fs.file( |
| fs.path.join(syntheticL10nPackagePath, fileName) |
| ).readAsStringSync(); |
| } |
| |
| setUp(() { |
| fs = MemoryFileSystem.test(); |
| logger = BufferLogger.test(); |
| artifacts = Artifacts.test(); |
| processManager = FakeProcessManager.empty(); |
| |
| defaultL10nPathString = fs.path.join('lib', 'l10n'); |
| syntheticPackagePath = fs.path.join('.dart_tool', 'flutter_gen'); |
| syntheticL10nPackagePath = fs.path.join(syntheticPackagePath, 'gen_l10n'); |
| precacheLanguageAndRegionTags(); |
| }); |
| |
| group('Setters', () { |
| testWithoutContext('setInputDirectory fails if the directory does not exist', () { |
| expect( |
| () => LocalizationsGenerator.inputDirectoryFromPath(fs, 'lib', fs.directory('bogus')), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Make sure that the correct path was provided'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('setting className fails if input string is empty', () { |
| _standardFlutterDirectoryL10nSetup(fs); |
| expect( |
| () => LocalizationsGenerator.classNameFromString(''), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('cannot be empty'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('sets absolute path of the target Flutter project', () { |
| // Set up project directory. |
| final Directory l10nDirectory = fs.currentDirectory |
| .childDirectory('absolute') |
| .childDirectory('path') |
| .childDirectory('to') |
| .childDirectory('flutter_project') |
| .childDirectory('lib') |
| .childDirectory('l10n') |
| ..createSync(recursive: true); |
| l10nDirectory.childFile(defaultTemplateArbFileName) |
| .writeAsStringSync(singleMessageArbFileString); |
| l10nDirectory.childFile(esArbFileName) |
| .writeAsStringSync(singleEsMessageArbFileString); |
| |
| // Run localizations generator in specified absolute path. |
| final String flutterProjectPath = fs.path.join('absolute', 'path', 'to', 'flutter_project'); |
| LocalizationsGenerator( |
| fileSystem: fs, |
| projectPathString: flutterProjectPath, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ) |
| ..loadResources() |
| ..writeOutputFiles(); |
| |
| // Output files should be generated in the provided absolute path. |
| expect( |
| fs.isFileSync(fs.path.join( |
| flutterProjectPath, |
| '.dart_tool', |
| 'flutter_gen', |
| 'gen_l10n', |
| 'output-localization-file_en.dart', |
| )), |
| true, |
| ); |
| expect( |
| fs.isFileSync(fs.path.join( |
| flutterProjectPath, |
| '.dart_tool', |
| 'flutter_gen', |
| 'gen_l10n', |
| 'output-localization-file_es.dart', |
| )), |
| true, |
| ); |
| }); |
| |
| testWithoutContext('throws error when directory at absolute path does not exist', () { |
| // Set up project directory. |
| final Directory l10nDirectory = fs.currentDirectory |
| .childDirectory('lib') |
| .childDirectory('l10n') |
| ..createSync(recursive: true); |
| l10nDirectory.childFile(defaultTemplateArbFileName) |
| .writeAsStringSync(singleMessageArbFileString); |
| l10nDirectory.childFile(esArbFileName) |
| .writeAsStringSync(singleEsMessageArbFileString); |
| |
| // Project path should be intentionally a directory that does not exist. |
| expect( |
| () => LocalizationsGenerator( |
| fileSystem: fs, |
| projectPathString: 'absolute/path/to/flutter_project', |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Directory does not exist'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('throws error when arb file does not exist', () { |
| // Set up project directory. |
| fs.currentDirectory |
| .childDirectory('lib') |
| .childDirectory('l10n') |
| .createSync(recursive: true); |
| |
| // Arb file should be nonexistent in the l10n directory. |
| expect( |
| () => LocalizationsGenerator( |
| fileSystem: fs, |
| projectPathString: './', |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains(', does not exist.'), |
| )), |
| ); |
| }); |
| |
| group('className should only take valid Dart class names', () { |
| setUp(() { |
| _standardFlutterDirectoryL10nSetup(fs); |
| }); |
| |
| testWithoutContext('fails on string with spaces', () { |
| expect( |
| () => LocalizationsGenerator.classNameFromString('String with spaces'), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('is not a valid public Dart class name'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('fails on non-alphanumeric symbols', () { |
| expect( |
| () => LocalizationsGenerator.classNameFromString('TestClass@123'), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('is not a valid public Dart class name'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('fails on camel-case', () { |
| expect( |
| () => LocalizationsGenerator.classNameFromString('camelCaseClassName'), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('is not a valid public Dart class name'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('fails when starting with a number', () { |
| expect( |
| () => LocalizationsGenerator.classNameFromString('123ClassName'), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('is not a valid public Dart class name'), |
| )), |
| ); |
| }); |
| }); |
| }); |
| |
| testWithoutContext('correctly adds a headerString when it is set', () { |
| final LocalizationsGenerator generator = setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, headerString: '/// Sample header'); |
| expect(generator.header, '/// Sample header'); |
| }); |
| |
| testWithoutContext('correctly adds a headerFile when it is set', () { |
| final LocalizationsGenerator generator = setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, headerFile: 'header.txt', setup: (Directory l10nDirectory) { |
| l10nDirectory.childFile('header.txt').writeAsStringSync('/// Sample header in a text file'); |
| }); |
| expect(generator.header, '/// Sample header in a text file'); |
| }); |
| |
| testWithoutContext('sets templateArbFileName with more than one underscore correctly', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }); |
| final Directory outputDirectory = fs.directory(syntheticL10nPackagePath); |
| expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); |
| }); |
| |
| testWithoutContext('filenames with invalid locales should not be recognized', () { |
| expect( |
| () { |
| // This attempts to create 'app_localizations_en_CA_foo.arb'. |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'en_CA_foo': singleMessageArbFileString, |
| }); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains("The following .arb file's locale could not be determined"), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('correctly creates an untranslated messages file (useSyntheticPackage = true)', () { |
| final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'); |
| setupLocalizations(<String, String>{ |
| 'en': twoMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, untranslatedMessagesFile: untranslatedMessagesFilePath); |
| final String unimplementedOutputString = fs.file(untranslatedMessagesFilePath).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')); |
| }); |
| |
| testWithoutContext('correctly creates an untranslated messages file (useSyntheticPackage = false)', () { |
| final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'); |
| setupLocalizations(<String, String>{ |
| 'en': twoMessageArbFileString, |
| 'es': singleMessageArbFileString, |
| }, useSyntheticPackage: false, untranslatedMessagesFile: untranslatedMessagesFilePath); |
| final String unimplementedOutputString = fs.file(untranslatedMessagesFilePath).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')); |
| }); |
| |
| testWithoutContext( |
| 'untranslated messages suggestion is printed when translation is missing: ' |
| 'command line message', |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': twoMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }); |
| expect( |
| logger.statusText, |
| contains('To see a detailed report, use the --untranslated-messages-file'), |
| ); |
| expect( |
| logger.statusText, |
| contains('flutter gen-l10n --untranslated-messages-file=desiredFileName.txt'), |
| ); |
| }, |
| ); |
| |
| testWithoutContext( |
| 'untranslated messages suggestion is printed when translation is missing: ' |
| 'l10n.yaml message', |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': twoMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, isFromYaml: true); |
| expect( |
| logger.statusText, |
| contains('To see a detailed report, use the untranslated-messages-file'), |
| ); |
| expect( |
| logger.statusText, |
| contains('untranslated-messages-file: desiredFileName.txt'), |
| ); |
| }, |
| ); |
| |
| testWithoutContext( |
| 'unimplemented messages suggestion is not printed when all messages ' |
| 'are fully translated', |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': twoMessageArbFileString, |
| 'es': twoMessageArbFileString, |
| }); |
| expect(logger.statusText, equals('')); |
| }, |
| ); |
| |
| testWithoutContext('untranslated messages file included in generated JSON list of outputs', () { |
| final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json'); |
| setupLocalizations( |
| <String, String>{ |
| 'en': twoMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, |
| untranslatedMessagesFile: untranslatedMessagesFilePath, |
| inputsAndOutputsListPath: syntheticL10nPackagePath, |
| ); |
| 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'))); |
| }); |
| |
| testWithoutContext( |
| 'uses inputPathString as outputPathString when the outputPathString is ' |
| 'null while not using the synthetic package option', |
| () { |
| _standardFlutterDirectoryL10nSetup(fs); |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| // outputPathString is intentionally not defined |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| useSyntheticPackage: false, |
| logger: logger, |
| ) |
| ..loadResources() |
| ..writeOutputFiles(); |
| |
| final Directory outputDirectory = fs.directory('lib').childDirectory('l10n'); |
| expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); |
| }, |
| ); |
| |
| testWithoutContext( |
| 'correctly generates output files in non-default output directory if it ' |
| 'already exists while not using the synthetic package option', |
| () { |
| final Directory l10nDirectory = fs.currentDirectory |
| .childDirectory('lib') |
| .childDirectory('l10n') |
| ..createSync(recursive: true); |
| // Create the directory 'lib/l10n/output'. |
| l10nDirectory.childDirectory('output'); |
| |
| l10nDirectory |
| .childFile(defaultTemplateArbFileName) |
| .writeAsStringSync(singleMessageArbFileString); |
| l10nDirectory |
| .childFile(esArbFileName) |
| .writeAsStringSync(singleEsMessageArbFileString); |
| |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: fs.path.join('lib', 'l10n', 'output'), |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| useSyntheticPackage: false, |
| logger: logger, |
| ) |
| ..loadResources() |
| ..writeOutputFiles(); |
| |
| final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); |
| expect(outputDirectory.existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); |
| }, |
| ); |
| |
| testWithoutContext( |
| 'correctly creates output directory if it does not exist and writes files ' |
| 'in it while not using the synthetic package option', |
| () { |
| _standardFlutterDirectoryL10nSetup(fs); |
| |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: fs.path.join('lib', 'l10n', 'output'), |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| useSyntheticPackage: false, |
| logger: logger, |
| ) |
| ..loadResources() |
| ..writeOutputFiles(); |
| |
| final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); |
| expect(outputDirectory.existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); |
| expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); |
| }, |
| ); |
| |
| testWithoutContext( |
| 'generates nullable localizations class getter via static `of` method ' |
| 'by default', |
| () { |
| final LocalizationsGenerator generator = setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }); |
| expect(generator.outputDirectory.existsSync(), isTrue); |
| expect(generator.outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); |
| expect( |
| generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), |
| contains('static AppLocalizations? of(BuildContext context)'), |
| ); |
| expect( |
| generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), |
| contains('return Localizations.of<AppLocalizations>(context, AppLocalizations);'), |
| ); |
| }, |
| ); |
| |
| testWithoutContext( |
| 'can generate non-nullable localizations class getter via static `of` method ', |
| () { |
| final LocalizationsGenerator generator = setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, usesNullableGetter: false); |
| expect(generator.outputDirectory.existsSync(), isTrue); |
| expect(generator.outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); |
| expect( |
| generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), |
| contains('static AppLocalizations of(BuildContext context)'), |
| ); |
| expect( |
| generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), |
| contains('return Localizations.of<AppLocalizations>(context, AppLocalizations)!;'), |
| ); |
| }, |
| ); |
| |
| testWithoutContext('creates list of inputs and outputs when file path is specified', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, inputsAndOutputsListPath: syntheticL10nPackagePath); |
| 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('inputs'), isTrue); |
| final List<dynamic> inputList = jsonResult['inputs'] as List<dynamic>; |
| 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<dynamic> outputList = jsonResult['outputs'] as List<dynamic>; |
| expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file.dart'))); |
| expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_en.dart'))); |
| expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_es.dart'))); |
| }); |
| |
| testWithoutContext('setting both a headerString and a headerFile should fail', () { |
| expect( |
| () { |
| setupLocalizations( |
| <String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, |
| headerString: '/// Sample header in a text file', |
| headerFile: 'header.txt', |
| setup: (Directory l10nDirectory) { |
| l10nDirectory.childFile('header.txt').writeAsStringSync('/// Sample header in a text file'); |
| }, |
| ); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Cannot accept both header and header file arguments'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('setting a headerFile that does not exist should fail', () { |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, headerFile: 'header.txt'); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Failed to read header file'), |
| )), |
| ); |
| }); |
| |
| group('generateLocalizations', () { |
| // Regression test for https://github.com/flutter/flutter/issues/119593 |
| testWithoutContext('other logs from flutter_tools does not affect gen-l10n', () async { |
| _standardFlutterDirectoryL10nSetup(fs); |
| |
| final Logger logger = BufferLogger.test(); |
| logger.printError('An error output from a different tool in flutter_tools'); |
| |
| // Should run without error. |
| await generateLocalizations( |
| fileSystem: fs, |
| options: LocalizationOptions( |
| arbDir: Uri.directory(defaultL10nPathString).path, |
| outputDir: Uri.directory(defaultL10nPathString, windows: false).path, |
| templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path, |
| syntheticPackage: false, |
| ), |
| logger: logger, |
| projectDir: fs.currentDirectory, |
| dependenciesDir: fs.currentDirectory, |
| artifacts: artifacts, |
| processManager: processManager, |
| ); |
| }); |
| |
| testWithoutContext('forwards arguments correctly', () async { |
| _standardFlutterDirectoryL10nSetup(fs); |
| final LocalizationOptions options = LocalizationOptions( |
| header: 'HEADER', |
| arbDir: Uri.directory(defaultL10nPathString).path, |
| useDeferredLoading: true, |
| outputClass: 'Foo', |
| outputLocalizationFile: Uri.file('bar.dart', windows: false).path, |
| outputDir: Uri.directory(defaultL10nPathString, windows: false).path, |
| preferredSupportedLocales: <String>['es'], |
| templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path, |
| untranslatedMessagesFile: Uri.file('untranslated', windows: false).path, |
| syntheticPackage: false, |
| requiredResourceAttributes: true, |
| nullableGetter: false, |
| ); |
| |
| // Verify that values are correctly passed through the localizations target. |
| final LocalizationsGenerator generator = await generateLocalizations( |
| fileSystem: fs, |
| options: options, |
| logger: logger, |
| projectDir: fs.currentDirectory, |
| dependenciesDir: fs.currentDirectory, |
| artifacts: artifacts, |
| processManager: processManager, |
| ); |
| |
| expect(generator.inputDirectory.path, '/lib/l10n/'); |
| expect(generator.outputDirectory.path, '/lib/l10n/'); |
| expect(generator.templateArbFile.path, '/lib/l10n/app_en.arb'); |
| expect(generator.baseOutputFile.path, '/lib/l10n/bar.dart'); |
| expect(generator.className, 'Foo'); |
| expect(generator.preferredSupportedLocales.single, LocaleInfo.fromString('es')); |
| expect(generator.header, 'HEADER'); |
| expect(generator.useDeferredLoading, isTrue); |
| expect(generator.inputsAndOutputsListFile?.path, '/gen_l10n_inputs_and_outputs.json'); |
| expect(generator.useSyntheticPackage, isFalse); |
| expect(generator.projectDirectory?.path, '/'); |
| expect(generator.areResourceAttributesRequired, isTrue); |
| expect(generator.untranslatedMessagesFile?.path, 'untranslated'); |
| expect(generator.usesNullableGetter, isFalse); |
| |
| // Just validate one file. |
| expect(fs.file('/lib/l10n/bar_en.dart').readAsStringSync(), ''' |
| HEADER |
| |
| import 'bar.dart'; |
| |
| /// The translations for English (`en`). |
| class FooEn extends Foo { |
| FooEn([String locale = 'en']) : super(locale); |
| |
| @override |
| String get title => 'Title'; |
| } |
| '''); |
| }); |
| |
| testWithoutContext('throws exception on missing flutter: generate: true flag', () async { |
| _standardFlutterDirectoryL10nSetup(fs); |
| |
| // Missing flutter: generate: true should throw exception. |
| fs.file('pubspec.yaml') |
| ..createSync(recursive: true) |
| ..writeAsStringSync(''' |
| flutter: |
| uses-material-design: true |
| '''); |
| |
| final LocalizationOptions options = LocalizationOptions( |
| header: 'HEADER', |
| headerFile: Uri.file('header', windows: false).path, |
| arbDir: Uri.file('arb', windows: false).path, |
| useDeferredLoading: true, |
| outputClass: 'Foo', |
| outputLocalizationFile: Uri.file('bar', windows: false).path, |
| preferredSupportedLocales: <String>['en_US'], |
| templateArbFile: Uri.file('example.arb', windows: false).path, |
| untranslatedMessagesFile: Uri.file('untranslated', windows: false).path, |
| ); |
| |
| expect( |
| () => generateLocalizations( |
| fileSystem: fs, |
| options: options, |
| logger: BufferLogger.test(), |
| projectDir: fs.currentDirectory, |
| dependenciesDir: fs.currentDirectory, |
| artifacts: artifacts, |
| processManager: processManager, |
| ), |
| throwsToolExit( |
| message: 'Attempted to generate localizations code without having the ' |
| 'flutter: generate flag turned on.', |
| ), |
| ); |
| }); |
| |
| testWithoutContext('uses the same line terminator as pubspec.yaml', () async { |
| _standardFlutterDirectoryL10nSetup(fs); |
| |
| fs.file('pubspec.yaml') |
| ..createSync(recursive: true) |
| ..writeAsStringSync(''' |
| flutter:\r |
| generate: true\r |
| '''); |
| |
| final LocalizationOptions options = LocalizationOptions( |
| arbDir: fs.path.join('lib', 'l10n'), |
| outputClass: defaultClassNameString, |
| outputLocalizationFile: defaultOutputFileString, |
| ); |
| await generateLocalizations( |
| fileSystem: fs, |
| options: options, |
| logger: BufferLogger.test(), |
| projectDir: fs.currentDirectory, |
| dependenciesDir: fs.currentDirectory, |
| artifacts: artifacts, |
| processManager: processManager, |
| ); |
| final String content = getGeneratedFileContent(locale: 'en'); |
| expect(content, contains('\r\n')); |
| }); |
| |
| testWithoutContext('blank lines generated nicely', () async { |
| _standardFlutterDirectoryL10nSetup(fs); |
| |
| // Test without headers. |
| await generateLocalizations( |
| fileSystem: fs, |
| options: LocalizationOptions( |
| arbDir: Uri.directory(defaultL10nPathString).path, |
| outputDir: Uri.directory(defaultL10nPathString, windows: false).path, |
| templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path, |
| syntheticPackage: false, |
| ), |
| logger: BufferLogger.test(), |
| projectDir: fs.currentDirectory, |
| dependenciesDir: fs.currentDirectory, |
| artifacts: artifacts, |
| processManager: processManager, |
| ); |
| |
| expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), ''' |
| import 'app_localizations.dart'; |
| |
| /// The translations for English (`en`). |
| class AppLocalizationsEn extends AppLocalizations { |
| AppLocalizationsEn([String locale = 'en']) : super(locale); |
| |
| @override |
| String get title => 'Title'; |
| } |
| '''); |
| |
| // Test with headers. |
| await generateLocalizations( |
| fileSystem: fs, |
| options: LocalizationOptions( |
| header: 'HEADER', |
| arbDir: Uri.directory(defaultL10nPathString).path, |
| outputDir: Uri.directory(defaultL10nPathString, windows: false).path, |
| templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path, |
| syntheticPackage: false, |
| ), |
| logger: logger, |
| projectDir: fs.currentDirectory, |
| dependenciesDir: fs.currentDirectory, |
| artifacts: artifacts, |
| processManager: processManager, |
| ); |
| |
| expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), ''' |
| HEADER |
| |
| import 'app_localizations.dart'; |
| |
| /// The translations for English (`en`). |
| class AppLocalizationsEn extends AppLocalizations { |
| AppLocalizationsEn([String locale = 'en']) : super(locale); |
| |
| @override |
| String get title => 'Title'; |
| } |
| '''); |
| }); |
| }); |
| |
| group('loadResources', () { |
| testWithoutContext('correctly initializes supportedLocales and supportedLanguageCodes properties', () { |
| _standardFlutterDirectoryL10nSetup(fs); |
| |
| final LocalizationsGenerator generator = LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ) |
| ..loadResources(); |
| |
| expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true); |
| expect(generator.supportedLocales.contains(LocaleInfo.fromString('es')), true); |
| }); |
| |
| testWithoutContext('correctly sorts supportedLocales and supportedLanguageCodes alphabetically', () { |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| // Write files in non-alphabetical order so that read performs in that order |
| l10nDirectory.childFile('app_zh.arb') |
| .writeAsStringSync(singleZhMessageArbFileString); |
| l10nDirectory.childFile('app_es.arb') |
| .writeAsStringSync(singleEsMessageArbFileString); |
| l10nDirectory.childFile('app_en.arb') |
| .writeAsStringSync(singleMessageArbFileString); |
| |
| final LocalizationsGenerator generator = LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ) |
| ..loadResources(); |
| |
| expect(generator.supportedLocales.first, LocaleInfo.fromString('en')); |
| expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es')); |
| expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('zh')); |
| }); |
| |
| testWithoutContext('adds preferred locales to the top of supportedLocales and supportedLanguageCodes', () { |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| l10nDirectory.childFile('app_en.arb') |
| .writeAsStringSync(singleMessageArbFileString); |
| l10nDirectory.childFile('app_es.arb') |
| .writeAsStringSync(singleEsMessageArbFileString); |
| l10nDirectory.childFile('app_zh.arb') |
| .writeAsStringSync(singleZhMessageArbFileString); |
| |
| const List<String> preferredSupportedLocale = <String>['zh', 'es']; |
| final LocalizationsGenerator generator = LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| preferredSupportedLocales: preferredSupportedLocale, |
| logger: logger, |
| ) |
| ..loadResources(); |
| |
| expect(generator.supportedLocales.first, LocaleInfo.fromString('zh')); |
| expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es')); |
| expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('en')); |
| }); |
| |
| testWithoutContext( |
| 'throws an error attempting to add preferred locales when there is no corresponding arb file for that locale', |
| () { |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| l10nDirectory.childFile('app_en.arb') |
| .writeAsStringSync(singleMessageArbFileString); |
| l10nDirectory.childFile('app_es.arb') |
| .writeAsStringSync(singleEsMessageArbFileString); |
| l10nDirectory.childFile('app_zh.arb') |
| .writeAsStringSync(singleZhMessageArbFileString); |
| |
| const List<String> preferredSupportedLocale = <String>['am', 'es']; |
| expect( |
| () { |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| preferredSupportedLocales: preferredSupportedLocale, |
| logger: logger, |
| ).loadResources(); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains("The preferred supported locale, 'am', cannot be added."), |
| )), |
| ); |
| }, |
| ); |
| |
| testWithoutContext('correctly sorts arbPathString alphabetically', () { |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| // Write files in non-alphabetical order so that read performs in that order |
| l10nDirectory.childFile('app_zh.arb') |
| .writeAsStringSync(singleZhMessageArbFileString); |
| l10nDirectory.childFile('app_es.arb') |
| .writeAsStringSync(singleEsMessageArbFileString); |
| l10nDirectory.childFile('app_en.arb') |
| .writeAsStringSync(singleMessageArbFileString); |
| |
| final LocalizationsGenerator generator = LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: defaultTemplateArbFileName, |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ) |
| ..loadResources(); |
| |
| expect(generator.arbPathStrings.first, fs.path.join('lib', 'l10n', 'app_en.arb')); |
| expect(generator.arbPathStrings.elementAt(1), fs.path.join('lib', 'l10n', 'app_es.arb')); |
| expect(generator.arbPathStrings.elementAt(2), fs.path.join('lib', 'l10n', 'app_zh.arb')); |
| }); |
| |
| testWithoutContext('correctly parses @@locale property in arb file', () { |
| const String arbFileWithEnLocale = ''' |
| { |
| "@@locale": "en", |
| "title": "Title", |
| "@title": { |
| "description": "Title for the application" |
| } |
| }'''; |
| |
| const String arbFileWithZhLocale = ''' |
| { |
| "@@locale": "zh", |
| "title": "标题", |
| "@title": { |
| "description": "Title for the 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); |
| |
| final LocalizationsGenerator generator = LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: 'first_file.arb', |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ) |
| ..loadResources(); |
| |
| expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true); |
| expect(generator.supportedLocales.contains(LocaleInfo.fromString('zh')), true); |
| }); |
| |
| testWithoutContext('correctly requires @@locale property in arb file to match the filename locale suffix', () { |
| const String arbFileWithEnLocale = ''' |
| { |
| "@@locale": "en", |
| "title": "Stocks", |
| "@title": { |
| "description": "Title for the Stocks application" |
| } |
| }'''; |
| |
| const String arbFileWithZhLocale = ''' |
| { |
| "@@locale": "zh", |
| "title": "标题", |
| "@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); |
| |
| expect( |
| () { |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: 'app_es.arb', |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ).loadResources(); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('The locale specified in @@locale and the arb filename do not match.'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext("throws when arb file's locale could not be determined", () { |
| fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true) |
| ..childFile('app.arb') |
| .writeAsStringSync(singleMessageArbFileString); |
| expect( |
| () { |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: 'app.arb', |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ).loadResources(); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('locale could not be determined'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('throws when an empty string is used as a key', () { |
| const String arbFileStringWithEmptyResourceId = ''' |
| { |
| "market": "MARKET", |
| "": { |
| "description": "This key is invalid" |
| } |
| }'''; |
| |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| l10nDirectory.childFile('app_en.arb') |
| .writeAsStringSync(arbFileStringWithEmptyResourceId); |
| |
| expect( |
| () => LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: 'app_en.arb', |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ).loadResources(), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Invalid ARB resource name ""'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('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); |
| |
| expect( |
| () { |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: 'app_en.arb', |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ).loadResources(); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains("Multiple arb files with the same 'en' locale detected"), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('throws when the base locale does not exist', () { |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| l10nDirectory.childFile('app_en_US.arb') |
| .writeAsStringSync(singleMessageArbFileString); |
| |
| expect( |
| () { |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: 'app_en_US.arb', |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ).loadResources(); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Arb file for a fallback, en, does not exist'), |
| )), |
| ); |
| }); |
| }); |
| |
| group('writeOutputFiles', () { |
| testWithoutContext('multiple messages with syntax error all log their errors', () { |
| try { |
| setupLocalizations(<String, String>{ |
| 'en': r''' |
| { |
| "msg1": "{", |
| "msg2": "{ {" |
| }'''}); |
| } on L10nException catch (error) { |
| expect(error.message, equals('Found syntax errors.')); |
| expect(logger.errorText, contains(''' |
| [app_en.arb:msg1] ICU Syntax Error: Expected "identifier" but found no tokens. |
| { |
| ^ |
| [app_en.arb:msg2] ICU Syntax Error: Expected "identifier" but found "{". |
| { { |
| ^''')); |
| } |
| }); |
| |
| testWithoutContext('no description generates generic comment', () { |
| setupLocalizations(<String, String>{ |
| 'en': r''' |
| { |
| "helloWorld": "Hello world!" |
| }''' |
| }); |
| expect(getGeneratedFileContent(), contains('/// No description provided for @helloWorld.')); |
| }); |
| |
| testWithoutContext('multiline descriptions are correctly formatted as comments', () { |
| setupLocalizations(<String, String>{ |
| 'en': r''' |
| { |
| "helloWorld": "Hello world!", |
| "@helloWorld": { |
| "description": "The generic example string in every language.\nUse this for tests!" |
| } |
| }'''}); |
| expect(getGeneratedFileContent(), contains(''' |
| /// The generic example string in every language. |
| /// Use this for tests!''')); |
| }); |
| |
| testWithoutContext('message without placeholders - should generate code comment with description and template message translation', () { |
| setupLocalizations(<String, String> { |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }); |
| final String content = getGeneratedFileContent(); |
| expect(content, contains('/// Title for the application.')); |
| expect(content, contains(''' |
| /// In en, this message translates to: |
| /// **'Title'**''')); |
| }); |
| |
| testWithoutContext('template message translation handles newline characters', () { |
| setupLocalizations(<String, String>{ |
| 'en': r''' |
| { |
| "title": "Title \n of the application", |
| "@title": { |
| "description": "Title for the application." |
| } |
| }''', |
| 'es': singleEsMessageArbFileString |
| }); |
| final String content = getGeneratedFileContent(); |
| expect(content, contains('/// Title for the application.')); |
| expect(content, contains(r''' |
| /// In en, this message translates to: |
| /// **'Title \n of the application'**''')); |
| }); |
| |
| testWithoutContext('message with placeholders - should generate code comment with description and template message translation', () { |
| setupLocalizations(<String, String>{ |
| 'en': r''' |
| { |
| "price": "The price of this item is: ${price}", |
| "@price": { |
| "description": "The price of an online shopping cart item.", |
| "placeholders": { |
| "price": { |
| "type": "double", |
| "format": "decimalPattern" |
| } |
| } |
| } |
| }''', |
| 'es': r''' |
| { |
| "price": "El precio de este artículo es: ${price}" |
| }''' |
| }); |
| final String content = getGeneratedFileContent(); |
| expect(content, contains('/// The price of an online shopping cart item.')); |
| expect(content, contains(r''' |
| /// In en, this message translates to: |
| /// **'The price of this item is: \${price}'**''')); |
| }); |
| |
| testWithoutContext('should generate a file per language', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'en_CA': ''' |
| { |
| "title": "Canadian Title" |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('class AppLocalizationsEn extends AppLocalizations')); |
| expect(getGeneratedFileContent(locale: 'en'), contains('class AppLocalizationsEnCa extends AppLocalizationsEn')); |
| expect(() => getGeneratedFileContent(locale: 'en_US'), throwsException); |
| }); |
| |
| testWithoutContext('language imports are sorted when preferredSupportedLocaleString is given', () { |
| const List<String> preferredSupportedLocales = <String>['zh']; |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'zh': singleZhMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, preferredSupportedLocales: preferredSupportedLocales); |
| final String content = getGeneratedFileContent(); |
| expect(content, contains( |
| ''' |
| import 'output-localization-file_en.dart'; |
| import 'output-localization-file_es.dart'; |
| import 'output-localization-file_zh.dart'; |
| ''')); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/88356 |
| testWithoutContext('full output file suffix is retained', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| }, outputFileString: 'output-localization-file.g.dart'); |
| final String baseLocalizationsFile = fs.file( |
| fs.path.join(syntheticL10nPackagePath, 'output-localization-file.g.dart'), |
| ).readAsStringSync(); |
| expect(baseLocalizationsFile, contains( |
| ''' |
| import 'output-localization-file_en.g.dart'; |
| ''')); |
| |
| final String englishLocalizationsFile = fs.file( |
| fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.g.dart'), |
| ).readAsStringSync(); |
| expect(englishLocalizationsFile, contains( |
| ''' |
| import 'output-localization-file.g.dart'; |
| ''')); |
| }); |
| |
| testWithoutContext('throws an exception when invalid output file name is passed in', () { |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| }, outputFileString: 'asdf'); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| allOf( |
| contains('output-localization-file'), |
| contains('asdf'), |
| contains('is invalid'), |
| contains('The file name must have a .dart extension.'), |
| ), |
| )), |
| ); |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| }, outputFileString: '.g.dart'); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| allOf( |
| contains('output-localization-file'), |
| contains('.g.dart'), |
| contains('is invalid'), |
| contains('The base name cannot be empty.'), |
| ), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('imports are deferred and loaded when useDeferredImports are set', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| }, useDeferredLoading: true); |
| final String content = getGeneratedFileContent(); |
| expect(content, contains( |
| ''' |
| import 'output-localization-file_en.dart' deferred as output-localization-file_en; |
| ''')); |
| expect(content, contains('output-localization-file_en.loadLibrary()')); |
| }); |
| |
| group('placeholder tests', () { |
| testWithoutContext('should automatically infer placeholders that are not explicitly defined', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "helloWorld": "Hello {name}" |
| }''' |
| }); |
| final String content = getGeneratedFileContent(locale: 'en'); |
| expect(content, contains('String helloWorld(Object name) {')); |
| }); |
| |
| testWithoutContext('placeholder parameter list should be consistent between languages', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "helloWorld": "Hello {name}", |
| "@helloWorld": { |
| "placeholders": { |
| "name": {} |
| } |
| } |
| }''', |
| 'es': ''' |
| { |
| "helloWorld": "Hola" |
| } |
| ''', |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('String helloWorld(Object name) {')); |
| expect(getGeneratedFileContent(locale: 'es'), contains('String helloWorld(Object name) {')); |
| }); |
| |
| testWithoutContext('braces are ignored as special characters if placeholder does not exist', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "helloWorld": "Hello {name}", |
| "@@helloWorld": { |
| "placeholders": { |
| "names": {} |
| } |
| } |
| }''' |
| }, relaxSyntax: true); |
| final String content = getGeneratedFileContent(locale: 'en'); |
| expect(content, contains("String get helloWorld => 'Hello {name}'")); |
| }); |
| }); |
| |
| group('DateTime tests', () { |
| testWithoutContext('imports package:intl', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "@@locale": "en", |
| "springBegins": "Spring begins on {springStartDate}", |
| "@springBegins": { |
| "description": "The first day of spring", |
| "placeholders": { |
| "springStartDate": { |
| "type": "DateTime", |
| "format": "yMd" |
| } |
| } |
| } |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode)); |
| }); |
| |
| testWithoutContext('throws an exception when improperly formatted date is passed in', () { |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "springBegins": "Spring begins on {springStartDate}", |
| "@springBegins": { |
| "placeholders": { |
| "springStartDate": { |
| "type": "DateTime", |
| "format": "asdf" |
| } |
| } |
| } |
| }''' |
| }); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| allOf( |
| contains('asdf'), |
| contains('springStartDate'), |
| contains('does not have a corresponding DateFormat'), |
| ), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('use standard date format whenever possible', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "springBegins": "Spring begins on {springStartDate}", |
| "@springBegins": { |
| "placeholders": { |
| "springStartDate": { |
| "type": "DateTime", |
| "format": "yMd", |
| "isCustomDateFormat": "true" |
| } |
| } |
| } |
| }''' |
| }); |
| final String content = getGeneratedFileContent(locale: 'en'); |
| expect(content, contains('DateFormat.yMd(localeName)')); |
| }); |
| |
| testWithoutContext('handle arbitrary formatted date', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "@@locale": "en", |
| "springBegins": "Spring begins on {springStartDate}", |
| "@springBegins": { |
| "description": "The first day of spring", |
| "placeholders": { |
| "springStartDate": { |
| "type": "DateTime", |
| "format": "asdf o'clock", |
| "isCustomDateFormat": "true" |
| } |
| } |
| } |
| }''' |
| }); |
| final String content = getGeneratedFileContent(locale: 'en'); |
| expect(content, contains(r"DateFormat('asdf o\'clock', localeName)")); |
| }); |
| |
| testWithoutContext('throws an exception when no format attribute is passed in', () { |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "springBegins": "Spring begins on {springStartDate}", |
| "@springBegins": { |
| "description": "The first day of spring", |
| "placeholders": { |
| "springStartDate": { |
| "type": "DateTime" |
| } |
| } |
| } |
| }''' |
| }); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('the "format" attribute needs to be set'), |
| )), |
| ); |
| }); |
| }); |
| |
| group('NumberFormat tests', () { |
| testWithoutContext('imports package:intl', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "courseCompletion": "You have completed {progress} of the course.", |
| "@courseCompletion": { |
| "description": "The amount of progress the student has made in their class.", |
| "placeholders": { |
| "progress": { |
| "type": "double", |
| "format": "percentPattern" |
| } |
| } |
| } |
| }''' |
| }); |
| final String content = getGeneratedFileContent(locale: 'en'); |
| expect(content, contains(intlImportDartCode)); |
| }); |
| |
| testWithoutContext('throws an exception when improperly formatted number is passed in', () { |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "courseCompletion": "You have completed {progress} of the course.", |
| "@courseCompletion": { |
| "description": "The amount of progress the student has made in their class.", |
| "placeholders": { |
| "progress": { |
| "type": "double", |
| "format": "asdf" |
| } |
| } |
| } |
| }''' |
| }); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| allOf( |
| contains('asdf'), |
| contains('progress'), |
| contains('does not have a corresponding NumberFormat'), |
| ), |
| )), |
| ); |
| }); |
| }); |
| |
| group('plural messages', () { |
| testWithoutContext('intl package import should be omitted in subclass files when no plurals are included', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }); |
| expect(getGeneratedFileContent(locale: 'es'), isNot(contains(intlImportDartCode))); |
| }); |
| |
| testWithoutContext('warnings are generated when plural parts are repeated', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}", |
| "@helloWorlds": { |
| "description": "Properly formatted but has redundant zero cases." |
| } |
| }''' |
| }); |
| expect(logger.hadWarningOutput, isTrue); |
| expect(logger.warningText, contains(''' |
| [app_en.arb:helloWorlds] ICU Syntax Warning: The plural part specified below is overridden by a later plural part. |
| {count,plural, =0{Hello}zero{hello} other{hi}} |
| ^''')); |
| }); |
| |
| testWithoutContext('undefined plural cases throws syntax error', () { |
| try { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "count": "{count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}}" |
| }''' |
| }); |
| } on L10nException catch (error) { |
| expect(error.message, contains('Found syntax errors.')); |
| expect(logger.hadErrorOutput, isTrue); |
| expect(logger.errorText, contains(''' |
| [app_en.arb:count] The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "many", or "other. |
| 3 is not a valid plural case. |
| {count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}} |
| ^''')); |
| } |
| }); |
| |
| testWithoutContext('should automatically infer plural placeholders that are not explicitly defined', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "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": { |
| "description": "Improperly formatted since it has no placeholder attribute." |
| } |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('String helloWorlds(num count) {')); |
| }); |
| |
| testWithoutContext('should throw attempting to generate a plural message with incorrect format for placeholders', () { |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "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": "Incorrectly a string, should be a map." |
| } |
| }''' |
| }); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| allOf( |
| contains('is not properly formatted'), |
| contains('Ensure that it is a map with string valued keys'), |
| ), |
| )), |
| ); |
| }); |
| }); |
| |
| group('select messages', () { |
| testWithoutContext('should automatically infer select placeholders that are not explicitly defined', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "genderSelect": "{gender, select, female {She} male {He} other {they} }", |
| "@genderSelect": { |
| "description": "Improperly formatted since it has no placeholder attribute." |
| } |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('String genderSelect(String gender) {')); |
| }); |
| |
| testWithoutContext('should throw attempting to generate a select message with incorrect format for placeholders', () { |
| expect( |
| () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "genderSelect": "{gender, select, female {She} male {He} other {they} }", |
| "@genderSelect": { |
| "placeholders": "Incorrectly a string, should be a map." |
| } |
| }''' |
| }); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| allOf( |
| contains('is not properly formatted'), |
| contains('Ensure that it is a map with string valued keys'), |
| ), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('should throw attempting to generate a select message with an incorrect message', () { |
| try { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "genderSelect": "{gender, select,}", |
| "@genderSelect": { |
| "placeholders": { |
| "gender": {} |
| } |
| } |
| }''' |
| }); |
| } on L10nException { |
| expect(logger.errorText, contains(''' |
| [app_en.arb:genderSelect] ICU Syntax Error: Select expressions must have an "other" case. |
| {gender, select,} |
| ^''') |
| ); |
| } |
| }); |
| }); |
| |
| group('argument messages', () { |
| testWithoutContext('should generate proper calls to intl.DateFormat', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "datetime": "{today, date, ::yMd}" |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.yMd(localeName).format(today)')); |
| }); |
| |
| testWithoutContext('should generate proper calls to intl.DateFormat when using time', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "datetime": "{current, time, ::jms}" |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.jms(localeName).format(current)')); |
| }); |
| |
| testWithoutContext('should not complain when placeholders are explicitly typed to DateTime', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "datetime": "{today, date, ::yMd}", |
| "@datetime": { |
| "placeholders": { |
| "today": { "type": "DateTime" } |
| } |
| } |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {')); |
| }); |
| |
| testWithoutContext('should automatically infer date time placeholders that are not explicitly defined', () { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "datetime": "{today, date, ::yMd}" |
| }''' |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {')); |
| }); |
| |
| testWithoutContext('should throw on invalid DateFormat', () { |
| try { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "datetime": "{today, date, ::yMMMMMd}" |
| }''' |
| }); |
| assert(false); |
| } on L10nException { |
| expect(logger.errorText, contains('Date format "yMMMMMd" for placeholder today does not have a corresponding DateFormat constructor')); |
| } |
| }); |
| }); |
| |
| // All error handling for messages should collect errors on a per-error |
| // basis and log them out individually. Then, it will throw an L10nException. |
| group('error handling tests', () { |
| testWithoutContext('syntax/code-gen errors properly logs errors per message', () { |
| // TODO(thkim1011): Fix error handling so that long indents don't get truncated. |
| // See https://github.com/flutter/flutter/issues/120490. |
| try { |
| setupLocalizations(<String, String>{ |
| 'en': ''' |
| { |
| "hello": "Hello { name", |
| "plural": "This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}", |
| "explanationWithLexingError": "The 'string above is incorrect as it forgets to close the brace", |
| "pluralWithInvalidCase": "{ count, plural, woohoo{huh?} other{lol} }" |
| }''' |
| }, useEscaping: true); |
| } on L10nException { |
| expect(logger.errorText, contains(''' |
| [app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens. |
| Hello { name |
| ^ |
| [app_en.arb:plural] ICU Syntax Error: Expected "}" but found no tokens. |
| This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs} |
| ^ |
| [app_en.arb:explanationWithLexingError] ICU Lexing Error: Unmatched single quotes. |
| The 'string above is incorrect as it forgets to close the brace |
| ^ |
| [app_en.arb:pluralWithInvalidCase] ICU Syntax Error: Plural expressions case must be one of "zero", "one", "two", "few", "many", or "other". |
| { count, plural, woohoo{huh?} other{lol} } |
| ^''')); |
| } |
| }); |
| |
| testWithoutContext('errors thrown in multiple languages are all shown', () { |
| try { |
| setupLocalizations(<String, String>{ |
| 'en': '{ "hello": "Hello { name" }', |
| 'es': '{ "hello": "Hola { name" }', |
| }); |
| } on L10nException { |
| expect(logger.errorText, contains(''' |
| [app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens. |
| Hello { name |
| ^ |
| [app_es.arb:hello] ICU Syntax Error: Expected "}" but found no tokens. |
| Hola { name |
| ^''')); |
| } |
| }); |
| }); |
| |
| |
| testWithoutContext('intl package import should be kept in subclass files when plurals are included', () { |
| const String pluralMessageArb = ''' |
| { |
| "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": { |
| "description": "A plural message", |
| "placeholders": { |
| "count": {} |
| } |
| } |
| } |
| '''; |
| const String pluralMessageEsArb = ''' |
| { |
| "helloWorlds": "{count,plural, =0{ES - Hello} =1{ES - Hello World} =2{ES - Hello two worlds} few{ES - Hello {count} worlds} many{ES - Hello all {count} worlds} other{ES - Hello other {count} worlds}}" |
| } |
| '''; |
| setupLocalizations(<String, String>{ |
| 'en': pluralMessageArb, |
| 'es': pluralMessageEsArb, |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode)); |
| expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode)); |
| }); |
| |
| testWithoutContext('intl package import should be kept in subclass files when select is included', () { |
| const String selectMessageArb = ''' |
| { |
| "genderSelect": "{gender, select, female {She} male {He} other {they} }", |
| "@genderSelect": { |
| "description": "A select message", |
| "placeholders": { |
| "gender": {} |
| } |
| } |
| } |
| '''; |
| const String selectMessageEsArb = ''' |
| { |
| "genderSelect": "{gender, select, female {ES - She} male {ES - He} other {ES - they} }" |
| } |
| '''; |
| setupLocalizations(<String, String>{ |
| 'en': selectMessageArb, |
| 'es': selectMessageEsArb, |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode)); |
| expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode)); |
| }); |
| |
| testWithoutContext('check indentation on generated files', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }); |
| // Tests a few of the lines in the generated code. |
| // Localizations lookup code |
| final String localizationsFile = getGeneratedFileContent(); |
| expect(localizationsFile.contains(' switch (locale.languageCode) {'), true); |
| expect(localizationsFile.contains(" case 'en': return AppLocalizationsEn();"), true); |
| expect(localizationsFile.contains(" case 'es': return AppLocalizationsEs();"), true); |
| expect(localizationsFile.contains(' }'), true); |
| |
| // Supported locales list |
| expect(localizationsFile.contains(' static const List<Locale> supportedLocales = <Locale>['), true); |
| expect(localizationsFile.contains(" Locale('en'),"), true); |
| expect(localizationsFile.contains(" Locale('es')"), true); |
| expect(localizationsFile.contains(' ];'), true); |
| }); |
| |
| testWithoutContext('foundation package import should be omitted from file template when deferred loading = true', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }, useDeferredLoading: true); |
| expect(getGeneratedFileContent(), isNot(contains(foundationImportDartCode))); |
| }); |
| |
| testWithoutContext('foundation package import should be kept in file template when deferred loading = false', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| 'es': singleEsMessageArbFileString, |
| }); |
| expect(getGeneratedFileContent(), contains(foundationImportDartCode)); |
| }); |
| |
| testWithoutContext('check for string interpolation rules', () { |
| const String enArbCheckList = ''' |
| { |
| "one": "The number of {one} elapsed is: 44", |
| "@one": { |
| "description": "test one", |
| "placeholders": { |
| "one": { |
| "type": "String" |
| } |
| } |
| }, |
| "two": "哈{two}哈", |
| "@two": { |
| "description": "test two", |
| "placeholders": { |
| "two": { |
| "type": "String" |
| } |
| } |
| }, |
| "three": "m{three}m", |
| "@three": { |
| "description": "test three", |
| "placeholders": { |
| "three": { |
| "type": "String" |
| } |
| } |
| }, |
| "four": "I have to work _{four}_ sometimes.", |
| "@four": { |
| "description": "test four", |
| "placeholders": { |
| "four": { |
| "type": "String" |
| } |
| } |
| }, |
| "five": "{five} elapsed.", |
| "@five": { |
| "description": "test five", |
| "placeholders": { |
| "five": { |
| "type": "String" |
| } |
| } |
| }, |
| "six": "{six}m", |
| "@six": { |
| "description": "test six", |
| "placeholders": { |
| "six": { |
| "type": "String" |
| } |
| } |
| }, |
| "seven": "hours elapsed: {seven}", |
| "@seven": { |
| "description": "test seven", |
| "placeholders": { |
| "seven": { |
| "type": "String" |
| } |
| } |
| }, |
| "eight": " {eight}", |
| "@eight": { |
| "description": "test eight", |
| "placeholders": { |
| "eight": { |
| "type": "String" |
| } |
| } |
| }, |
| "nine": "m{nine}", |
| "@nine": { |
| "description": "test nine", |
| "placeholders": { |
| "nine": { |
| "type": "String" |
| } |
| } |
| } |
| } |
| '''; |
| |
| // It's fine that the arb is identical -- Just checking |
| // generated code for use of '${variable}' vs '$variable' |
| const String esArbCheckList = ''' |
| { |
| "one": "The number of {one} elapsed is: 44", |
| "two": "哈{two}哈", |
| "three": "m{three}m", |
| "four": "I have to work _{four}_ sometimes.", |
| "five": "{five} elapsed.", |
| "six": "{six}m", |
| "seven": "hours elapsed: {seven}", |
| "eight": " {eight}", |
| "nine": "m{nine}" |
| } |
| '''; |
| setupLocalizations(<String, String>{ |
| 'en': enArbCheckList, |
| 'es': esArbCheckList, |
| }); |
| final String localizationsFile = getGeneratedFileContent(locale: 'es'); |
| expect(localizationsFile, contains(r'$one')); |
| expect(localizationsFile, contains(r'$two')); |
| expect(localizationsFile, contains(r'${three}')); |
| expect(localizationsFile, contains(r'${four}')); |
| expect(localizationsFile, contains(r'$five')); |
| expect(localizationsFile, contains(r'${six}m')); |
| expect(localizationsFile, contains(r'$seven')); |
| expect(localizationsFile, contains(r'$eight')); |
| expect(localizationsFile, contains(r'$nine')); |
| }); |
| |
| testWithoutContext('check for string interpolation rules - plurals', () { |
| const String enArbCheckList = ''' |
| { |
| "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}", |
| "@first": { |
| "description": "First set of plural messages to test.", |
| "placeholders": { |
| "count": {} |
| } |
| }, |
| "second": "{count,plural, =0{test {count}} other{ {count}}}", |
| "@second": { |
| "description": "Second set of plural messages to test.", |
| "placeholders": { |
| "count": {} |
| } |
| }, |
| "third": "{total,plural, =0{test {total}} other{ {total}}}", |
| "@third": { |
| "description": "Third set of plural messages to test, for number.", |
| "placeholders": { |
| "total": { |
| "type": "int", |
| "format": "compactLong" |
| } |
| } |
| } |
| } |
| '''; |
| |
| // It's fine that the arb is identical -- Just checking |
| // generated code for use of '${variable}' vs '$variable' |
| const String esArbCheckList = ''' |
| { |
| "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}", |
| "second": "{count,plural, =0{test {count}} other{ {count}}}" |
| } |
| '''; |
| setupLocalizations(<String, String>{ |
| 'en': enArbCheckList, |
| 'es': esArbCheckList, |
| }); |
| final String localizationsFile = getGeneratedFileContent(locale: 'es'); |
| expect(localizationsFile, contains(r'test $count test')); |
| expect(localizationsFile, contains(r'哈$count哈')); |
| expect(localizationsFile, contains(r'm${count}m')); |
| expect(localizationsFile, contains(r'_${count}_')); |
| expect(localizationsFile, contains(r'$count test')); |
| expect(localizationsFile, contains(r'${count}m')); |
| expect(localizationsFile, contains(r'test $count')); |
| expect(localizationsFile, contains(r' $count')); |
| expect(localizationsFile, contains(r'String totalString = totalNumberFormat')); |
| expect(localizationsFile, contains(r'totalString')); |
| expect(localizationsFile, contains(r'totalString')); |
| }); |
| |
| testWithoutContext( |
| 'should throw with descriptive error message when failing to parse the ' |
| 'arb file', |
| () { |
| const String arbFileWithTrailingComma = ''' |
| { |
| "title": "Stocks", |
| "@title": { |
| "description": "Title for the Stocks application" |
| }, |
| }'''; |
| expect( |
| () { |
| setupLocalizations(<String, String>{ 'en': arbFileWithTrailingComma }); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| allOf( |
| contains('app_en.arb'), |
| contains('FormatException'), |
| contains('Unexpected character'), |
| ), |
| )), |
| ); |
| }, |
| ); |
| |
| testWithoutContext('should throw when resource is missing resource attribute (isResourceAttributeRequired = true)', () { |
| const String arbFileWithMissingResourceAttribute = ''' |
| { |
| "title": "Stocks" |
| }'''; |
| expect( |
| () { |
| setupLocalizations( |
| <String, String>{ 'en': arbFileWithMissingResourceAttribute }, |
| areResourceAttributeRequired: true, |
| ); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Resource attribute "@title" was not found'), |
| )), |
| ); |
| }); |
| |
| group('checks for method/getter formatting', () { |
| testWithoutContext('cannot contain non-alphanumeric symbols', () { |
| const String nonAlphaNumericArbFile = ''' |
| { |
| "title!!": "Stocks", |
| "@title!!": { |
| "description": "Title for the Stocks application" |
| } |
| }'''; |
| expect( |
| () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Invalid ARB resource name'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('must start with lowercase character', () { |
| const String nonAlphaNumericArbFile = ''' |
| { |
| "Title": "Stocks", |
| "@Title": { |
| "description": "Title for the Stocks application" |
| } |
| }'''; |
| expect( |
| () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Invalid ARB resource name'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('cannot start with a number', () { |
| const String nonAlphaNumericArbFile = ''' |
| { |
| "123title": "Stocks", |
| "@123title": { |
| "description": "Title for the Stocks application" |
| } |
| }'''; |
| expect( |
| () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }), |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('Invalid ARB resource name'), |
| )), |
| ); |
| }); |
| |
| testWithoutContext('can start with and contain a dollar sign', () { |
| const String dollarArbFile = r''' |
| { |
| "$title$": "Stocks", |
| "@$title$": { |
| "description": "Title for the application" |
| } |
| }'''; |
| setupLocalizations(<String, String>{ 'en': dollarArbFile }); |
| }); |
| }); |
| |
| testWithoutContext('throws when the language code is not supported', () { |
| const String arbFileWithInvalidCode = ''' |
| { |
| "@@locale": "invalid", |
| "title": "invalid" |
| }'''; |
| |
| final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') |
| ..createSync(recursive: true); |
| l10nDirectory.childFile('app_invalid.arb') |
| .writeAsStringSync(arbFileWithInvalidCode); |
| |
| expect( |
| () { |
| LocalizationsGenerator( |
| fileSystem: fs, |
| inputPathString: defaultL10nPathString, |
| outputPathString: defaultL10nPathString, |
| templateArbFileName: 'app_invalid.arb', |
| outputFileString: defaultOutputFileString, |
| classNameString: defaultClassNameString, |
| logger: logger, |
| ) |
| ..loadResources() |
| ..writeOutputFiles(); |
| }, |
| throwsA(isA<L10nException>().having( |
| (L10nException e) => e.message, |
| 'message', |
| contains('"invalid" is not a supported language code.'), |
| )), |
| ); |
| }); |
| }); |
| |
| testWithoutContext('should generate a valid pubspec.yaml file when using synthetic package if it does not already exist', () { |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| }); |
| final Directory outputDirectory = fs.directory(syntheticPackagePath); |
| final File pubspecFile = outputDirectory.childFile('pubspec.yaml'); |
| expect(pubspecFile.existsSync(), isTrue); |
| |
| final YamlNode yamlNode = loadYamlNode(pubspecFile.readAsStringSync()); |
| expect(yamlNode, isA<YamlMap>()); |
| |
| final YamlMap yamlMap = yamlNode as YamlMap; |
| final String pubspecName = yamlMap['name'] as String; |
| final String pubspecDescription = yamlMap['description'] as String; |
| expect(pubspecName, 'synthetic_package'); |
| expect(pubspecDescription, "The Flutter application's synthetic package."); |
| }); |
| |
| testWithoutContext('should not overwrite existing pubspec.yaml file when using synthetic package', () { |
| final File pubspecFile = fs.file(fs.path.join(syntheticPackagePath, 'pubspec.yaml')) |
| ..createSync(recursive: true) |
| ..writeAsStringSync('abcd'); |
| setupLocalizations(<String, String>{ |
| 'en': singleMessageArbFileString, |
| }); |
| // The original pubspec file should not be overwritten. |
| expect(pubspecFile.readAsStringSync(), 'abcd'); |
| }); |
| |
| testWithoutContext('can use type: int without specifying a format', () { |
| const String arbFile = ''' |
| { |
| "orderNumber": "This is order #{number}.", |
| "@orderNumber": { |
| "description": "The title for an order with a given number.", |
| "placeholders": { |
| "number": { |
| "type": "int" |
| } |
| } |
| } |
| }'''; |
| setupLocalizations(<String, String>{ |
| 'en': arbFile, |
| }); |
| expect(getGeneratedFileContent(locale: 'en'), containsIgnoringWhitespace(r''' |
| String orderNumber(int number) { |
| return 'This is order #$number.'; |
| } |
| ''')); |
| expect(getGeneratedFileContent(locale: 'en'), isNot(contains(intlImportDartCode))); |
| }); |
| |
| testWithoutContext('app localizations lookup is a public method', () { |
| setupLocalizations(<String, String>{ 'en': singleMessageArbFileString }); |
| expect(getGeneratedFileContent(), containsIgnoringWhitespace(r''' |
| AppLocalizations lookupAppLocalizations(Locale locale) { |
| ''')); |
| }); |
| |
| testWithoutContext('escaping with single quotes', () { |
| const String arbFile = ''' |
| { |
| "singleQuote": "Flutter''s amazing!", |
| "@singleQuote": { |
| "description": "A message with a single quote." |
| } |
| }'''; |
| setupLocalizations(<String, String>{ 'en': arbFile }, useEscaping: true); |
| expect(getGeneratedFileContent(locale: 'en'), contains(r"Flutter\'s amazing")); |
| }); |
| |
| testWithoutContext('suppress warnings flag actually suppresses warnings', () { |
| const String pluralMessageWithOverriddenParts = ''' |
| { |
| "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}", |
| "@helloWorlds": { |
| "description": "Properly formatted but has redundant zero cases.", |
| "placeholders": { |
| "count": {} |
| } |
| } |
| }'''; |
| setupLocalizations( |
| <String, String>{ 'en': pluralMessageWithOverriddenParts }, |
| suppressWarnings: true, |
| ); |
| expect(logger.hadWarningOutput, isFalse); |
| }); |
| |
| testWithoutContext('can use decimalPatternDigits with decimalDigits optional parameter', () { |
| const String arbFile = ''' |
| { |
| "treeHeight": "Tree height is {height}m.", |
| "@treeHeight": { |
| "placeholders": { |
| "height": { |
| "type": "double", |
| "format": "decimalPatternDigits", |
| "optionalParameters": { |
| "decimalDigits": 3 |
| } |
| } |
| } |
| } |
| }'''; |
| setupLocalizations(<String, String>{ 'en': arbFile }); |
| final String localizationsFile = getGeneratedFileContent(locale: 'en'); |
| expect(localizationsFile, containsIgnoringWhitespace(r''' |
| String treeHeight(double height) { |
| ''')); |
| expect(localizationsFile, containsIgnoringWhitespace(r''' |
| NumberFormat.decimalPatternDigits( |
| locale: localeName, |
| decimalDigits: 3 |
| ); |
| ''')); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/125461. |
| testWithoutContext('dollar signs are escaped properly when there is a select clause', () { |
| const String dollarSignWithSelect = r''' |
| { |
| "dollarSignWithSelect": "$nice_bug\nHello Bug! Manifistation #1 {selectPlaceholder, select, case{message} other{messageOther}}" |
| }'''; |
| setupLocalizations(<String, String>{ 'en': dollarSignWithSelect }); |
| expect(getGeneratedFileContent(locale: 'en'), contains(r'\$nice_bug\nHello Bug! Manifistation #1 $_temp0')); |
| }); |
| } |