1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:file/memory.dart';
6import 'package:flutter_tools/src/artifacts.dart';
7import 'package:flutter_tools/src/base/file_system.dart';
8import 'package:flutter_tools/src/base/logger.dart';
9import 'package:flutter_tools/src/convert.dart';
10import 'package:flutter_tools/src/localizations/gen_l10n.dart';
11import 'package:flutter_tools/src/localizations/gen_l10n_types.dart';
12import 'package:flutter_tools/src/localizations/localizations_utils.dart';
13import 'package:yaml/yaml.dart';
14
15import '../src/common.dart';
16import '../src/fake_process_manager.dart';
17
18const String defaultTemplateArbFileName = 'app_en.arb';
19const String defaultOutputFileString = 'output-localization-file.dart';
20const String defaultClassNameString = 'AppLocalizations';
21const String singleMessageArbFileString = '''
22{
23 "title": "Title",
24 "@title": {
25 "description": "Title for the application."
26 }
27}''';
28const String twoMessageArbFileString = '''
29{
30 "title": "Title",
31 "@title": {
32 "description": "Title for the application."
33 },
34 "subtitle": "Subtitle",
35 "@subtitle": {
36 "description": "Subtitle for the application."
37 }
38}''';
39const String esArbFileName = 'app_es.arb';
40const String singleEsMessageArbFileString = '''
41{
42 "title": "Título"
43}''';
44const String singleZhMessageArbFileString = '''
45{
46 "title": "标题"
47}''';
48const String intlImportDartCode = '''
49import 'package:intl/intl.dart' as intl;
50''';
51const String foundationImportDartCode = '''
52import 'package:flutter/foundation.dart';
53''';
54
55void _standardFlutterDirectoryL10nSetup(FileSystem fs) {
56 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
57 ..createSync(recursive: true);
58 l10nDirectory.childFile(defaultTemplateArbFileName)
59 .writeAsStringSync(singleMessageArbFileString);
60 l10nDirectory.childFile(esArbFileName)
61 .writeAsStringSync(singleEsMessageArbFileString);
62 fs.file('pubspec.yaml')
63 ..createSync(recursive: true)
64 ..writeAsStringSync('''
65flutter:
66 generate: true
67''');
68
69}
70
71void main() {
72 late MemoryFileSystem fs;
73 late BufferLogger logger;
74 late Artifacts artifacts;
75 late ProcessManager processManager;
76 late String defaultL10nPathString;
77 late String syntheticPackagePath;
78 late String syntheticL10nPackagePath;
79
80 LocalizationsGenerator setupLocalizations(
81 Map<String, String> localeToArbFile,
82 {
83 String? yamlFile,
84 String? outputPathString,
85 String? outputFileString,
86 String? headerString,
87 String? headerFile,
88 String? untranslatedMessagesFile,
89 bool useSyntheticPackage = true,
90 bool isFromYaml = false,
91 bool usesNullableGetter = true,
92 String? inputsAndOutputsListPath,
93 List<String>? preferredSupportedLocales,
94 bool useDeferredLoading = false,
95 bool useEscaping = false,
96 bool areResourceAttributeRequired = false,
97 bool suppressWarnings = false,
98 bool relaxSyntax = false,
99 bool useNamedParameters = false,
100 void Function(Directory)? setup,
101 }
102 ) {
103 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
104 ..createSync(recursive: true);
105 for (final String locale in localeToArbFile.keys) {
106 l10nDirectory.childFile('app_$locale.arb')
107 .writeAsStringSync(localeToArbFile[locale]!);
108 }
109 if (setup != null) {
110 setup(l10nDirectory);
111 }
112 return LocalizationsGenerator(
113 fileSystem: fs,
114 inputPathString: l10nDirectory.path,
115 outputPathString: outputPathString ?? l10nDirectory.path,
116 templateArbFileName: defaultTemplateArbFileName,
117 outputFileString: outputFileString ?? defaultOutputFileString,
118 classNameString: defaultClassNameString,
119 headerString: headerString,
120 headerFile: headerFile,
121 logger: logger,
122 untranslatedMessagesFile: untranslatedMessagesFile,
123 useSyntheticPackage: useSyntheticPackage,
124 inputsAndOutputsListPath: inputsAndOutputsListPath,
125 usesNullableGetter: usesNullableGetter,
126 preferredSupportedLocales: preferredSupportedLocales,
127 useDeferredLoading: useDeferredLoading,
128 useEscaping: useEscaping,
129 areResourceAttributesRequired: areResourceAttributeRequired,
130 suppressWarnings: suppressWarnings,
131 useRelaxedSyntax: relaxSyntax,
132 useNamedParameters: useNamedParameters,
133 )
134 ..loadResources()
135 ..writeOutputFiles(isFromYaml: isFromYaml);
136 }
137
138 String getGeneratedFileContent({String? locale}) {
139 final String fileName = locale == null ? 'output-localization-file.dart' : 'output-localization-file_$locale.dart';
140 return fs.file(
141 fs.path.join(syntheticL10nPackagePath, fileName)
142 ).readAsStringSync();
143 }
144
145 setUp(() {
146 fs = MemoryFileSystem.test();
147 logger = BufferLogger.test();
148 artifacts = Artifacts.test();
149 processManager = FakeProcessManager.empty();
150
151 defaultL10nPathString = fs.path.join('lib', 'l10n');
152 syntheticPackagePath = fs.path.join('.dart_tool', 'flutter_gen');
153 syntheticL10nPackagePath = fs.path.join(syntheticPackagePath, 'gen_l10n');
154 precacheLanguageAndRegionTags();
155 });
156
157 group('Setters', () {
158 testWithoutContext('setInputDirectory fails if the directory does not exist', () {
159 expect(
160 () => LocalizationsGenerator.inputDirectoryFromPath(fs, 'lib', fs.directory('bogus')),
161 throwsA(isA<L10nException>().having(
162 (L10nException e) => e.message,
163 'message',
164 contains('Make sure that the correct path was provided'),
165 )),
166 );
167 });
168
169 testWithoutContext('setting className fails if input string is empty', () {
170 _standardFlutterDirectoryL10nSetup(fs);
171 expect(
172 () => LocalizationsGenerator.classNameFromString(''),
173 throwsA(isA<L10nException>().having(
174 (L10nException e) => e.message,
175 'message',
176 contains('cannot be empty'),
177 )),
178 );
179 });
180
181 testWithoutContext('sets absolute path of the target Flutter project', () {
182 // Set up project directory.
183 final Directory l10nDirectory = fs.currentDirectory
184 .childDirectory('absolute')
185 .childDirectory('path')
186 .childDirectory('to')
187 .childDirectory('flutter_project')
188 .childDirectory('lib')
189 .childDirectory('l10n')
190 ..createSync(recursive: true);
191 l10nDirectory.childFile(defaultTemplateArbFileName)
192 .writeAsStringSync(singleMessageArbFileString);
193 l10nDirectory.childFile(esArbFileName)
194 .writeAsStringSync(singleEsMessageArbFileString);
195
196 // Run localizations generator in specified absolute path.
197 final String flutterProjectPath = fs.path.join('absolute', 'path', 'to', 'flutter_project');
198 LocalizationsGenerator(
199 fileSystem: fs,
200 projectPathString: flutterProjectPath,
201 inputPathString: defaultL10nPathString,
202 outputPathString: defaultL10nPathString,
203 templateArbFileName: defaultTemplateArbFileName,
204 outputFileString: defaultOutputFileString,
205 classNameString: defaultClassNameString,
206 logger: logger,
207 )
208 ..loadResources()
209 ..writeOutputFiles();
210
211 // Output files should be generated in the provided absolute path.
212 expect(
213 fs.isFileSync(fs.path.join(
214 flutterProjectPath,
215 '.dart_tool',
216 'flutter_gen',
217 'gen_l10n',
218 'output-localization-file_en.dart',
219 )),
220 true,
221 );
222 expect(
223 fs.isFileSync(fs.path.join(
224 flutterProjectPath,
225 '.dart_tool',
226 'flutter_gen',
227 'gen_l10n',
228 'output-localization-file_es.dart',
229 )),
230 true,
231 );
232 });
233
234 testWithoutContext('throws error when directory at absolute path does not exist', () {
235 // Set up project directory.
236 final Directory l10nDirectory = fs.currentDirectory
237 .childDirectory('lib')
238 .childDirectory('l10n')
239 ..createSync(recursive: true);
240 l10nDirectory.childFile(defaultTemplateArbFileName)
241 .writeAsStringSync(singleMessageArbFileString);
242 l10nDirectory.childFile(esArbFileName)
243 .writeAsStringSync(singleEsMessageArbFileString);
244
245 // Project path should be intentionally a directory that does not exist.
246 expect(
247 () => LocalizationsGenerator(
248 fileSystem: fs,
249 projectPathString: 'absolute/path/to/flutter_project',
250 inputPathString: defaultL10nPathString,
251 outputPathString: defaultL10nPathString,
252 templateArbFileName: defaultTemplateArbFileName,
253 outputFileString: defaultOutputFileString,
254 classNameString: defaultClassNameString,
255 logger: logger,
256 ),
257 throwsA(isA<L10nException>().having(
258 (L10nException e) => e.message,
259 'message',
260 contains('Directory does not exist'),
261 )),
262 );
263 });
264
265 testWithoutContext('throws error when arb file does not exist', () {
266 // Set up project directory.
267 fs.currentDirectory
268 .childDirectory('lib')
269 .childDirectory('l10n')
270 .createSync(recursive: true);
271
272 // Arb file should be nonexistent in the l10n directory.
273 expect(
274 () => LocalizationsGenerator(
275 fileSystem: fs,
276 projectPathString: './',
277 inputPathString: defaultL10nPathString,
278 outputPathString: defaultL10nPathString,
279 templateArbFileName: defaultTemplateArbFileName,
280 outputFileString: defaultOutputFileString,
281 classNameString: defaultClassNameString,
282 logger: logger,
283 ),
284 throwsA(isA<L10nException>().having(
285 (L10nException e) => e.message,
286 'message',
287 contains(', does not exist.'),
288 )),
289 );
290 });
291
292 group('className should only take valid Dart class names', () {
293 setUp(() {
294 _standardFlutterDirectoryL10nSetup(fs);
295 });
296
297 testWithoutContext('fails on string with spaces', () {
298 expect(
299 () => LocalizationsGenerator.classNameFromString('String with spaces'),
300 throwsA(isA<L10nException>().having(
301 (L10nException e) => e.message,
302 'message',
303 contains('is not a valid public Dart class name'),
304 )),
305 );
306 });
307
308 testWithoutContext('fails on non-alphanumeric symbols', () {
309 expect(
310 () => LocalizationsGenerator.classNameFromString('TestClass@123'),
311 throwsA(isA<L10nException>().having(
312 (L10nException e) => e.message,
313 'message',
314 contains('is not a valid public Dart class name'),
315 )),
316 );
317 });
318
319 testWithoutContext('fails on camel-case', () {
320 expect(
321 () => LocalizationsGenerator.classNameFromString('camelCaseClassName'),
322 throwsA(isA<L10nException>().having(
323 (L10nException e) => e.message,
324 'message',
325 contains('is not a valid public Dart class name'),
326 )),
327 );
328 });
329
330 testWithoutContext('fails when starting with a number', () {
331 expect(
332 () => LocalizationsGenerator.classNameFromString('123ClassName'),
333 throwsA(isA<L10nException>().having(
334 (L10nException e) => e.message,
335 'message',
336 contains('is not a valid public Dart class name'),
337 )),
338 );
339 });
340 });
341 });
342
343 testWithoutContext('correctly adds a headerString when it is set', () {
344 final LocalizationsGenerator generator = setupLocalizations(<String, String>{
345 'en': singleMessageArbFileString,
346 'es': singleEsMessageArbFileString,
347 }, headerString: '/// Sample header');
348 expect(generator.header, '/// Sample header');
349 });
350
351 testWithoutContext('correctly adds a headerFile when it is set', () {
352 final LocalizationsGenerator generator = setupLocalizations(<String, String>{
353 'en': singleMessageArbFileString,
354 'es': singleEsMessageArbFileString,
355 }, headerFile: 'header.txt', setup: (Directory l10nDirectory) {
356 l10nDirectory.childFile('header.txt').writeAsStringSync('/// Sample header in a text file');
357 });
358 expect(generator.header, '/// Sample header in a text file');
359 });
360
361 testWithoutContext('sets templateArbFileName with more than one underscore correctly', () {
362 setupLocalizations(<String, String>{
363 'en': singleMessageArbFileString,
364 'es': singleEsMessageArbFileString,
365 });
366 final Directory outputDirectory = fs.directory(syntheticL10nPackagePath);
367 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
368 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
369 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
370 });
371
372 testWithoutContext('filenames with invalid locales should not be recognized', () {
373 expect(
374 () {
375 // This attempts to create 'app_localizations_en_CA_foo.arb'.
376 setupLocalizations(<String, String>{
377 'en': singleMessageArbFileString,
378 'en_CA_foo': singleMessageArbFileString,
379 });
380 },
381 throwsA(isA<L10nException>().having(
382 (L10nException e) => e.message,
383 'message',
384 contains("The following .arb file's locale could not be determined"),
385 )),
386 );
387 });
388
389 testWithoutContext('correctly creates an untranslated messages file (useSyntheticPackage = true)', () {
390 final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json');
391 setupLocalizations(<String, String>{
392 'en': twoMessageArbFileString,
393 'es': singleEsMessageArbFileString,
394 }, untranslatedMessagesFile: untranslatedMessagesFilePath);
395 final String unimplementedOutputString = fs.file(untranslatedMessagesFilePath).readAsStringSync();
396 try {
397 // Since ARB file is essentially JSON, decoding it should not fail.
398 json.decode(unimplementedOutputString);
399 } on Exception {
400 fail('Parsing arb file should not fail');
401 }
402 expect(unimplementedOutputString, contains('es'));
403 expect(unimplementedOutputString, contains('subtitle'));
404 });
405
406 testWithoutContext('correctly creates an untranslated messages file (useSyntheticPackage = false)', () {
407 final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json');
408 setupLocalizations(<String, String>{
409 'en': twoMessageArbFileString,
410 'es': singleMessageArbFileString,
411 }, useSyntheticPackage: false, untranslatedMessagesFile: untranslatedMessagesFilePath);
412 final String unimplementedOutputString = fs.file(untranslatedMessagesFilePath).readAsStringSync();
413 try {
414 // Since ARB file is essentially JSON, decoding it should not fail.
415 json.decode(unimplementedOutputString);
416 } on Exception {
417 fail('Parsing arb file should not fail');
418 }
419 expect(unimplementedOutputString, contains('es'));
420 expect(unimplementedOutputString, contains('subtitle'));
421 });
422
423 testWithoutContext(
424 'untranslated messages suggestion is printed when translation is missing: '
425 'command line message',
426 () {
427 setupLocalizations(<String, String>{
428 'en': twoMessageArbFileString,
429 'es': singleEsMessageArbFileString,
430 });
431 expect(
432 logger.statusText,
433 contains('To see a detailed report, use the --untranslated-messages-file'),
434 );
435 expect(
436 logger.statusText,
437 contains('flutter gen-l10n --untranslated-messages-file=desiredFileName.txt'),
438 );
439 },
440 );
441
442 testWithoutContext(
443 'untranslated messages suggestion is printed when translation is missing: '
444 'l10n.yaml message',
445 () {
446 setupLocalizations(<String, String>{
447 'en': twoMessageArbFileString,
448 'es': singleEsMessageArbFileString,
449 }, isFromYaml: true);
450 expect(
451 logger.statusText,
452 contains('To see a detailed report, use the untranslated-messages-file'),
453 );
454 expect(
455 logger.statusText,
456 contains('untranslated-messages-file: desiredFileName.txt'),
457 );
458 },
459 );
460
461 testWithoutContext(
462 'unimplemented messages suggestion is not printed when all messages '
463 'are fully translated',
464 () {
465 setupLocalizations(<String, String>{
466 'en': twoMessageArbFileString,
467 'es': twoMessageArbFileString,
468 });
469 expect(logger.statusText, equals(''));
470 },
471 );
472
473 testWithoutContext('untranslated messages file included in generated JSON list of outputs', () {
474 final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json');
475 setupLocalizations(
476 <String, String>{
477 'en': twoMessageArbFileString,
478 'es': singleEsMessageArbFileString,
479 },
480 untranslatedMessagesFile: untranslatedMessagesFilePath,
481 inputsAndOutputsListPath: syntheticL10nPackagePath,
482 );
483 final File inputsAndOutputsList = fs.file(
484 fs.path.join(syntheticL10nPackagePath, 'gen_l10n_inputs_and_outputs.json')
485 );
486 expect(inputsAndOutputsList.existsSync(), isTrue);
487 final Map<String, dynamic> jsonResult = json.decode(
488 inputsAndOutputsList.readAsStringSync(),
489 ) as Map<String, dynamic>;
490 expect(jsonResult.containsKey('outputs'), isTrue);
491 final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>;
492 expect(outputList, contains(contains('unimplemented_message_translations.json')));
493 });
494
495 testWithoutContext(
496 'uses inputPathString as outputPathString when the outputPathString is '
497 'null while not using the synthetic package option',
498 () {
499 _standardFlutterDirectoryL10nSetup(fs);
500 LocalizationsGenerator(
501 fileSystem: fs,
502 inputPathString: defaultL10nPathString,
503 // outputPathString is intentionally not defined
504 templateArbFileName: defaultTemplateArbFileName,
505 outputFileString: defaultOutputFileString,
506 classNameString: defaultClassNameString,
507 useSyntheticPackage: false,
508 logger: logger,
509 )
510 ..loadResources()
511 ..writeOutputFiles();
512
513 final Directory outputDirectory = fs.directory('lib').childDirectory('l10n');
514 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
515 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
516 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
517 },
518 );
519
520 testWithoutContext(
521 'correctly generates output files in non-default output directory if it '
522 'already exists while not using the synthetic package option',
523 () {
524 final Directory l10nDirectory = fs.currentDirectory
525 .childDirectory('lib')
526 .childDirectory('l10n')
527 ..createSync(recursive: true);
528 // Create the directory 'lib/l10n/output'.
529 l10nDirectory.childDirectory('output');
530
531 l10nDirectory
532 .childFile(defaultTemplateArbFileName)
533 .writeAsStringSync(singleMessageArbFileString);
534 l10nDirectory
535 .childFile(esArbFileName)
536 .writeAsStringSync(singleEsMessageArbFileString);
537
538 LocalizationsGenerator(
539 fileSystem: fs,
540 inputPathString: defaultL10nPathString,
541 outputPathString: fs.path.join('lib', 'l10n', 'output'),
542 templateArbFileName: defaultTemplateArbFileName,
543 outputFileString: defaultOutputFileString,
544 classNameString: defaultClassNameString,
545 useSyntheticPackage: false,
546 logger: logger,
547 )
548 ..loadResources()
549 ..writeOutputFiles();
550
551 final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output');
552 expect(outputDirectory.existsSync(), isTrue);
553 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
554 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
555 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
556 },
557 );
558
559 testWithoutContext(
560 'correctly creates output directory if it does not exist and writes files '
561 'in it while not using the synthetic package option',
562 () {
563 _standardFlutterDirectoryL10nSetup(fs);
564
565 LocalizationsGenerator(
566 fileSystem: fs,
567 inputPathString: defaultL10nPathString,
568 outputPathString: fs.path.join('lib', 'l10n', 'output'),
569 templateArbFileName: defaultTemplateArbFileName,
570 outputFileString: defaultOutputFileString,
571 classNameString: defaultClassNameString,
572 useSyntheticPackage: false,
573 logger: logger,
574 )
575 ..loadResources()
576 ..writeOutputFiles();
577
578 final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output');
579 expect(outputDirectory.existsSync(), isTrue);
580 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
581 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
582 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
583 },
584 );
585
586 testWithoutContext(
587 'generates nullable localizations class getter via static `of` method '
588 'by default',
589 () {
590 final LocalizationsGenerator generator = setupLocalizations(<String, String>{
591 'en': singleMessageArbFileString,
592 'es': singleEsMessageArbFileString,
593 });
594 expect(generator.outputDirectory.existsSync(), isTrue);
595 expect(generator.outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
596 expect(
597 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
598 contains('static AppLocalizations? of(BuildContext context)'),
599 );
600 expect(
601 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
602 contains('return Localizations.of<AppLocalizations>(context, AppLocalizations);'),
603 );
604 },
605 );
606
607 testWithoutContext(
608 'can generate non-nullable localizations class getter via static `of` method ',
609 () {
610 final LocalizationsGenerator generator = setupLocalizations(<String, String>{
611 'en': singleMessageArbFileString,
612 'es': singleEsMessageArbFileString,
613 }, usesNullableGetter: false);
614 expect(generator.outputDirectory.existsSync(), isTrue);
615 expect(generator.outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
616 expect(
617 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
618 contains('static AppLocalizations of(BuildContext context)'),
619 );
620 expect(
621 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
622 contains('return Localizations.of<AppLocalizations>(context, AppLocalizations)!;'),
623 );
624 },
625 );
626
627 testWithoutContext('creates list of inputs and outputs when file path is specified', () {
628 setupLocalizations(<String, String>{
629 'en': singleMessageArbFileString,
630 'es': singleEsMessageArbFileString,
631 }, inputsAndOutputsListPath: syntheticL10nPackagePath);
632 final File inputsAndOutputsList = fs.file(
633 fs.path.join(syntheticL10nPackagePath, 'gen_l10n_inputs_and_outputs.json'),
634 );
635 expect(inputsAndOutputsList.existsSync(), isTrue);
636
637 final Map<String, dynamic> jsonResult = json.decode(inputsAndOutputsList.readAsStringSync()) as Map<String, dynamic>;
638 expect(jsonResult.containsKey('inputs'), isTrue);
639 final List<dynamic> inputList = jsonResult['inputs'] as List<dynamic>;
640 expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_en.arb')));
641 expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_es.arb')));
642
643 expect(jsonResult.containsKey('outputs'), isTrue);
644 final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>;
645 expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file.dart')));
646 expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_en.dart')));
647 expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_es.dart')));
648 });
649
650 testWithoutContext('setting both a headerString and a headerFile should fail', () {
651 expect(
652 () {
653 setupLocalizations(
654 <String, String>{
655 'en': singleMessageArbFileString,
656 'es': singleEsMessageArbFileString,
657 },
658 headerString: '/// Sample header in a text file',
659 headerFile: 'header.txt',
660 setup: (Directory l10nDirectory) {
661 l10nDirectory.childFile('header.txt').writeAsStringSync('/// Sample header in a text file');
662 },
663 );
664 },
665 throwsA(isA<L10nException>().having(
666 (L10nException e) => e.message,
667 'message',
668 contains('Cannot accept both header and header file arguments'),
669 )),
670 );
671 });
672
673 testWithoutContext('setting a headerFile that does not exist should fail', () {
674 expect(
675 () {
676 setupLocalizations(<String, String>{
677 'en': singleMessageArbFileString,
678 'es': singleEsMessageArbFileString,
679 }, headerFile: 'header.txt');
680 },
681 throwsA(isA<L10nException>().having(
682 (L10nException e) => e.message,
683 'message',
684 contains('Failed to read header file'),
685 )),
686 );
687 });
688
689 group('generateLocalizations', () {
690 testWithoutContext('works even if CWD does not have a pubspec.yaml', () async {
691 final Directory projectDir = fs.currentDirectory.childDirectory('project')..createSync(recursive: true);
692 final Directory l10nDirectory = projectDir.childDirectory('lib').childDirectory('l10n')
693 ..createSync(recursive: true);
694 l10nDirectory.childFile(defaultTemplateArbFileName)
695 .writeAsStringSync(singleMessageArbFileString);
696 l10nDirectory.childFile(esArbFileName)
697 .writeAsStringSync(singleEsMessageArbFileString);
698 projectDir.childFile('pubspec.yaml')
699 ..createSync(recursive: true)
700 ..writeAsStringSync('''
701flutter:
702 generate: true
703''');
704
705 final Logger logger = BufferLogger.test();
706 logger.printError('An error output from a different tool in flutter_tools');
707
708 // Should run without error.
709 await generateLocalizations(
710 fileSystem: fs,
711 options: LocalizationOptions(
712 arbDir: Uri.directory(defaultL10nPathString).path,
713 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
714 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
715 syntheticPackage: false,
716 ),
717 logger: logger,
718 projectDir: projectDir,
719 dependenciesDir: fs.currentDirectory,
720 artifacts: artifacts,
721 processManager: processManager,
722 );
723 });
724
725 testWithoutContext('other logs from flutter_tools does not affect gen-l10n', () async {
726 _standardFlutterDirectoryL10nSetup(fs);
727
728 final Logger logger = BufferLogger.test();
729 logger.printError('An error output from a different tool in flutter_tools');
730
731 // Should run without error.
732 await generateLocalizations(
733 fileSystem: fs,
734 options: LocalizationOptions(
735 arbDir: Uri.directory(defaultL10nPathString).path,
736 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
737 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
738 syntheticPackage: false,
739 ),
740 logger: logger,
741 projectDir: fs.currentDirectory,
742 dependenciesDir: fs.currentDirectory,
743 artifacts: artifacts,
744 processManager: processManager,
745 );
746 });
747
748 testWithoutContext('forwards arguments correctly', () async {
749 _standardFlutterDirectoryL10nSetup(fs);
750 final LocalizationOptions options = LocalizationOptions(
751 header: 'HEADER',
752 arbDir: Uri.directory(defaultL10nPathString).path,
753 useDeferredLoading: true,
754 outputClass: 'Foo',
755 outputLocalizationFile: Uri.file('bar.dart', windows: false).path,
756 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
757 preferredSupportedLocales: <String>['es'],
758 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
759 untranslatedMessagesFile: Uri.file('untranslated', windows: false).path,
760 syntheticPackage: false,
761 requiredResourceAttributes: true,
762 nullableGetter: false,
763 );
764
765 // Verify that values are correctly passed through the localizations target.
766 final LocalizationsGenerator generator = await generateLocalizations(
767 fileSystem: fs,
768 options: options,
769 logger: logger,
770 projectDir: fs.currentDirectory,
771 dependenciesDir: fs.currentDirectory,
772 artifacts: artifacts,
773 processManager: processManager,
774 );
775
776 expect(generator.inputDirectory.path, '/lib/l10n/');
777 expect(generator.outputDirectory.path, '/lib/l10n/');
778 expect(generator.templateArbFile.path, '/lib/l10n/app_en.arb');
779 expect(generator.baseOutputFile.path, '/lib/l10n/bar.dart');
780 expect(generator.className, 'Foo');
781 expect(generator.preferredSupportedLocales.single, LocaleInfo.fromString('es'));
782 expect(generator.header, 'HEADER');
783 expect(generator.useDeferredLoading, isTrue);
784 expect(generator.inputsAndOutputsListFile?.path, '/gen_l10n_inputs_and_outputs.json');
785 expect(generator.useSyntheticPackage, isFalse);
786 expect(generator.projectDirectory?.path, '/');
787 expect(generator.areResourceAttributesRequired, isTrue);
788 expect(generator.untranslatedMessagesFile?.path, 'untranslated');
789 expect(generator.usesNullableGetter, isFalse);
790
791 // Just validate one file.
792 expect(fs.file('/lib/l10n/bar_en.dart').readAsStringSync(), '''
793HEADER
794
795import 'bar.dart';
796
797// ignore_for_file: type=lint
798
799/// The translations for English (`en`).
800class FooEn extends Foo {
801 FooEn([String locale = 'en']) : super(locale);
802
803 @override
804 String get title => 'Title';
805}
806''');
807 });
808
809 testWithoutContext('throws exception on missing flutter: generate: true flag', () async {
810 _standardFlutterDirectoryL10nSetup(fs);
811
812 // Missing flutter: generate: true should throw exception.
813 fs.file('pubspec.yaml')
814 ..createSync(recursive: true)
815 ..writeAsStringSync('''
816flutter:
817 uses-material-design: true
818''');
819
820 final LocalizationOptions options = LocalizationOptions(
821 header: 'HEADER',
822 headerFile: Uri.file('header', windows: false).path,
823 arbDir: Uri.file('arb', windows: false).path,
824 useDeferredLoading: true,
825 outputClass: 'Foo',
826 outputLocalizationFile: Uri.file('bar', windows: false).path,
827 preferredSupportedLocales: <String>['en_US'],
828 templateArbFile: Uri.file('example.arb', windows: false).path,
829 untranslatedMessagesFile: Uri.file('untranslated', windows: false).path,
830 );
831
832 expect(
833 () => generateLocalizations(
834 fileSystem: fs,
835 options: options,
836 logger: BufferLogger.test(),
837 projectDir: fs.currentDirectory,
838 dependenciesDir: fs.currentDirectory,
839 artifacts: artifacts,
840 processManager: processManager,
841 ),
842 throwsToolExit(
843 message: 'Attempted to generate localizations code without having the '
844 'flutter: generate flag turned on.',
845 ),
846 );
847 });
848
849 testWithoutContext('uses the same line terminator as pubspec.yaml', () async {
850 _standardFlutterDirectoryL10nSetup(fs);
851
852 fs.file('pubspec.yaml')
853 ..createSync(recursive: true)
854 ..writeAsStringSync('''
855flutter:\r
856 generate: true\r
857''');
858
859 final LocalizationOptions options = LocalizationOptions(
860 arbDir: fs.path.join('lib', 'l10n'),
861 outputClass: defaultClassNameString,
862 outputLocalizationFile: defaultOutputFileString,
863 );
864 await generateLocalizations(
865 fileSystem: fs,
866 options: options,
867 logger: BufferLogger.test(),
868 projectDir: fs.currentDirectory,
869 dependenciesDir: fs.currentDirectory,
870 artifacts: artifacts,
871 processManager: processManager,
872 );
873 final String content = getGeneratedFileContent(locale: 'en');
874 expect(content, contains('\r\n'));
875 });
876
877 testWithoutContext('blank lines generated nicely', () async {
878 _standardFlutterDirectoryL10nSetup(fs);
879
880 // Test without headers.
881 await generateLocalizations(
882 fileSystem: fs,
883 options: LocalizationOptions(
884 arbDir: Uri.directory(defaultL10nPathString).path,
885 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
886 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
887 syntheticPackage: false,
888 ),
889 logger: BufferLogger.test(),
890 projectDir: fs.currentDirectory,
891 dependenciesDir: fs.currentDirectory,
892 artifacts: artifacts,
893 processManager: processManager,
894 );
895
896 expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
897import 'app_localizations.dart';
898
899// ignore_for_file: type=lint
900
901/// The translations for English (`en`).
902class AppLocalizationsEn extends AppLocalizations {
903 AppLocalizationsEn([String locale = 'en']) : super(locale);
904
905 @override
906 String get title => 'Title';
907}
908''');
909
910 // Test with headers.
911 await generateLocalizations(
912 fileSystem: fs,
913 options: LocalizationOptions(
914 header: 'HEADER',
915 arbDir: Uri.directory(defaultL10nPathString).path,
916 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
917 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
918 syntheticPackage: false,
919 ),
920 logger: logger,
921 projectDir: fs.currentDirectory,
922 dependenciesDir: fs.currentDirectory,
923 artifacts: artifacts,
924 processManager: processManager,
925 );
926
927 expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
928HEADER
929
930import 'app_localizations.dart';
931
932// ignore_for_file: type=lint
933
934/// The translations for English (`en`).
935class AppLocalizationsEn extends AppLocalizations {
936 AppLocalizationsEn([String locale = 'en']) : super(locale);
937
938 @override
939 String get title => 'Title';
940}
941''');
942 });
943 });
944
945 group('loadResources', () {
946 testWithoutContext('correctly initializes supportedLocales and supportedLanguageCodes properties', () {
947 _standardFlutterDirectoryL10nSetup(fs);
948
949 final LocalizationsGenerator generator = LocalizationsGenerator(
950 fileSystem: fs,
951 inputPathString: defaultL10nPathString,
952 outputPathString: defaultL10nPathString,
953 templateArbFileName: defaultTemplateArbFileName,
954 outputFileString: defaultOutputFileString,
955 classNameString: defaultClassNameString,
956 logger: logger,
957 )
958 ..loadResources();
959
960 expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true);
961 expect(generator.supportedLocales.contains(LocaleInfo.fromString('es')), true);
962 });
963
964 testWithoutContext('correctly sorts supportedLocales and supportedLanguageCodes alphabetically', () {
965 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
966 ..createSync(recursive: true);
967 // Write files in non-alphabetical order so that read performs in that order
968 l10nDirectory.childFile('app_zh.arb')
969 .writeAsStringSync(singleZhMessageArbFileString);
970 l10nDirectory.childFile('app_es.arb')
971 .writeAsStringSync(singleEsMessageArbFileString);
972 l10nDirectory.childFile('app_en.arb')
973 .writeAsStringSync(singleMessageArbFileString);
974
975 final LocalizationsGenerator generator = LocalizationsGenerator(
976 fileSystem: fs,
977 inputPathString: defaultL10nPathString,
978 outputPathString: defaultL10nPathString,
979 templateArbFileName: defaultTemplateArbFileName,
980 outputFileString: defaultOutputFileString,
981 classNameString: defaultClassNameString,
982 logger: logger,
983 )
984 ..loadResources();
985
986 expect(generator.supportedLocales.first, LocaleInfo.fromString('en'));
987 expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es'));
988 expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('zh'));
989 });
990
991 testWithoutContext('adds preferred locales to the top of supportedLocales and supportedLanguageCodes', () {
992 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
993 ..createSync(recursive: true);
994 l10nDirectory.childFile('app_en.arb')
995 .writeAsStringSync(singleMessageArbFileString);
996 l10nDirectory.childFile('app_es.arb')
997 .writeAsStringSync(singleEsMessageArbFileString);
998 l10nDirectory.childFile('app_zh.arb')
999 .writeAsStringSync(singleZhMessageArbFileString);
1000
1001 const List<String> preferredSupportedLocale = <String>['zh', 'es'];
1002 final LocalizationsGenerator generator = LocalizationsGenerator(
1003 fileSystem: fs,
1004 inputPathString: defaultL10nPathString,
1005 outputPathString: defaultL10nPathString,
1006 templateArbFileName: defaultTemplateArbFileName,
1007 outputFileString: defaultOutputFileString,
1008 classNameString: defaultClassNameString,
1009 preferredSupportedLocales: preferredSupportedLocale,
1010 logger: logger,
1011 )
1012 ..loadResources();
1013
1014 expect(generator.supportedLocales.first, LocaleInfo.fromString('zh'));
1015 expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es'));
1016 expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('en'));
1017 });
1018
1019 testWithoutContext(
1020 'throws an error attempting to add preferred locales when there is no corresponding arb file for that locale',
1021 () {
1022 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1023 ..createSync(recursive: true);
1024 l10nDirectory.childFile('app_en.arb')
1025 .writeAsStringSync(singleMessageArbFileString);
1026 l10nDirectory.childFile('app_es.arb')
1027 .writeAsStringSync(singleEsMessageArbFileString);
1028 l10nDirectory.childFile('app_zh.arb')
1029 .writeAsStringSync(singleZhMessageArbFileString);
1030
1031 const List<String> preferredSupportedLocale = <String>['am', 'es'];
1032 expect(
1033 () {
1034 LocalizationsGenerator(
1035 fileSystem: fs,
1036 inputPathString: defaultL10nPathString,
1037 outputPathString: defaultL10nPathString,
1038 templateArbFileName: defaultTemplateArbFileName,
1039 outputFileString: defaultOutputFileString,
1040 classNameString: defaultClassNameString,
1041 preferredSupportedLocales: preferredSupportedLocale,
1042 logger: logger,
1043 ).loadResources();
1044 },
1045 throwsA(isA<L10nException>().having(
1046 (L10nException e) => e.message,
1047 'message',
1048 contains("The preferred supported locale, 'am', cannot be added."),
1049 )),
1050 );
1051 },
1052 );
1053
1054 testWithoutContext('correctly sorts arbPathString alphabetically', () {
1055 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1056 ..createSync(recursive: true);
1057 // Write files in non-alphabetical order so that read performs in that order
1058 l10nDirectory.childFile('app_zh.arb')
1059 .writeAsStringSync(singleZhMessageArbFileString);
1060 l10nDirectory.childFile('app_es.arb')
1061 .writeAsStringSync(singleEsMessageArbFileString);
1062 l10nDirectory.childFile('app_en.arb')
1063 .writeAsStringSync(singleMessageArbFileString);
1064
1065 final LocalizationsGenerator generator = LocalizationsGenerator(
1066 fileSystem: fs,
1067 inputPathString: defaultL10nPathString,
1068 outputPathString: defaultL10nPathString,
1069 templateArbFileName: defaultTemplateArbFileName,
1070 outputFileString: defaultOutputFileString,
1071 classNameString: defaultClassNameString,
1072 logger: logger,
1073 )
1074 ..loadResources();
1075
1076 expect(generator.arbPathStrings.first, fs.path.join('lib', 'l10n', 'app_en.arb'));
1077 expect(generator.arbPathStrings.elementAt(1), fs.path.join('lib', 'l10n', 'app_es.arb'));
1078 expect(generator.arbPathStrings.elementAt(2), fs.path.join('lib', 'l10n', 'app_zh.arb'));
1079 });
1080
1081 testWithoutContext('correctly parses @@locale property in arb file', () {
1082 const String arbFileWithEnLocale = '''
1083{
1084 "@@locale": "en",
1085 "title": "Title",
1086 "@title": {
1087 "description": "Title for the application"
1088 }
1089}''';
1090
1091 const String arbFileWithZhLocale = '''
1092{
1093 "@@locale": "zh",
1094 "title": "标题",
1095 "@title": {
1096 "description": "Title for the application"
1097 }
1098}''';
1099
1100 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1101 ..createSync(recursive: true);
1102 l10nDirectory.childFile('first_file.arb')
1103 .writeAsStringSync(arbFileWithEnLocale);
1104 l10nDirectory.childFile('second_file.arb')
1105 .writeAsStringSync(arbFileWithZhLocale);
1106
1107 final LocalizationsGenerator generator = LocalizationsGenerator(
1108 fileSystem: fs,
1109 inputPathString: defaultL10nPathString,
1110 outputPathString: defaultL10nPathString,
1111 templateArbFileName: 'first_file.arb',
1112 outputFileString: defaultOutputFileString,
1113 classNameString: defaultClassNameString,
1114 logger: logger,
1115 )
1116 ..loadResources();
1117
1118 expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true);
1119 expect(generator.supportedLocales.contains(LocaleInfo.fromString('zh')), true);
1120 });
1121
1122 testWithoutContext('correctly requires @@locale property in arb file to match the filename locale suffix', () {
1123 const String arbFileWithEnLocale = '''
1124{
1125 "@@locale": "en",
1126 "title": "Stocks",
1127 "@title": {
1128 "description": "Title for the Stocks application"
1129 }
1130}''';
1131
1132 const String arbFileWithZhLocale = '''
1133{
1134 "@@locale": "zh",
1135 "title": "标题",
1136 "@title": {
1137 "description": "Title for the Stocks application"
1138 }
1139}''';
1140
1141 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1142 ..createSync(recursive: true);
1143 l10nDirectory.childFile('app_es.arb')
1144 .writeAsStringSync(arbFileWithEnLocale);
1145 l10nDirectory.childFile('app_am.arb')
1146 .writeAsStringSync(arbFileWithZhLocale);
1147
1148 expect(
1149 () {
1150 LocalizationsGenerator(
1151 fileSystem: fs,
1152 inputPathString: defaultL10nPathString,
1153 outputPathString: defaultL10nPathString,
1154 templateArbFileName: 'app_es.arb',
1155 outputFileString: defaultOutputFileString,
1156 classNameString: defaultClassNameString,
1157 logger: logger,
1158 ).loadResources();
1159 },
1160 throwsA(isA<L10nException>().having(
1161 (L10nException e) => e.message,
1162 'message',
1163 contains('The locale specified in @@locale and the arb filename do not match.'),
1164 )),
1165 );
1166 });
1167
1168 testWithoutContext("throws when arb file's locale could not be determined", () {
1169 fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1170 ..createSync(recursive: true)
1171 ..childFile('app.arb')
1172 .writeAsStringSync(singleMessageArbFileString);
1173 expect(
1174 () {
1175 LocalizationsGenerator(
1176 fileSystem: fs,
1177 inputPathString: defaultL10nPathString,
1178 outputPathString: defaultL10nPathString,
1179 templateArbFileName: 'app.arb',
1180 outputFileString: defaultOutputFileString,
1181 classNameString: defaultClassNameString,
1182 logger: logger,
1183 ).loadResources();
1184 },
1185 throwsA(isA<L10nException>().having(
1186 (L10nException e) => e.message,
1187 'message',
1188 contains('locale could not be determined'),
1189 )),
1190 );
1191 });
1192
1193 testWithoutContext('throws when an empty string is used as a key', () {
1194 const String arbFileStringWithEmptyResourceId = '''
1195{
1196 "market": "MARKET",
1197 "": {
1198 "description": "This key is invalid"
1199 }
1200}''';
1201
1202 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1203 ..createSync(recursive: true);
1204 l10nDirectory.childFile('app_en.arb')
1205 .writeAsStringSync(arbFileStringWithEmptyResourceId);
1206
1207 expect(
1208 () => LocalizationsGenerator(
1209 fileSystem: fs,
1210 inputPathString: defaultL10nPathString,
1211 outputPathString: defaultL10nPathString,
1212 templateArbFileName: 'app_en.arb',
1213 outputFileString: defaultOutputFileString,
1214 classNameString: defaultClassNameString,
1215 logger: logger,
1216 ).loadResources(),
1217 throwsA(isA<L10nException>().having(
1218 (L10nException e) => e.message,
1219 'message',
1220 contains('Invalid ARB resource name ""'),
1221 )),
1222 );
1223 });
1224
1225 testWithoutContext('throws when the same locale is detected more than once', () {
1226 const String secondMessageArbFileString = '''
1227{
1228 "market": "MARKET",
1229 "@market": {
1230 "description": "Label for the Market tab"
1231 }
1232}''';
1233
1234 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1235 ..createSync(recursive: true);
1236 l10nDirectory.childFile('app_en.arb')
1237 .writeAsStringSync(singleMessageArbFileString);
1238 l10nDirectory.childFile('app2_en.arb')
1239 .writeAsStringSync(secondMessageArbFileString);
1240
1241 expect(
1242 () {
1243 LocalizationsGenerator(
1244 fileSystem: fs,
1245 inputPathString: defaultL10nPathString,
1246 outputPathString: defaultL10nPathString,
1247 templateArbFileName: 'app_en.arb',
1248 outputFileString: defaultOutputFileString,
1249 classNameString: defaultClassNameString,
1250 logger: logger,
1251 ).loadResources();
1252 },
1253 throwsA(isA<L10nException>().having(
1254 (L10nException e) => e.message,
1255 'message',
1256 contains("Multiple arb files with the same 'en' locale detected"),
1257 )),
1258 );
1259 });
1260
1261 testWithoutContext('throws when the base locale does not exist', () {
1262 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1263 ..createSync(recursive: true);
1264 l10nDirectory.childFile('app_en_US.arb')
1265 .writeAsStringSync(singleMessageArbFileString);
1266
1267 expect(
1268 () {
1269 LocalizationsGenerator(
1270 fileSystem: fs,
1271 inputPathString: defaultL10nPathString,
1272 outputPathString: defaultL10nPathString,
1273 templateArbFileName: 'app_en_US.arb',
1274 outputFileString: defaultOutputFileString,
1275 classNameString: defaultClassNameString,
1276 logger: logger,
1277 ).loadResources();
1278 },
1279 throwsA(isA<L10nException>().having(
1280 (L10nException e) => e.message,
1281 'message',
1282 contains('Arb file for a fallback, en, does not exist'),
1283 )),
1284 );
1285 });
1286
1287 testWithoutContext('AppResourceBundle throws if file contains non-string value', () {
1288 const String inputPathString = 'lib/l10n';
1289 const String templateArbFileName = 'app_en.arb';
1290 const String outputFileString = 'app_localizations.dart';
1291 const String classNameString = 'AppLocalizations';
1292
1293 fs.file(fs.path.join(inputPathString, templateArbFileName))
1294 ..createSync(recursive: true)
1295 ..writeAsStringSync('{ "helloWorld": "Hello World!" }');
1296 fs.file(fs.path.join(inputPathString, 'app_es.arb'))
1297 ..createSync(recursive: true)
1298 ..writeAsStringSync('{ "helloWorld": {} }');
1299
1300 final LocalizationsGenerator generator = LocalizationsGenerator(
1301 fileSystem: fs,
1302 inputPathString: inputPathString,
1303 templateArbFileName: templateArbFileName,
1304 outputFileString: outputFileString,
1305 classNameString: classNameString,
1306 logger: logger,
1307 );
1308 expect(
1309 () => generator.loadResources(),
1310 throwsToolExit(message: 'Localized message for key "helloWorld" in '
1311 '"lib/l10n/app_es.arb" is not a string.'),
1312 );
1313 });
1314 });
1315
1316 group('writeOutputFiles', () {
1317 testWithoutContext('multiple messages with syntax error all log their errors', () {
1318 try {
1319 setupLocalizations(<String, String>{
1320 'en': r'''
1321{
1322 "msg1": "{",
1323 "msg2": "{ {"
1324}'''});
1325 } on L10nException catch (error) {
1326 expect(error.message, equals('Found syntax errors.'));
1327 expect(logger.errorText, contains('''
1328[app_en.arb:msg1] ICU Syntax Error: Expected "identifier" but found no tokens.
1329 {
1330 ^
1331[app_en.arb:msg2] ICU Syntax Error: Expected "identifier" but found "{".
1332 { {
1333 ^'''));
1334 }
1335 });
1336
1337 testWithoutContext('no description generates generic comment', () {
1338 setupLocalizations(<String, String>{
1339 'en': r'''
1340{
1341 "helloWorld": "Hello world!"
1342}'''
1343 });
1344 expect(getGeneratedFileContent(), contains('/// No description provided for @helloWorld.'));
1345 });
1346
1347 testWithoutContext('multiline descriptions are correctly formatted as comments', () {
1348 setupLocalizations(<String, String>{
1349 'en': r'''
1350{
1351 "helloWorld": "Hello world!",
1352 "@helloWorld": {
1353 "description": "The generic example string in every language.\nUse this for tests!"
1354 }
1355}'''});
1356 expect(getGeneratedFileContent(), contains('''
1357 /// The generic example string in every language.
1358 /// Use this for tests!'''));
1359 });
1360
1361 testWithoutContext('message without placeholders - should generate code comment with description and template message translation', () {
1362 setupLocalizations(<String, String> {
1363 'en': singleMessageArbFileString,
1364 'es': singleEsMessageArbFileString,
1365 });
1366 final String content = getGeneratedFileContent();
1367 expect(content, contains('/// Title for the application.'));
1368 expect(content, contains('''
1369 /// In en, this message translates to:
1370 /// **'Title'**'''));
1371 });
1372
1373 testWithoutContext('template message translation handles newline characters', () {
1374 setupLocalizations(<String, String>{
1375 'en': r'''
1376{
1377 "title": "Title \n of the application",
1378 "@title": {
1379 "description": "Title for the application."
1380 }
1381}''',
1382 'es': singleEsMessageArbFileString
1383 });
1384 final String content = getGeneratedFileContent();
1385 expect(content, contains('/// Title for the application.'));
1386 expect(content, contains(r'''
1387 /// In en, this message translates to:
1388 /// **'Title \n of the application'**'''));
1389 });
1390
1391 testWithoutContext('message with placeholders - should generate code comment with description and template message translation', () {
1392 setupLocalizations(<String, String>{
1393 'en': r'''
1394{
1395 "price": "The price of this item is: ${price}",
1396 "@price": {
1397 "description": "The price of an online shopping cart item.",
1398 "placeholders": {
1399 "price": {
1400 "type": "double",
1401 "format": "decimalPattern"
1402 }
1403 }
1404 }
1405}''',
1406 'es': r'''
1407{
1408 "price": "El precio de este artículo es: ${price}"
1409}'''
1410 });
1411 final String content = getGeneratedFileContent();
1412 expect(content, contains('/// The price of an online shopping cart item.'));
1413 expect(content, contains(r'''
1414 /// In en, this message translates to:
1415 /// **'The price of this item is: \${price}'**'''));
1416 });
1417
1418 testWithoutContext('should generate a file per language', () {
1419 setupLocalizations(<String, String>{
1420 'en': singleMessageArbFileString,
1421 'en_CA': '''
1422{
1423 "title": "Canadian Title"
1424}'''
1425 });
1426 expect(getGeneratedFileContent(locale: 'en'), contains('class AppLocalizationsEn extends AppLocalizations'));
1427 expect(getGeneratedFileContent(locale: 'en'), contains('class AppLocalizationsEnCa extends AppLocalizationsEn'));
1428 expect(() => getGeneratedFileContent(locale: 'en_US'), throwsException);
1429 });
1430
1431 testWithoutContext('language imports are sorted when preferredSupportedLocaleString is given', () {
1432 const List<String> preferredSupportedLocales = <String>['zh'];
1433 setupLocalizations(<String, String>{
1434 'en': singleMessageArbFileString,
1435 'zh': singleZhMessageArbFileString,
1436 'es': singleEsMessageArbFileString,
1437 }, preferredSupportedLocales: preferredSupportedLocales);
1438 final String content = getGeneratedFileContent();
1439 expect(content, contains(
1440'''
1441import 'output-localization-file_en.dart';
1442import 'output-localization-file_es.dart';
1443import 'output-localization-file_zh.dart';
1444'''));
1445 });
1446
1447 // Regression test for https://github.com/flutter/flutter/issues/88356
1448 testWithoutContext('full output file suffix is retained', () {
1449 setupLocalizations(<String, String>{
1450 'en': singleMessageArbFileString,
1451 }, outputFileString: 'output-localization-file.g.dart');
1452 final String baseLocalizationsFile = fs.file(
1453 fs.path.join(syntheticL10nPackagePath, 'output-localization-file.g.dart'),
1454 ).readAsStringSync();
1455 expect(baseLocalizationsFile, contains(
1456'''
1457import 'output-localization-file_en.g.dart';
1458'''));
1459
1460 final String englishLocalizationsFile = fs.file(
1461 fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.g.dart'),
1462 ).readAsStringSync();
1463 expect(englishLocalizationsFile, contains(
1464'''
1465import 'output-localization-file.g.dart';
1466'''));
1467 });
1468
1469 testWithoutContext('throws an exception when invalid output file name is passed in', () {
1470 expect(
1471 () {
1472 setupLocalizations(<String, String>{
1473 'en': singleMessageArbFileString,
1474 }, outputFileString: 'asdf');
1475 },
1476 throwsA(isA<L10nException>().having(
1477 (L10nException e) => e.message,
1478 'message',
1479 allOf(
1480 contains('output-localization-file'),
1481 contains('asdf'),
1482 contains('is invalid'),
1483 contains('The file name must have a .dart extension.'),
1484 ),
1485 )),
1486 );
1487 expect(
1488 () {
1489 setupLocalizations(<String, String>{
1490 'en': singleMessageArbFileString,
1491 }, outputFileString: '.g.dart');
1492 },
1493 throwsA(isA<L10nException>().having(
1494 (L10nException e) => e.message,
1495 'message',
1496 allOf(
1497 contains('output-localization-file'),
1498 contains('.g.dart'),
1499 contains('is invalid'),
1500 contains('The base name cannot be empty.'),
1501 ),
1502 )),
1503 );
1504 });
1505
1506 testWithoutContext('imports are deferred and loaded when useDeferredImports are set', () {
1507 setupLocalizations(<String, String>{
1508 'en': singleMessageArbFileString,
1509 }, useDeferredLoading: true);
1510 final String content = getGeneratedFileContent();
1511 expect(content, contains(
1512'''
1513import 'output-localization-file_en.dart' deferred as output-localization-file_en;
1514'''));
1515 expect(content, contains('output-localization-file_en.loadLibrary()'));
1516 });
1517
1518 group('placeholder tests', () {
1519 testWithoutContext('should automatically infer placeholders that are not explicitly defined', () {
1520 setupLocalizations(<String, String>{
1521 'en': '''
1522{
1523 "helloWorld": "Hello {name}"
1524}'''
1525 });
1526 final String content = getGeneratedFileContent(locale: 'en');
1527 expect(content, contains('String helloWorld(Object name) {'));
1528 });
1529
1530 testWithoutContext('placeholder parameter list should be consistent between languages', () {
1531 setupLocalizations(<String, String>{
1532 'en': '''
1533{
1534 "helloWorld": "Hello {name}",
1535 "@helloWorld": {
1536 "placeholders": {
1537 "name": {}
1538 }
1539 }
1540}''',
1541 'es': '''
1542{
1543 "helloWorld": "Hola"
1544}
1545''',
1546 });
1547 expect(getGeneratedFileContent(locale: 'en'), contains('String helloWorld(Object name) {'));
1548 expect(getGeneratedFileContent(locale: 'es'), contains('String helloWorld(Object name) {'));
1549 });
1550
1551 testWithoutContext('braces are ignored as special characters if placeholder does not exist', () {
1552 setupLocalizations(<String, String>{
1553 'en': '''
1554{
1555 "helloWorld": "Hello {name}",
1556 "@@helloWorld": {
1557 "placeholders": {
1558 "names": {}
1559 }
1560 }
1561}'''
1562 }, relaxSyntax: true);
1563 final String content = getGeneratedFileContent(locale: 'en');
1564 expect(content, contains("String get helloWorld => 'Hello {name}'"));
1565 });
1566 });
1567
1568 group('DateTime tests', () {
1569 testWithoutContext('imports package:intl', () {
1570 setupLocalizations(<String, String>{
1571 'en': '''
1572{
1573 "@@locale": "en",
1574 "springBegins": "Spring begins on {springStartDate}",
1575 "@springBegins": {
1576 "description": "The first day of spring",
1577 "placeholders": {
1578 "springStartDate": {
1579 "type": "DateTime",
1580 "format": "yMd"
1581 }
1582 }
1583 }
1584}'''
1585 });
1586 expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
1587 });
1588
1589 testWithoutContext('throws an exception when improperly formatted date is passed in', () {
1590 expect(
1591 () {
1592 setupLocalizations(<String, String>{
1593 'en': '''
1594{
1595 "springBegins": "Spring begins on {springStartDate}",
1596 "@springBegins": {
1597 "placeholders": {
1598 "springStartDate": {
1599 "type": "DateTime",
1600 "format": "asdf"
1601 }
1602 }
1603 }
1604}'''
1605 });
1606 },
1607 throwsA(isA<L10nException>().having(
1608 (L10nException e) => e.message,
1609 'message',
1610 allOf(
1611 contains('asdf'),
1612 contains('springStartDate'),
1613 contains('does not have a corresponding DateFormat'),
1614 ),
1615 )),
1616 );
1617 });
1618
1619 testWithoutContext('use standard date format whenever possible', () {
1620 setupLocalizations(<String, String>{
1621 'en': '''
1622{
1623 "springBegins": "Spring begins on {springStartDate}",
1624 "@springBegins": {
1625 "placeholders": {
1626 "springStartDate": {
1627 "type": "DateTime",
1628 "format": "yMd",
1629 "isCustomDateFormat": "true"
1630 }
1631 }
1632 }
1633}'''
1634 });
1635 final String content = getGeneratedFileContent(locale: 'en');
1636 expect(content, contains('DateFormat.yMd(localeName)'));
1637 });
1638
1639 testWithoutContext('handle arbitrary formatted date', () {
1640 setupLocalizations(<String, String>{
1641 'en': '''
1642{
1643 "@@locale": "en",
1644 "springBegins": "Spring begins on {springStartDate}",
1645 "@springBegins": {
1646 "description": "The first day of spring",
1647 "placeholders": {
1648 "springStartDate": {
1649 "type": "DateTime",
1650 "format": "asdf o'clock",
1651 "isCustomDateFormat": "true"
1652 }
1653 }
1654 }
1655}'''
1656 });
1657 final String content = getGeneratedFileContent(locale: 'en');
1658 expect(content, contains(r"DateFormat('asdf o\'clock', localeName)"));
1659 });
1660
1661 testWithoutContext('handle arbitrary formatted date with actual boolean', () {
1662 setupLocalizations(<String, String>{
1663 'en': '''
1664{
1665 "@@locale": "en",
1666 "springBegins": "Spring begins on {springStartDate}",
1667 "@springBegins": {
1668 "description": "The first day of spring",
1669 "placeholders": {
1670 "springStartDate": {
1671 "type": "DateTime",
1672 "format": "asdf o'clock",
1673 "isCustomDateFormat": true
1674 }
1675 }
1676 }
1677}'''
1678 });
1679 final String content = getGeneratedFileContent(locale: 'en');
1680 expect(content, contains(r"DateFormat('asdf o\'clock', localeName)"));
1681 });
1682
1683 testWithoutContext('throws an exception when no format attribute is passed in', () {
1684 expect(
1685 () {
1686 setupLocalizations(<String, String>{
1687 'en': '''
1688{
1689 "springBegins": "Spring begins on {springStartDate}",
1690 "@springBegins": {
1691 "description": "The first day of spring",
1692 "placeholders": {
1693 "springStartDate": {
1694 "type": "DateTime"
1695 }
1696 }
1697 }
1698}'''
1699 });
1700 },
1701 throwsA(isA<L10nException>().having(
1702 (L10nException e) => e.message,
1703 'message',
1704 contains('the "format" attribute needs to be set'),
1705 )),
1706 );
1707 });
1708 });
1709
1710 group('NumberFormat tests', () {
1711 testWithoutContext('imports package:intl', () {
1712 setupLocalizations(<String, String>{
1713 'en': '''
1714{
1715 "courseCompletion": "You have completed {progress} of the course.",
1716 "@courseCompletion": {
1717 "description": "The amount of progress the student has made in their class.",
1718 "placeholders": {
1719 "progress": {
1720 "type": "double",
1721 "format": "percentPattern"
1722 }
1723 }
1724 }
1725}'''
1726 });
1727 final String content = getGeneratedFileContent(locale: 'en');
1728 expect(content, contains(intlImportDartCode));
1729 });
1730
1731 testWithoutContext('throws an exception when improperly formatted number is passed in', () {
1732 expect(
1733 () {
1734 setupLocalizations(<String, String>{
1735 'en': '''
1736{
1737 "courseCompletion": "You have completed {progress} of the course.",
1738 "@courseCompletion": {
1739 "description": "The amount of progress the student has made in their class.",
1740 "placeholders": {
1741 "progress": {
1742 "type": "double",
1743 "format": "asdf"
1744 }
1745 }
1746 }
1747}'''
1748 });
1749 },
1750 throwsA(isA<L10nException>().having(
1751 (L10nException e) => e.message,
1752 'message',
1753 allOf(
1754 contains('asdf'),
1755 contains('progress'),
1756 contains('does not have a corresponding NumberFormat'),
1757 ),
1758 )),
1759 );
1760 });
1761 });
1762
1763 group('plural messages', () {
1764 testWithoutContext('intl package import should be omitted in subclass files when no plurals are included', () {
1765 setupLocalizations(<String, String>{
1766 'en': singleMessageArbFileString,
1767 'es': singleEsMessageArbFileString,
1768 });
1769 expect(getGeneratedFileContent(locale: 'es'), isNot(contains(intlImportDartCode)));
1770 });
1771
1772 testWithoutContext('warnings are generated when plural parts are repeated', () {
1773 setupLocalizations(<String, String>{
1774 'en': '''
1775{
1776 "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
1777 "@helloWorlds": {
1778 "description": "Properly formatted but has redundant zero cases."
1779 }
1780}'''
1781 });
1782 expect(logger.hadWarningOutput, isTrue);
1783 expect(logger.warningText, contains('''
1784[app_en.arb:helloWorlds] ICU Syntax Warning: The plural part specified below is overridden by a later plural part.
1785 {count,plural, =0{Hello}zero{hello} other{hi}}
1786 ^'''));
1787 });
1788
1789 testWithoutContext('undefined plural cases throws syntax error', () {
1790 try {
1791 setupLocalizations(<String, String>{
1792 'en': '''
1793{
1794 "count": "{count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}}"
1795}'''
1796 });
1797 } on L10nException catch (error) {
1798 expect(error.message, contains('Found syntax errors.'));
1799 expect(logger.hadErrorOutput, isTrue);
1800 expect(logger.errorText, contains('''
1801[app_en.arb:count] The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "many", or "other.
1802 3 is not a valid plural case.
1803 {count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}}
1804 ^'''));
1805 }
1806 });
1807
1808 testWithoutContext('should automatically infer plural placeholders that are not explicitly defined', () {
1809 setupLocalizations(<String, String>{
1810 'en': '''
1811{
1812 "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}}",
1813 "@helloWorlds": {
1814 "description": "Improperly formatted since it has no placeholder attribute."
1815 }
1816}'''
1817 });
1818 expect(getGeneratedFileContent(locale: 'en'), contains('String helloWorlds(num count) {'));
1819 });
1820
1821 testWithoutContext('should throw attempting to generate a plural message with incorrect format for placeholders', () {
1822 expect(
1823 () {
1824 setupLocalizations(<String, String>{
1825 'en': '''
1826{
1827 "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}}",
1828 "@helloWorlds": {
1829 "placeholders": "Incorrectly a string, should be a map."
1830 }
1831}'''
1832 });
1833 },
1834 throwsA(isA<L10nException>().having(
1835 (L10nException e) => e.message,
1836 'message',
1837 allOf(
1838 contains('is not properly formatted'),
1839 contains('Ensure that it is a map with string valued keys'),
1840 ),
1841 )),
1842 );
1843 });
1844 });
1845
1846 group('select messages', () {
1847 testWithoutContext('should automatically infer select placeholders that are not explicitly defined', () {
1848 setupLocalizations(<String, String>{
1849 'en': '''
1850{
1851 "genderSelect": "{gender, select, female {She} male {He} other {they} }",
1852 "@genderSelect": {
1853 "description": "Improperly formatted since it has no placeholder attribute."
1854 }
1855}'''
1856 });
1857 expect(getGeneratedFileContent(locale: 'en'), contains('String genderSelect(String gender) {'));
1858 });
1859
1860 testWithoutContext('should throw attempting to generate a select message with incorrect format for placeholders', () {
1861 expect(
1862 () {
1863 setupLocalizations(<String, String>{
1864 'en': '''
1865{
1866 "genderSelect": "{gender, select, female {She} male {He} other {they} }",
1867 "@genderSelect": {
1868 "placeholders": "Incorrectly a string, should be a map."
1869 }
1870}'''
1871 });
1872 },
1873 throwsA(isA<L10nException>().having(
1874 (L10nException e) => e.message,
1875 'message',
1876 allOf(
1877 contains('is not properly formatted'),
1878 contains('Ensure that it is a map with string valued keys'),
1879 ),
1880 )),
1881 );
1882 });
1883
1884 testWithoutContext('should throw attempting to generate a select message with an incorrect message', () {
1885 try {
1886 setupLocalizations(<String, String>{
1887 'en': '''
1888{
1889 "genderSelect": "{gender, select,}",
1890 "@genderSelect": {
1891 "placeholders": {
1892 "gender": {}
1893 }
1894 }
1895}'''
1896 });
1897 } on L10nException {
1898 expect(logger.errorText, contains('''
1899[app_en.arb:genderSelect] ICU Syntax Error: Select expressions must have an "other" case.
1900 {gender, select,}
1901 ^''')
1902 );
1903 }
1904 });
1905 });
1906
1907 group('argument messages', () {
1908 testWithoutContext('should generate proper calls to intl.DateFormat', () {
1909 setupLocalizations(<String, String>{
1910 'en': '''
1911{
1912 "datetime": "{today, date, ::yMd}"
1913}'''
1914 });
1915 expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.yMd(localeName).format(today)'));
1916 });
1917
1918 testWithoutContext('should generate proper calls to intl.DateFormat when using time', () {
1919 setupLocalizations(<String, String>{
1920 'en': '''
1921{
1922 "datetime": "{current, time, ::jms}"
1923}'''
1924 });
1925 expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.jms(localeName).format(current)'));
1926 });
1927
1928 testWithoutContext('should not complain when placeholders are explicitly typed to DateTime', () {
1929 setupLocalizations(<String, String>{
1930 'en': '''
1931{
1932 "datetime": "{today, date, ::yMd}",
1933 "@datetime": {
1934 "placeholders": {
1935 "today": { "type": "DateTime" }
1936 }
1937 }
1938}'''
1939 });
1940 expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {'));
1941 });
1942
1943 testWithoutContext('should automatically infer date time placeholders that are not explicitly defined', () {
1944 setupLocalizations(<String, String>{
1945 'en': '''
1946{
1947 "datetime": "{today, date, ::yMd}"
1948}'''
1949 });
1950 expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {'));
1951 });
1952
1953 testWithoutContext('should throw on invalid DateFormat', () {
1954 try {
1955 setupLocalizations(<String, String>{
1956 'en': '''
1957{
1958 "datetime": "{today, date, ::yMMMMMd}"
1959}'''
1960 });
1961 assert(false);
1962 } on L10nException {
1963 expect(logger.errorText, contains('Date format "yMMMMMd" for placeholder today does not have a corresponding DateFormat constructor'));
1964 }
1965 });
1966 });
1967
1968 // All error handling for messages should collect errors on a per-error
1969 // basis and log them out individually. Then, it will throw an L10nException.
1970 group('error handling tests', () {
1971 testWithoutContext('syntax/code-gen errors properly logs errors per message', () {
1972 // TODO(thkim1011): Fix error handling so that long indents don't get truncated.
1973 // See https://github.com/flutter/flutter/issues/120490.
1974 try {
1975 setupLocalizations(<String, String>{
1976 'en': '''
1977{
1978 "hello": "Hello { name",
1979 "plural": "This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}",
1980 "explanationWithLexingError": "The 'string above is incorrect as it forgets to close the brace",
1981 "pluralWithInvalidCase": "{ count, plural, woohoo{huh?} other{lol} }"
1982}'''
1983 }, useEscaping: true);
1984 } on L10nException {
1985 expect(logger.errorText, contains('''
1986[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
1987 Hello { name
1988 ^
1989[app_en.arb:plural] ICU Syntax Error: Expected "}" but found no tokens.
1990 This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}
1991 ^
1992[app_en.arb:explanationWithLexingError] ICU Lexing Error: Unmatched single quotes.
1993 The 'string above is incorrect as it forgets to close the brace
1994 ^
1995[app_en.arb:pluralWithInvalidCase] ICU Syntax Error: Plural expressions case must be one of "zero", "one", "two", "few", "many", or "other".
1996 { count, plural, woohoo{huh?} other{lol} }
1997 ^'''));
1998 }
1999 });
2000
2001 testWithoutContext('errors thrown in multiple languages are all shown', () {
2002 try {
2003 setupLocalizations(<String, String>{
2004 'en': '{ "hello": "Hello { name" }',
2005 'es': '{ "hello": "Hola { name" }',
2006 });
2007 } on L10nException {
2008 expect(logger.errorText, contains('''
2009[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2010 Hello { name
2011 ^
2012[app_es.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2013 Hola { name
2014 ^'''));
2015 }
2016 });
2017 });
2018
2019
2020 testWithoutContext('intl package import should be kept in subclass files when plurals are included', () {
2021 const String pluralMessageArb = '''
2022{
2023 "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}}",
2024 "@helloWorlds": {
2025 "description": "A plural message",
2026 "placeholders": {
2027 "count": {}
2028 }
2029 }
2030}
2031''';
2032 const String pluralMessageEsArb = '''
2033{
2034 "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}}"
2035}
2036''';
2037 setupLocalizations(<String, String>{
2038 'en': pluralMessageArb,
2039 'es': pluralMessageEsArb,
2040 });
2041 expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
2042 expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
2043 });
2044
2045 testWithoutContext('intl package import should be kept in subclass files when select is included', () {
2046 const String selectMessageArb = '''
2047{
2048 "genderSelect": "{gender, select, female {She} male {He} other {they} }",
2049 "@genderSelect": {
2050 "description": "A select message",
2051 "placeholders": {
2052 "gender": {}
2053 }
2054 }
2055}
2056''';
2057 const String selectMessageEsArb = '''
2058{
2059 "genderSelect": "{gender, select, female {ES - She} male {ES - He} other {ES - they} }"
2060}
2061''';
2062 setupLocalizations(<String, String>{
2063 'en': selectMessageArb,
2064 'es': selectMessageEsArb,
2065 });
2066 expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
2067 expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
2068 });
2069
2070 testWithoutContext('check indentation on generated files', () {
2071 setupLocalizations(<String, String>{
2072 'en': singleMessageArbFileString,
2073 'es': singleEsMessageArbFileString,
2074 });
2075 // Tests a few of the lines in the generated code.
2076 // Localizations lookup code
2077 final String localizationsFile = getGeneratedFileContent();
2078 expect(localizationsFile.contains(' switch (locale.languageCode) {'), true);
2079 expect(localizationsFile.contains(" case 'en': return AppLocalizationsEn();"), true);
2080 expect(localizationsFile.contains(" case 'es': return AppLocalizationsEs();"), true);
2081 expect(localizationsFile.contains(' }'), true);
2082
2083 // Supported locales list
2084 expect(localizationsFile.contains(' static const List<Locale> supportedLocales = <Locale>['), true);
2085 expect(localizationsFile.contains(" Locale('en'),"), true);
2086 expect(localizationsFile.contains(" Locale('es')"), true);
2087 expect(localizationsFile.contains(' ];'), true);
2088 });
2089
2090 testWithoutContext('foundation package import should be omitted from file template when deferred loading = true', () {
2091 setupLocalizations(<String, String>{
2092 'en': singleMessageArbFileString,
2093 'es': singleEsMessageArbFileString,
2094 }, useDeferredLoading: true);
2095 expect(getGeneratedFileContent(), isNot(contains(foundationImportDartCode)));
2096 });
2097
2098 testWithoutContext('foundation package import should be kept in file template when deferred loading = false', () {
2099 setupLocalizations(<String, String>{
2100 'en': singleMessageArbFileString,
2101 'es': singleEsMessageArbFileString,
2102 });
2103 expect(getGeneratedFileContent(), contains(foundationImportDartCode));
2104 });
2105
2106 testWithoutContext('check for string interpolation rules', () {
2107 const String enArbCheckList = '''
2108{
2109 "one": "The number of {one} elapsed is: 44",
2110 "@one": {
2111 "description": "test one",
2112 "placeholders": {
2113 "one": {
2114 "type": "String"
2115 }
2116 }
2117 },
2118 "two": "哈{two}哈",
2119 "@two": {
2120 "description": "test two",
2121 "placeholders": {
2122 "two": {
2123 "type": "String"
2124 }
2125 }
2126 },
2127 "three": "m{three}m",
2128 "@three": {
2129 "description": "test three",
2130 "placeholders": {
2131 "three": {
2132 "type": "String"
2133 }
2134 }
2135 },
2136 "four": "I have to work _{four}_ sometimes.",
2137 "@four": {
2138 "description": "test four",
2139 "placeholders": {
2140 "four": {
2141 "type": "String"
2142 }
2143 }
2144 },
2145 "five": "{five} elapsed.",
2146 "@five": {
2147 "description": "test five",
2148 "placeholders": {
2149 "five": {
2150 "type": "String"
2151 }
2152 }
2153 },
2154 "six": "{six}m",
2155 "@six": {
2156 "description": "test six",
2157 "placeholders": {
2158 "six": {
2159 "type": "String"
2160 }
2161 }
2162 },
2163 "seven": "hours elapsed: {seven}",
2164 "@seven": {
2165 "description": "test seven",
2166 "placeholders": {
2167 "seven": {
2168 "type": "String"
2169 }
2170 }
2171 },
2172 "eight": " {eight}",
2173 "@eight": {
2174 "description": "test eight",
2175 "placeholders": {
2176 "eight": {
2177 "type": "String"
2178 }
2179 }
2180 },
2181 "nine": "m{nine}",
2182 "@nine": {
2183 "description": "test nine",
2184 "placeholders": {
2185 "nine": {
2186 "type": "String"
2187 }
2188 }
2189 }
2190}
2191''';
2192
2193 // It's fine that the arb is identical -- Just checking
2194 // generated code for use of '${variable}' vs '$variable'
2195 const String esArbCheckList = '''
2196{
2197 "one": "The number of {one} elapsed is: 44",
2198 "two": "哈{two}哈",
2199 "three": "m{three}m",
2200 "four": "I have to work _{four}_ sometimes.",
2201 "five": "{five} elapsed.",
2202 "six": "{six}m",
2203 "seven": "hours elapsed: {seven}",
2204 "eight": " {eight}",
2205 "nine": "m{nine}"
2206}
2207''';
2208 setupLocalizations(<String, String>{
2209 'en': enArbCheckList,
2210 'es': esArbCheckList,
2211 });
2212 final String localizationsFile = getGeneratedFileContent(locale: 'es');
2213 expect(localizationsFile, contains(r'$one'));
2214 expect(localizationsFile, contains(r'$two'));
2215 expect(localizationsFile, contains(r'${three}'));
2216 expect(localizationsFile, contains(r'${four}'));
2217 expect(localizationsFile, contains(r'$five'));
2218 expect(localizationsFile, contains(r'${six}m'));
2219 expect(localizationsFile, contains(r'$seven'));
2220 expect(localizationsFile, contains(r'$eight'));
2221 expect(localizationsFile, contains(r'$nine'));
2222 });
2223
2224 testWithoutContext('check for string interpolation rules - plurals', () {
2225 const String enArbCheckList = '''
2226{
2227 "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
2228 "@first": {
2229 "description": "First set of plural messages to test.",
2230 "placeholders": {
2231 "count": {}
2232 }
2233 },
2234 "second": "{count,plural, =0{test {count}} other{ {count}}}",
2235 "@second": {
2236 "description": "Second set of plural messages to test.",
2237 "placeholders": {
2238 "count": {}
2239 }
2240 },
2241 "third": "{total,plural, =0{test {total}} other{ {total}}}",
2242 "@third": {
2243 "description": "Third set of plural messages to test, for number.",
2244 "placeholders": {
2245 "total": {
2246 "type": "int",
2247 "format": "compactLong"
2248 }
2249 }
2250 }
2251}
2252''';
2253
2254 // It's fine that the arb is identical -- Just checking
2255 // generated code for use of '${variable}' vs '$variable'
2256 const String esArbCheckList = '''
2257{
2258 "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
2259 "second": "{count,plural, =0{test {count}} other{ {count}}}"
2260}
2261''';
2262 setupLocalizations(<String, String>{
2263 'en': enArbCheckList,
2264 'es': esArbCheckList,
2265 });
2266 final String localizationsFile = getGeneratedFileContent(locale: 'es');
2267 expect(localizationsFile, contains(r'test $count test'));
2268 expect(localizationsFile, contains(r'哈$count哈'));
2269 expect(localizationsFile, contains(r'm${count}m'));
2270 expect(localizationsFile, contains(r'_${count}_'));
2271 expect(localizationsFile, contains(r'$count test'));
2272 expect(localizationsFile, contains(r'${count}m'));
2273 expect(localizationsFile, contains(r'test $count'));
2274 expect(localizationsFile, contains(r' $count'));
2275 expect(localizationsFile, contains(r'String totalString = totalNumberFormat'));
2276 expect(localizationsFile, contains(r'totalString'));
2277 expect(localizationsFile, contains(r'totalString'));
2278 });
2279
2280 testWithoutContext(
2281 'should throw with descriptive error message when failing to parse the '
2282 'arb file',
2283 () {
2284 const String arbFileWithTrailingComma = '''
2285{
2286 "title": "Stocks",
2287 "@title": {
2288 "description": "Title for the Stocks application"
2289 },
2290}''';
2291 expect(
2292 () {
2293 setupLocalizations(<String, String>{ 'en': arbFileWithTrailingComma });
2294 },
2295 throwsA(isA<L10nException>().having(
2296 (L10nException e) => e.message,
2297 'message',
2298 allOf(
2299 contains('app_en.arb'),
2300 contains('FormatException'),
2301 contains('Unexpected character'),
2302 ),
2303 )),
2304 );
2305 },
2306 );
2307
2308 testWithoutContext('should throw when resource is missing resource attribute (isResourceAttributeRequired = true)', () {
2309 const String arbFileWithMissingResourceAttribute = '''
2310{
2311 "title": "Stocks"
2312}''';
2313 expect(
2314 () {
2315 setupLocalizations(
2316 <String, String>{ 'en': arbFileWithMissingResourceAttribute },
2317 areResourceAttributeRequired: true,
2318 );
2319 },
2320 throwsA(isA<L10nException>().having(
2321 (L10nException e) => e.message,
2322 'message',
2323 contains('Resource attribute "@title" was not found'),
2324 )),
2325 );
2326 });
2327
2328 group('checks for method/getter formatting', () {
2329 testWithoutContext('cannot contain non-alphanumeric symbols', () {
2330 const String nonAlphaNumericArbFile = '''
2331{
2332 "title!!": "Stocks",
2333 "@title!!": {
2334 "description": "Title for the Stocks application"
2335 }
2336}''';
2337 expect(
2338 () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }),
2339 throwsA(isA<L10nException>().having(
2340 (L10nException e) => e.message,
2341 'message',
2342 contains('Invalid ARB resource name'),
2343 )),
2344 );
2345 });
2346
2347 testWithoutContext('must start with lowercase character', () {
2348 const String nonAlphaNumericArbFile = '''
2349{
2350 "Title": "Stocks",
2351 "@Title": {
2352 "description": "Title for the Stocks application"
2353 }
2354}''';
2355 expect(
2356 () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }),
2357 throwsA(isA<L10nException>().having(
2358 (L10nException e) => e.message,
2359 'message',
2360 contains('Invalid ARB resource name'),
2361 )),
2362 );
2363 });
2364
2365 testWithoutContext('cannot start with a number', () {
2366 const String nonAlphaNumericArbFile = '''
2367{
2368 "123title": "Stocks",
2369 "@123title": {
2370 "description": "Title for the Stocks application"
2371 }
2372}''';
2373 expect(
2374 () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }),
2375 throwsA(isA<L10nException>().having(
2376 (L10nException e) => e.message,
2377 'message',
2378 contains('Invalid ARB resource name'),
2379 )),
2380 );
2381 });
2382
2383 testWithoutContext('can start with and contain a dollar sign', () {
2384 const String dollarArbFile = r'''
2385{
2386 "$title$": "Stocks",
2387 "@$title$": {
2388 "description": "Title for the application"
2389 }
2390}''';
2391 setupLocalizations(<String, String>{ 'en': dollarArbFile });
2392 });
2393 });
2394
2395 testWithoutContext('throws when the language code is not supported', () {
2396 const String arbFileWithInvalidCode = '''
2397{
2398 "@@locale": "invalid",
2399 "title": "invalid"
2400}''';
2401
2402 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
2403 ..createSync(recursive: true);
2404 l10nDirectory.childFile('app_invalid.arb')
2405 .writeAsStringSync(arbFileWithInvalidCode);
2406
2407 expect(
2408 () {
2409 LocalizationsGenerator(
2410 fileSystem: fs,
2411 inputPathString: defaultL10nPathString,
2412 outputPathString: defaultL10nPathString,
2413 templateArbFileName: 'app_invalid.arb',
2414 outputFileString: defaultOutputFileString,
2415 classNameString: defaultClassNameString,
2416 logger: logger,
2417 )
2418 ..loadResources()
2419 ..writeOutputFiles();
2420 },
2421 throwsA(isA<L10nException>().having(
2422 (L10nException e) => e.message,
2423 'message',
2424 contains('"invalid" is not a supported language code.'),
2425 )),
2426 );
2427 });
2428 });
2429
2430 testWithoutContext('should generate a valid pubspec.yaml file when using synthetic package if it does not already exist', () {
2431 setupLocalizations(<String, String>{
2432 'en': singleMessageArbFileString,
2433 });
2434 final Directory outputDirectory = fs.directory(syntheticPackagePath);
2435 final File pubspecFile = outputDirectory.childFile('pubspec.yaml');
2436 expect(pubspecFile.existsSync(), isTrue);
2437
2438 final YamlNode yamlNode = loadYamlNode(pubspecFile.readAsStringSync());
2439 expect(yamlNode, isA<YamlMap>());
2440
2441 final YamlMap yamlMap = yamlNode as YamlMap;
2442 final String pubspecName = yamlMap['name'] as String;
2443 final String pubspecDescription = yamlMap['description'] as String;
2444 expect(pubspecName, 'synthetic_package');
2445 expect(pubspecDescription, "The Flutter application's synthetic package.");
2446 });
2447
2448 testWithoutContext('should not overwrite existing pubspec.yaml file when using synthetic package', () {
2449 final File pubspecFile = fs.file(fs.path.join(syntheticPackagePath, 'pubspec.yaml'))
2450 ..createSync(recursive: true)
2451 ..writeAsStringSync('abcd');
2452 setupLocalizations(<String, String>{
2453 'en': singleMessageArbFileString,
2454 });
2455 // The original pubspec file should not be overwritten.
2456 expect(pubspecFile.readAsStringSync(), 'abcd');
2457 });
2458
2459 testWithoutContext('can use type: int without specifying a format', () {
2460 const String arbFile = '''
2461{
2462 "orderNumber": "This is order #{number}.",
2463 "@orderNumber": {
2464 "description": "The title for an order with a given number.",
2465 "placeholders": {
2466 "number": {
2467 "type": "int"
2468 }
2469 }
2470 }
2471}''';
2472 setupLocalizations(<String, String>{
2473 'en': arbFile,
2474 });
2475 expect(getGeneratedFileContent(locale: 'en'), containsIgnoringWhitespace(r'''
2476String orderNumber(int number) {
2477 return 'This is order #$number.';
2478}
2479'''));
2480 expect(getGeneratedFileContent(locale: 'en'), isNot(contains(intlImportDartCode)));
2481 });
2482
2483 testWithoutContext('app localizations lookup is a public method', () {
2484 setupLocalizations(<String, String>{ 'en': singleMessageArbFileString });
2485 expect(getGeneratedFileContent(), containsIgnoringWhitespace(r'''
2486AppLocalizations lookupAppLocalizations(Locale locale) {
2487'''));
2488 });
2489
2490 testWithoutContext('escaping with single quotes', () {
2491 const String arbFile = '''
2492{
2493 "singleQuote": "Flutter''s amazing!",
2494 "@singleQuote": {
2495 "description": "A message with a single quote."
2496 }
2497}''';
2498 setupLocalizations(<String, String>{ 'en': arbFile }, useEscaping: true);
2499 expect(getGeneratedFileContent(locale: 'en'), contains(r"Flutter\'s amazing"));
2500 });
2501
2502 testWithoutContext('suppress warnings flag actually suppresses warnings', () {
2503 const String pluralMessageWithOverriddenParts = '''
2504{
2505 "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
2506 "@helloWorlds": {
2507 "description": "Properly formatted but has redundant zero cases.",
2508 "placeholders": {
2509 "count": {}
2510 }
2511 }
2512}''';
2513 setupLocalizations(
2514 <String, String>{ 'en': pluralMessageWithOverriddenParts },
2515 suppressWarnings: true,
2516 );
2517 expect(logger.hadWarningOutput, isFalse);
2518 });
2519
2520 testWithoutContext('can use decimalPatternDigits with decimalDigits optional parameter', () {
2521 const String arbFile = '''
2522{
2523 "treeHeight": "Tree height is {height}m.",
2524 "@treeHeight": {
2525 "placeholders": {
2526 "height": {
2527 "type": "double",
2528 "format": "decimalPatternDigits",
2529 "optionalParameters": {
2530 "decimalDigits": 3
2531 }
2532 }
2533 }
2534 }
2535}''';
2536 setupLocalizations(<String, String>{ 'en': arbFile });
2537 final String localizationsFile = getGeneratedFileContent(locale: 'en');
2538 expect(localizationsFile, containsIgnoringWhitespace(r'''
2539String treeHeight(double height) {
2540'''));
2541 expect(localizationsFile, containsIgnoringWhitespace(r'''
2542NumberFormat.decimalPatternDigits(
2543 locale: localeName,
2544 decimalDigits: 3
2545);
2546'''));
2547 });
2548
2549 // Regression test for https://github.com/flutter/flutter/issues/125461.
2550 testWithoutContext('dollar signs are escaped properly when there is a select clause', () {
2551 const String dollarSignWithSelect = r'''
2552{
2553 "dollarSignWithSelect": "$nice_bug\nHello Bug! Manifestation #1 {selectPlaceholder, select, case{message} other{messageOther}}"
2554}''';
2555 setupLocalizations(<String, String>{ 'en': dollarSignWithSelect });
2556 expect(getGeneratedFileContent(locale: 'en'), contains(r'\$nice_bug\nHello Bug! Manifestation #1 $_temp0'));
2557 });
2558
2559 testWithoutContext('can generate method with named parameter', () {
2560 const String arbFile = '''
2561{
2562 "helloName": "Hello {name}!",
2563 "@helloName": {
2564 "description": "A more personal greeting",
2565 "placeholders": {
2566 "name": {
2567 "type": "String",
2568 "description": "The name of the person to greet"
2569 }
2570 }
2571 },
2572 "helloNameAndAge": "Hello {name}! You are {age} years old.",
2573 "@helloNameAndAge": {
2574 "description": "A more personal greeting",
2575 "placeholders": {
2576 "name": {
2577 "type": "String",
2578 "description": "The name of the person to greet"
2579 },
2580 "age": {
2581 "type": "int",
2582 "description": "The age of the person to greet"
2583 }
2584 }
2585 }
2586}
2587 ''';
2588 setupLocalizations(<String, String>{ 'en': arbFile }, useNamedParameters: true);
2589 final String localizationsFile = getGeneratedFileContent(locale: 'en');
2590 expect(localizationsFile, containsIgnoringWhitespace(r'''
2591String helloName({required String name}) {
2592 '''));
2593 expect(localizationsFile, containsIgnoringWhitespace(r'''
2594String helloNameAndAge({required String name, required int age}) {
2595 '''));
2596 });
2597}
2598

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com