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/features.dart';
11import 'package:flutter_tools/src/localizations/gen_l10n.dart';
12import 'package:flutter_tools/src/localizations/gen_l10n_types.dart';
13import 'package:flutter_tools/src/localizations/localizations_utils.dart';
14import 'package:yaml/yaml.dart';
15
16import '../src/common.dart';
17import '../src/context.dart';
18import '../src/fakes.dart';
19
20const String defaultTemplateArbFileName = 'app_en.arb';
21const String defaultOutputFileString = 'output-localization-file.dart';
22const String defaultClassNameString = 'AppLocalizations';
23const String singleMessageArbFileString = '''
24{
25 "title": "Title",
26 "@title": {
27 "description": "Title for the application."
28 }
29}''';
30const String twoMessageArbFileString = '''
31{
32 "title": "Title",
33 "@title": {
34 "description": "Title for the application."
35 },
36 "subtitle": "Subtitle",
37 "@subtitle": {
38 "description": "Subtitle for the application."
39 }
40}''';
41const String esArbFileName = 'app_es.arb';
42const String singleEsMessageArbFileString = '''
43{
44 "title": "Título"
45}''';
46const String singleZhMessageArbFileString = '''
47{
48 "title": "标题"
49}''';
50const String intlImportDartCode = '''
51import 'package:intl/intl.dart' as intl;
52''';
53const String foundationImportDartCode = '''
54import 'package:flutter/foundation.dart';
55''';
56
57void _standardFlutterDirectoryL10nSetup(FileSystem fs) {
58 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
59 ..createSync(recursive: true);
60 l10nDirectory.childFile(defaultTemplateArbFileName).writeAsStringSync(singleMessageArbFileString);
61 l10nDirectory.childFile(esArbFileName).writeAsStringSync(singleEsMessageArbFileString);
62 fs.file('pubspec.yaml')
63 ..createSync(recursive: true)
64 ..writeAsStringSync('''
65flutter:
66 generate: true
67''');
68}
69
70void main() {
71 // TODO(matanlurey): Remove after `explicit-package-dependencies` is enabled by default.
72 FeatureFlags enableExplicitPackageDependencies() {
73 return TestFeatureFlags(isExplicitPackageDependenciesEnabled: true);
74 }
75
76 late MemoryFileSystem fs;
77 late BufferLogger logger;
78 late Artifacts artifacts;
79 late String defaultL10nPathString;
80 late String syntheticPackagePath;
81 late String syntheticL10nPackagePath;
82
83 LocalizationsGenerator setupLocalizations(
84 Map<String, String> localeToArbFile, {
85 String? yamlFile,
86 String? outputPathString,
87 String? outputFileString,
88 String? headerString,
89 String? headerFile,
90 String? untranslatedMessagesFile,
91 bool useSyntheticPackage = true,
92 bool isFromYaml = false,
93 bool usesNullableGetter = true,
94 String? inputsAndOutputsListPath,
95 List<String>? preferredSupportedLocales,
96 bool useDeferredLoading = false,
97 bool useEscaping = false,
98 bool areResourceAttributeRequired = false,
99 bool suppressWarnings = false,
100 bool relaxSyntax = false,
101 bool useNamedParameters = false,
102 void Function(Directory)? setup,
103 }) {
104 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
105 ..createSync(recursive: true);
106 for (final String locale in localeToArbFile.keys) {
107 l10nDirectory.childFile('app_$locale.arb').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 getSyntheticGeneratedFileContent({String? locale}) {
139 final String fileName =
140 locale == null ? 'output-localization-file.dart' : 'output-localization-file_$locale.dart';
141 return fs.file(fs.path.join(syntheticL10nPackagePath, fileName)).readAsStringSync();
142 }
143
144 String getInPackageGeneratedFileContent({String? locale}) {
145 final String fileName =
146 locale == null ? 'output-localization-file.dart' : 'output-localization-file_$locale.dart';
147 return fs.file(fs.path.join(defaultL10nPathString, fileName)).readAsStringSync();
148 }
149
150 setUp(() {
151 fs = MemoryFileSystem.test();
152 logger = BufferLogger.test();
153 artifacts = Artifacts.test();
154
155 defaultL10nPathString = fs.path.join('lib', 'l10n');
156 syntheticPackagePath = fs.path.join('.dart_tool', 'flutter_gen');
157 syntheticL10nPackagePath = fs.path.join(syntheticPackagePath, 'gen_l10n');
158 precacheLanguageAndRegionTags();
159 });
160
161 group('Setters', () {
162 testWithoutContext('setInputDirectory fails if the directory does not exist', () {
163 expect(
164 () => LocalizationsGenerator.inputDirectoryFromPath(fs, 'lib', fs.directory('bogus')),
165 throwsA(
166 isA<L10nException>().having(
167 (L10nException e) => e.message,
168 'message',
169 contains('Make sure that the correct path was provided'),
170 ),
171 ),
172 );
173 });
174
175 testWithoutContext('setting className fails if input string is empty', () {
176 _standardFlutterDirectoryL10nSetup(fs);
177 expect(
178 () => LocalizationsGenerator.classNameFromString(''),
179 throwsA(
180 isA<L10nException>().having(
181 (L10nException e) => e.message,
182 'message',
183 contains('cannot be empty'),
184 ),
185 ),
186 );
187 });
188
189 testWithoutContext('sets absolute path of the target Flutter project', () {
190 // Set up project directory.
191 final Directory l10nDirectory = fs.currentDirectory
192 .childDirectory('absolute')
193 .childDirectory('path')
194 .childDirectory('to')
195 .childDirectory('flutter_project')
196 .childDirectory('lib')
197 .childDirectory('l10n')..createSync(recursive: true);
198 l10nDirectory
199 .childFile(defaultTemplateArbFileName)
200 .writeAsStringSync(singleMessageArbFileString);
201 l10nDirectory.childFile(esArbFileName).writeAsStringSync(singleEsMessageArbFileString);
202
203 // Run localizations generator in specified absolute path.
204 final String flutterProjectPath = fs.path.join('absolute', 'path', 'to', 'flutter_project');
205 LocalizationsGenerator(
206 fileSystem: fs,
207 projectPathString: flutterProjectPath,
208 inputPathString: defaultL10nPathString,
209 outputPathString: defaultL10nPathString,
210 templateArbFileName: defaultTemplateArbFileName,
211 outputFileString: defaultOutputFileString,
212 classNameString: defaultClassNameString,
213 logger: logger,
214 )
215 ..loadResources()
216 ..writeOutputFiles();
217
218 // Output files should be generated in the provided absolute path.
219 expect(
220 fs.isFileSync(
221 fs.path.join(
222 flutterProjectPath,
223 '.dart_tool',
224 'flutter_gen',
225 'gen_l10n',
226 'output-localization-file_en.dart',
227 ),
228 ),
229 true,
230 );
231 expect(
232 fs.isFileSync(
233 fs.path.join(
234 flutterProjectPath,
235 '.dart_tool',
236 'flutter_gen',
237 'gen_l10n',
238 'output-localization-file_es.dart',
239 ),
240 ),
241 true,
242 );
243 });
244
245 testWithoutContext('throws error when directory at absolute path does not exist', () {
246 // Set up project directory.
247 final Directory l10nDirectory = fs.currentDirectory
248 .childDirectory('lib')
249 .childDirectory('l10n')..createSync(recursive: true);
250 l10nDirectory
251 .childFile(defaultTemplateArbFileName)
252 .writeAsStringSync(singleMessageArbFileString);
253 l10nDirectory.childFile(esArbFileName).writeAsStringSync(singleEsMessageArbFileString);
254
255 // Project path should be intentionally a directory that does not exist.
256 expect(
257 () => LocalizationsGenerator(
258 fileSystem: fs,
259 projectPathString: 'absolute/path/to/flutter_project',
260 inputPathString: defaultL10nPathString,
261 outputPathString: defaultL10nPathString,
262 templateArbFileName: defaultTemplateArbFileName,
263 outputFileString: defaultOutputFileString,
264 classNameString: defaultClassNameString,
265 logger: logger,
266 ),
267 throwsA(
268 isA<L10nException>().having(
269 (L10nException e) => e.message,
270 'message',
271 contains('Directory does not exist'),
272 ),
273 ),
274 );
275 });
276
277 testWithoutContext('throws error when arb file does not exist', () {
278 // Set up project directory.
279 fs.currentDirectory.childDirectory('lib').childDirectory('l10n').createSync(recursive: true);
280
281 // Arb file should be nonexistent in the l10n directory.
282 expect(
283 () => LocalizationsGenerator(
284 fileSystem: fs,
285 projectPathString: './',
286 inputPathString: defaultL10nPathString,
287 outputPathString: defaultL10nPathString,
288 templateArbFileName: defaultTemplateArbFileName,
289 outputFileString: defaultOutputFileString,
290 classNameString: defaultClassNameString,
291 logger: logger,
292 ),
293 throwsA(
294 isA<L10nException>().having(
295 (L10nException e) => e.message,
296 'message',
297 contains(', does not exist.'),
298 ),
299 ),
300 );
301 });
302
303 group('className should only take valid Dart class names', () {
304 setUp(() {
305 _standardFlutterDirectoryL10nSetup(fs);
306 });
307
308 testWithoutContext('fails on string with spaces', () {
309 expect(
310 () => LocalizationsGenerator.classNameFromString('String with spaces'),
311 throwsA(
312 isA<L10nException>().having(
313 (L10nException e) => e.message,
314 'message',
315 contains('is not a valid public Dart class name'),
316 ),
317 ),
318 );
319 });
320
321 testWithoutContext('fails on non-alphanumeric symbols', () {
322 expect(
323 () => LocalizationsGenerator.classNameFromString('TestClass@123'),
324 throwsA(
325 isA<L10nException>().having(
326 (L10nException e) => e.message,
327 'message',
328 contains('is not a valid public Dart class name'),
329 ),
330 ),
331 );
332 });
333
334 testWithoutContext('fails on camel-case', () {
335 expect(
336 () => LocalizationsGenerator.classNameFromString('camelCaseClassName'),
337 throwsA(
338 isA<L10nException>().having(
339 (L10nException e) => e.message,
340 'message',
341 contains('is not a valid public Dart class name'),
342 ),
343 ),
344 );
345 });
346
347 testWithoutContext('fails when starting with a number', () {
348 expect(
349 () => LocalizationsGenerator.classNameFromString('123ClassName'),
350 throwsA(
351 isA<L10nException>().having(
352 (L10nException e) => e.message,
353 'message',
354 contains('is not a valid public Dart class name'),
355 ),
356 ),
357 );
358 });
359 });
360 });
361
362 testWithoutContext('correctly adds a headerString when it is set', () {
363 final LocalizationsGenerator generator = setupLocalizations(<String, String>{
364 'en': singleMessageArbFileString,
365 'es': singleEsMessageArbFileString,
366 }, headerString: '/// Sample header');
367 expect(generator.header, '/// Sample header');
368 });
369
370 testWithoutContext('correctly adds a headerFile when it is set', () {
371 final LocalizationsGenerator generator = setupLocalizations(
372 <String, String>{'en': singleMessageArbFileString, 'es': singleEsMessageArbFileString},
373 headerFile: 'header.txt',
374 setup: (Directory l10nDirectory) {
375 l10nDirectory.childFile('header.txt').writeAsStringSync('/// Sample header in a text file');
376 },
377 );
378 expect(generator.header, '/// Sample header in a text file');
379 });
380
381 testWithoutContext('sets templateArbFileName with more than one underscore correctly', () {
382 setupLocalizations(<String, String>{
383 'en': singleMessageArbFileString,
384 'es': singleEsMessageArbFileString,
385 });
386 final Directory outputDirectory = fs.directory(syntheticL10nPackagePath);
387 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
388 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
389 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
390 });
391
392 testWithoutContext('filenames with invalid locales should not be recognized', () {
393 expect(
394 () {
395 // This attempts to create 'app_localizations_en_CA_foo.arb'.
396 setupLocalizations(<String, String>{
397 'en': singleMessageArbFileString,
398 'en_CA_foo': singleMessageArbFileString,
399 });
400 },
401 throwsA(
402 isA<L10nException>().having(
403 (L10nException e) => e.message,
404 'message',
405 contains("The following .arb file's locale could not be determined"),
406 ),
407 ),
408 );
409 });
410
411 testWithoutContext(
412 'correctly creates an untranslated messages file (useSyntheticPackage = true)',
413 () {
414 final String untranslatedMessagesFilePath = fs.path.join(
415 'lib',
416 'l10n',
417 'unimplemented_message_translations.json',
418 );
419 setupLocalizations(<String, String>{
420 'en': twoMessageArbFileString,
421 'es': singleEsMessageArbFileString,
422 }, untranslatedMessagesFile: untranslatedMessagesFilePath);
423 final String unimplementedOutputString =
424 fs.file(untranslatedMessagesFilePath).readAsStringSync();
425 try {
426 // Since ARB file is essentially JSON, decoding it should not fail.
427 json.decode(unimplementedOutputString);
428 } on Exception {
429 fail('Parsing arb file should not fail');
430 }
431 expect(unimplementedOutputString, contains('es'));
432 expect(unimplementedOutputString, contains('subtitle'));
433 },
434 );
435
436 testWithoutContext(
437 'correctly creates an untranslated messages file (useSyntheticPackage = false)',
438 () {
439 final String untranslatedMessagesFilePath = fs.path.join(
440 'lib',
441 'l10n',
442 'unimplemented_message_translations.json',
443 );
444 setupLocalizations(
445 <String, String>{'en': twoMessageArbFileString, 'es': singleMessageArbFileString},
446 useSyntheticPackage: false,
447 untranslatedMessagesFile: untranslatedMessagesFilePath,
448 );
449 final String unimplementedOutputString =
450 fs.file(untranslatedMessagesFilePath).readAsStringSync();
451 try {
452 // Since ARB file is essentially JSON, decoding it should not fail.
453 json.decode(unimplementedOutputString);
454 } on Exception {
455 fail('Parsing arb file should not fail');
456 }
457 expect(unimplementedOutputString, contains('es'));
458 expect(unimplementedOutputString, contains('subtitle'));
459 },
460 );
461
462 testWithoutContext('untranslated messages suggestion is printed when translation is missing: '
463 'command line message', () {
464 setupLocalizations(<String, String>{
465 'en': twoMessageArbFileString,
466 'es': singleEsMessageArbFileString,
467 });
468 expect(
469 logger.statusText,
470 contains('To see a detailed report, use the --untranslated-messages-file'),
471 );
472 expect(
473 logger.statusText,
474 contains('flutter gen-l10n --untranslated-messages-file=desiredFileName.txt'),
475 );
476 });
477
478 testWithoutContext('untranslated messages suggestion is printed when translation is missing: '
479 'l10n.yaml message', () {
480 setupLocalizations(<String, String>{
481 'en': twoMessageArbFileString,
482 'es': singleEsMessageArbFileString,
483 }, isFromYaml: true);
484 expect(
485 logger.statusText,
486 contains('To see a detailed report, use the untranslated-messages-file'),
487 );
488 expect(logger.statusText, contains('untranslated-messages-file: desiredFileName.txt'));
489 });
490
491 testWithoutContext('unimplemented messages suggestion is not printed when all messages '
492 'are fully translated', () {
493 setupLocalizations(<String, String>{
494 'en': twoMessageArbFileString,
495 'es': twoMessageArbFileString,
496 });
497 expect(logger.statusText, equals(''));
498 });
499
500 testWithoutContext('untranslated messages file included in generated JSON list of outputs', () {
501 final String untranslatedMessagesFilePath = fs.path.join(
502 'lib',
503 'l10n',
504 'unimplemented_message_translations.json',
505 );
506 setupLocalizations(
507 <String, String>{'en': twoMessageArbFileString, 'es': singleEsMessageArbFileString},
508 untranslatedMessagesFile: untranslatedMessagesFilePath,
509 inputsAndOutputsListPath: syntheticL10nPackagePath,
510 );
511 final File inputsAndOutputsList = fs.file(
512 fs.path.join(syntheticL10nPackagePath, 'gen_l10n_inputs_and_outputs.json'),
513 );
514 expect(inputsAndOutputsList.existsSync(), isTrue);
515 final Map<String, dynamic> jsonResult =
516 json.decode(inputsAndOutputsList.readAsStringSync()) as Map<String, dynamic>;
517 expect(jsonResult.containsKey('outputs'), isTrue);
518 final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>;
519 expect(outputList, contains(contains('unimplemented_message_translations.json')));
520 });
521
522 testWithoutContext('uses inputPathString as outputPathString when the outputPathString is '
523 'null while not using the synthetic package option', () {
524 _standardFlutterDirectoryL10nSetup(fs);
525 LocalizationsGenerator(
526 fileSystem: fs,
527 inputPathString: defaultL10nPathString,
528 // outputPathString is intentionally not defined
529 templateArbFileName: defaultTemplateArbFileName,
530 outputFileString: defaultOutputFileString,
531 classNameString: defaultClassNameString,
532 useSyntheticPackage: false,
533 logger: logger,
534 )
535 ..loadResources()
536 ..writeOutputFiles();
537
538 final Directory outputDirectory = fs.directory('lib').childDirectory('l10n');
539 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
540 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
541 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
542 });
543
544 testWithoutContext('correctly generates output files in non-default output directory if it '
545 'already exists while not using the synthetic package option', () {
546 final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
547 ..createSync(recursive: true);
548 // Create the directory 'lib/l10n/output'.
549 l10nDirectory.childDirectory('output');
550
551 l10nDirectory
552 .childFile(defaultTemplateArbFileName)
553 .writeAsStringSync(singleMessageArbFileString);
554 l10nDirectory.childFile(esArbFileName).writeAsStringSync(singleEsMessageArbFileString);
555
556 LocalizationsGenerator(
557 fileSystem: fs,
558 inputPathString: defaultL10nPathString,
559 outputPathString: fs.path.join('lib', 'l10n', 'output'),
560 templateArbFileName: defaultTemplateArbFileName,
561 outputFileString: defaultOutputFileString,
562 classNameString: defaultClassNameString,
563 useSyntheticPackage: false,
564 logger: logger,
565 )
566 ..loadResources()
567 ..writeOutputFiles();
568
569 final Directory outputDirectory = fs
570 .directory('lib')
571 .childDirectory('l10n')
572 .childDirectory('output');
573 expect(outputDirectory.existsSync(), isTrue);
574 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
575 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
576 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
577 });
578
579 testWithoutContext('correctly creates output directory if it does not exist and writes files '
580 'in it while not using the synthetic package option', () {
581 _standardFlutterDirectoryL10nSetup(fs);
582
583 LocalizationsGenerator(
584 fileSystem: fs,
585 inputPathString: defaultL10nPathString,
586 outputPathString: fs.path.join('lib', 'l10n', 'output'),
587 templateArbFileName: defaultTemplateArbFileName,
588 outputFileString: defaultOutputFileString,
589 classNameString: defaultClassNameString,
590 useSyntheticPackage: false,
591 logger: logger,
592 )
593 ..loadResources()
594 ..writeOutputFiles();
595
596 final Directory outputDirectory = fs
597 .directory('lib')
598 .childDirectory('l10n')
599 .childDirectory('output');
600 expect(outputDirectory.existsSync(), isTrue);
601 expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
602 expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
603 expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
604 });
605
606 testWithoutContext('generates nullable localizations class getter via static `of` method '
607 'by default', () {
608 final LocalizationsGenerator generator = setupLocalizations(<String, String>{
609 'en': singleMessageArbFileString,
610 'es': singleEsMessageArbFileString,
611 });
612 expect(generator.outputDirectory.existsSync(), isTrue);
613 expect(
614 generator.outputDirectory.childFile('output-localization-file.dart').existsSync(),
615 isTrue,
616 );
617 expect(
618 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
619 contains('static AppLocalizations? of(BuildContext context)'),
620 );
621 expect(
622 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
623 contains('return Localizations.of<AppLocalizations>(context, AppLocalizations);'),
624 );
625 });
626
627 testWithoutContext(
628 'can generate non-nullable localizations class getter via static `of` method ',
629 () {
630 final LocalizationsGenerator generator = setupLocalizations(<String, String>{
631 'en': singleMessageArbFileString,
632 'es': singleEsMessageArbFileString,
633 }, usesNullableGetter: false);
634 expect(generator.outputDirectory.existsSync(), isTrue);
635 expect(
636 generator.outputDirectory.childFile('output-localization-file.dart').existsSync(),
637 isTrue,
638 );
639 expect(
640 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
641 contains('static AppLocalizations of(BuildContext context)'),
642 );
643 expect(
644 generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
645 contains('return Localizations.of<AppLocalizations>(context, AppLocalizations)!;'),
646 );
647 },
648 );
649
650 testWithoutContext('creates list of inputs and outputs when file path is specified', () {
651 setupLocalizations(<String, String>{
652 'en': singleMessageArbFileString,
653 'es': singleEsMessageArbFileString,
654 }, inputsAndOutputsListPath: syntheticL10nPackagePath);
655 final File inputsAndOutputsList = fs.file(
656 fs.path.join(syntheticL10nPackagePath, 'gen_l10n_inputs_and_outputs.json'),
657 );
658 expect(inputsAndOutputsList.existsSync(), isTrue);
659
660 final Map<String, dynamic> jsonResult =
661 json.decode(inputsAndOutputsList.readAsStringSync()) as Map<String, dynamic>;
662 expect(jsonResult.containsKey('inputs'), isTrue);
663 final List<dynamic> inputList = jsonResult['inputs'] as List<dynamic>;
664 expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_en.arb')));
665 expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_es.arb')));
666
667 expect(jsonResult.containsKey('outputs'), isTrue);
668 final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>;
669 expect(
670 outputList,
671 contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file.dart')),
672 );
673 expect(
674 outputList,
675 contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_en.dart')),
676 );
677 expect(
678 outputList,
679 contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_es.dart')),
680 );
681 });
682
683 testWithoutContext('setting both a headerString and a headerFile should fail', () {
684 expect(
685 () {
686 setupLocalizations(
687 <String, String>{'en': singleMessageArbFileString, 'es': singleEsMessageArbFileString},
688 headerString: '/// Sample header in a text file',
689 headerFile: 'header.txt',
690 setup: (Directory l10nDirectory) {
691 l10nDirectory
692 .childFile('header.txt')
693 .writeAsStringSync('/// Sample header in a text file');
694 },
695 );
696 },
697 throwsA(
698 isA<L10nException>().having(
699 (L10nException e) => e.message,
700 'message',
701 contains('Cannot accept both header and header file arguments'),
702 ),
703 ),
704 );
705 });
706
707 testWithoutContext('setting a headerFile that does not exist should fail', () {
708 expect(
709 () {
710 setupLocalizations(<String, String>{
711 'en': singleMessageArbFileString,
712 'es': singleEsMessageArbFileString,
713 }, headerFile: 'header.txt');
714 },
715 throwsA(
716 isA<L10nException>().having(
717 (L10nException e) => e.message,
718 'message',
719 contains('Failed to read header file'),
720 ),
721 ),
722 );
723 });
724
725 group('generateLocalizations', () {
726 testWithoutContext('works even if CWD does not have a pubspec.yaml', () async {
727 final Directory projectDir = fs.currentDirectory.childDirectory('project')
728 ..createSync(recursive: true);
729 final Directory l10nDirectory = projectDir.childDirectory('lib').childDirectory('l10n')
730 ..createSync(recursive: true);
731 l10nDirectory
732 .childFile(defaultTemplateArbFileName)
733 .writeAsStringSync(singleMessageArbFileString);
734 l10nDirectory.childFile(esArbFileName).writeAsStringSync(singleEsMessageArbFileString);
735 projectDir.childFile('pubspec.yaml')
736 ..createSync(recursive: true)
737 ..writeAsStringSync('''
738flutter:
739 generate: true
740''');
741
742 final Logger logger = BufferLogger.test();
743 logger.printError('An error output from a different tool in flutter_tools');
744
745 // Should run without error.
746 await generateLocalizations(
747 fileSystem: fs,
748 options: LocalizationOptions(
749 arbDir: Uri.directory(defaultL10nPathString).path,
750 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
751 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
752 syntheticPackage: false,
753 ),
754 logger: logger,
755 projectDir: projectDir,
756 dependenciesDir: fs.currentDirectory,
757 artifacts: artifacts,
758 processManager: FakeProcessManager.any(),
759 );
760 });
761
762 testWithoutContext('other logs from flutter_tools does not affect gen-l10n', () async {
763 _standardFlutterDirectoryL10nSetup(fs);
764
765 final Logger logger = BufferLogger.test();
766 logger.printError('An error output from a different tool in flutter_tools');
767
768 // Should run without error.
769 await generateLocalizations(
770 fileSystem: fs,
771 options: LocalizationOptions(
772 arbDir: Uri.directory(defaultL10nPathString).path,
773 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
774 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
775 syntheticPackage: false,
776 ),
777 logger: logger,
778 projectDir: fs.currentDirectory,
779 dependenciesDir: fs.currentDirectory,
780 artifacts: artifacts,
781 processManager: FakeProcessManager.any(),
782 );
783 });
784
785 testWithoutContext('forwards arguments correctly', () async {
786 _standardFlutterDirectoryL10nSetup(fs);
787 final LocalizationOptions options = LocalizationOptions(
788 header: 'HEADER',
789 arbDir: Uri.directory(defaultL10nPathString).path,
790 useDeferredLoading: true,
791 outputClass: 'Foo',
792 outputLocalizationFile: Uri.file('bar.dart', windows: false).path,
793 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
794 preferredSupportedLocales: <String>['es'],
795 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
796 untranslatedMessagesFile: Uri.file('untranslated', windows: false).path,
797 syntheticPackage: false,
798 requiredResourceAttributes: true,
799 nullableGetter: false,
800 );
801
802 // Verify that values are correctly passed through the localizations target.
803 final LocalizationsGenerator generator = await generateLocalizations(
804 fileSystem: fs,
805 options: options,
806 logger: logger,
807 projectDir: fs.currentDirectory,
808 dependenciesDir: fs.currentDirectory,
809 artifacts: artifacts,
810 processManager: FakeProcessManager.any(),
811 );
812
813 expect(generator.inputDirectory.path, '/lib/l10n/');
814 expect(generator.outputDirectory.path, '/lib/l10n/');
815 expect(generator.templateArbFile.path, '/lib/l10n/app_en.arb');
816 expect(generator.baseOutputFile.path, '/lib/l10n/bar.dart');
817 expect(generator.className, 'Foo');
818 expect(generator.preferredSupportedLocales.single, LocaleInfo.fromString('es'));
819 expect(generator.header, 'HEADER');
820 expect(generator.useDeferredLoading, isTrue);
821 expect(generator.inputsAndOutputsListFile?.path, '/gen_l10n_inputs_and_outputs.json');
822 expect(generator.useSyntheticPackage, isFalse);
823 expect(generator.projectDirectory?.path, '/');
824 expect(generator.areResourceAttributesRequired, isTrue);
825 expect(generator.untranslatedMessagesFile?.path, 'untranslated');
826 expect(generator.usesNullableGetter, isFalse);
827
828 // Just validate one file.
829 expect(fs.file('/lib/l10n/bar_en.dart').readAsStringSync(), '''
830HEADER
831
832// ignore: unused_import
833import 'package:intl/intl.dart' as intl;
834import 'bar.dart';
835
836// ignore_for_file: type=lint
837
838/// The translations for English (`en`).
839class FooEn extends Foo {
840 FooEn([String locale = 'en']) : super(locale);
841
842 @override
843 String get title => 'Title';
844}
845''');
846 });
847
848 testUsingContext(
849 'throws exception on missing flutter: generate: true flag',
850 () async {
851 _standardFlutterDirectoryL10nSetup(fs);
852
853 // Missing flutter: generate: true should throw exception.
854 fs.file('pubspec.yaml')
855 ..createSync(recursive: true)
856 ..writeAsStringSync('''
857flutter:
858 uses-material-design: true
859''');
860
861 final LocalizationOptions options = LocalizationOptions(
862 header: 'HEADER',
863 headerFile: Uri.file('header', windows: false).path,
864 arbDir: Uri.file('arb', windows: false).path,
865 useDeferredLoading: true,
866 outputClass: 'Foo',
867 outputLocalizationFile: Uri.file('bar', windows: false).path,
868 preferredSupportedLocales: <String>['en_US'],
869 templateArbFile: Uri.file('example.arb', windows: false).path,
870 untranslatedMessagesFile: Uri.file('untranslated', windows: false).path,
871 );
872
873 expect(
874 () => generateLocalizations(
875 fileSystem: fs,
876 options: options,
877 logger: BufferLogger.test(),
878 projectDir: fs.currentDirectory,
879 dependenciesDir: fs.currentDirectory,
880 artifacts: artifacts,
881 processManager: FakeProcessManager.any(),
882 ),
883 throwsToolExit(
884 message:
885 'Attempted to generate localizations code without having the '
886 'flutter: generate flag turned on.',
887 ),
888 );
889 },
890 overrides: <Type, Generator>{FeatureFlags: enableExplicitPackageDependencies},
891 );
892
893 testUsingContext(
894 'uses the same line terminator as pubspec.yaml',
895 () async {
896 _standardFlutterDirectoryL10nSetup(fs);
897
898 fs.file('pubspec.yaml')
899 ..createSync(recursive: true)
900 ..writeAsStringSync('''
901flutter:\r
902 generate: true\r
903''');
904
905 final LocalizationOptions options = LocalizationOptions(
906 arbDir: fs.path.join('lib', 'l10n'),
907 outputClass: defaultClassNameString,
908 outputLocalizationFile: defaultOutputFileString,
909 );
910 await generateLocalizations(
911 fileSystem: fs,
912 options: options,
913 logger: BufferLogger.test(),
914 projectDir: fs.currentDirectory,
915 dependenciesDir: fs.currentDirectory,
916 artifacts: artifacts,
917 processManager: FakeProcessManager.any(),
918 );
919 final String content = getInPackageGeneratedFileContent(locale: 'en');
920 expect(content, contains('\r\n'));
921 },
922 overrides: <Type, Generator>{FeatureFlags: enableExplicitPackageDependencies},
923 );
924
925 testWithoutContext('blank lines generated nicely', () async {
926 _standardFlutterDirectoryL10nSetup(fs);
927
928 // Test without headers.
929 await generateLocalizations(
930 fileSystem: fs,
931 options: LocalizationOptions(
932 arbDir: Uri.directory(defaultL10nPathString).path,
933 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
934 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
935 syntheticPackage: false,
936 ),
937 logger: BufferLogger.test(),
938 projectDir: fs.currentDirectory,
939 dependenciesDir: fs.currentDirectory,
940 artifacts: artifacts,
941 processManager: FakeProcessManager.any(),
942 );
943
944 expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
945// ignore: unused_import
946import 'package:intl/intl.dart' as intl;
947import 'app_localizations.dart';
948
949// ignore_for_file: type=lint
950
951/// The translations for English (`en`).
952class AppLocalizationsEn extends AppLocalizations {
953 AppLocalizationsEn([String locale = 'en']) : super(locale);
954
955 @override
956 String get title => 'Title';
957}
958''');
959
960 // Test with headers.
961 await generateLocalizations(
962 fileSystem: fs,
963 options: LocalizationOptions(
964 header: 'HEADER',
965 arbDir: Uri.directory(defaultL10nPathString).path,
966 outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
967 templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
968 syntheticPackage: false,
969 ),
970 logger: logger,
971 projectDir: fs.currentDirectory,
972 dependenciesDir: fs.currentDirectory,
973 artifacts: artifacts,
974 processManager: FakeProcessManager.any(),
975 );
976
977 expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
978HEADER
979
980// ignore: unused_import
981import 'package:intl/intl.dart' as intl;
982import 'app_localizations.dart';
983
984// ignore_for_file: type=lint
985
986/// The translations for English (`en`).
987class AppLocalizationsEn extends AppLocalizations {
988 AppLocalizationsEn([String locale = 'en']) : super(locale);
989
990 @override
991 String get title => 'Title';
992}
993''');
994 });
995 });
996
997 group('loadResources', () {
998 testWithoutContext(
999 'correctly initializes supportedLocales and supportedLanguageCodes properties',
1000 () {
1001 _standardFlutterDirectoryL10nSetup(fs);
1002
1003 final LocalizationsGenerator generator = LocalizationsGenerator(
1004 fileSystem: fs,
1005 inputPathString: defaultL10nPathString,
1006 outputPathString: defaultL10nPathString,
1007 templateArbFileName: defaultTemplateArbFileName,
1008 outputFileString: defaultOutputFileString,
1009 classNameString: defaultClassNameString,
1010 logger: logger,
1011 )..loadResources();
1012
1013 expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true);
1014 expect(generator.supportedLocales.contains(LocaleInfo.fromString('es')), true);
1015 },
1016 );
1017
1018 testWithoutContext(
1019 'correctly sorts supportedLocales and supportedLanguageCodes alphabetically',
1020 () {
1021 final Directory l10nDirectory = fs.currentDirectory
1022 .childDirectory('lib')
1023 .childDirectory('l10n')..createSync(recursive: true);
1024 // Write files in non-alphabetical order so that read performs in that order
1025 l10nDirectory.childFile('app_zh.arb').writeAsStringSync(singleZhMessageArbFileString);
1026 l10nDirectory.childFile('app_es.arb').writeAsStringSync(singleEsMessageArbFileString);
1027 l10nDirectory.childFile('app_en.arb').writeAsStringSync(singleMessageArbFileString);
1028
1029 final LocalizationsGenerator generator = LocalizationsGenerator(
1030 fileSystem: fs,
1031 inputPathString: defaultL10nPathString,
1032 outputPathString: defaultL10nPathString,
1033 templateArbFileName: defaultTemplateArbFileName,
1034 outputFileString: defaultOutputFileString,
1035 classNameString: defaultClassNameString,
1036 logger: logger,
1037 )..loadResources();
1038
1039 expect(generator.supportedLocales.first, LocaleInfo.fromString('en'));
1040 expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es'));
1041 expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('zh'));
1042 },
1043 );
1044
1045 testWithoutContext(
1046 'adds preferred locales to the top of supportedLocales and supportedLanguageCodes',
1047 () {
1048 final Directory l10nDirectory = fs.currentDirectory
1049 .childDirectory('lib')
1050 .childDirectory('l10n')..createSync(recursive: true);
1051 l10nDirectory.childFile('app_en.arb').writeAsStringSync(singleMessageArbFileString);
1052 l10nDirectory.childFile('app_es.arb').writeAsStringSync(singleEsMessageArbFileString);
1053 l10nDirectory.childFile('app_zh.arb').writeAsStringSync(singleZhMessageArbFileString);
1054
1055 const List<String> preferredSupportedLocale = <String>['zh', 'es'];
1056 final LocalizationsGenerator generator = LocalizationsGenerator(
1057 fileSystem: fs,
1058 inputPathString: defaultL10nPathString,
1059 outputPathString: defaultL10nPathString,
1060 templateArbFileName: defaultTemplateArbFileName,
1061 outputFileString: defaultOutputFileString,
1062 classNameString: defaultClassNameString,
1063 preferredSupportedLocales: preferredSupportedLocale,
1064 logger: logger,
1065 )..loadResources();
1066
1067 expect(generator.supportedLocales.first, LocaleInfo.fromString('zh'));
1068 expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es'));
1069 expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('en'));
1070 },
1071 );
1072
1073 testWithoutContext(
1074 'throws an error attempting to add preferred locales when there is no corresponding arb file for that locale',
1075 () {
1076 final Directory l10nDirectory = fs.currentDirectory
1077 .childDirectory('lib')
1078 .childDirectory('l10n')..createSync(recursive: true);
1079 l10nDirectory.childFile('app_en.arb').writeAsStringSync(singleMessageArbFileString);
1080 l10nDirectory.childFile('app_es.arb').writeAsStringSync(singleEsMessageArbFileString);
1081 l10nDirectory.childFile('app_zh.arb').writeAsStringSync(singleZhMessageArbFileString);
1082
1083 const List<String> preferredSupportedLocale = <String>['am', 'es'];
1084 expect(
1085 () {
1086 LocalizationsGenerator(
1087 fileSystem: fs,
1088 inputPathString: defaultL10nPathString,
1089 outputPathString: defaultL10nPathString,
1090 templateArbFileName: defaultTemplateArbFileName,
1091 outputFileString: defaultOutputFileString,
1092 classNameString: defaultClassNameString,
1093 preferredSupportedLocales: preferredSupportedLocale,
1094 logger: logger,
1095 ).loadResources();
1096 },
1097 throwsA(
1098 isA<L10nException>().having(
1099 (L10nException e) => e.message,
1100 'message',
1101 contains("The preferred supported locale, 'am', cannot be added."),
1102 ),
1103 ),
1104 );
1105 },
1106 );
1107
1108 testWithoutContext('correctly sorts arbPathString alphabetically', () {
1109 final Directory l10nDirectory = fs.currentDirectory
1110 .childDirectory('lib')
1111 .childDirectory('l10n')..createSync(recursive: true);
1112 // Write files in non-alphabetical order so that read performs in that order
1113 l10nDirectory.childFile('app_zh.arb').writeAsStringSync(singleZhMessageArbFileString);
1114 l10nDirectory.childFile('app_es.arb').writeAsStringSync(singleEsMessageArbFileString);
1115 l10nDirectory.childFile('app_en.arb').writeAsStringSync(singleMessageArbFileString);
1116
1117 final LocalizationsGenerator generator = LocalizationsGenerator(
1118 fileSystem: fs,
1119 inputPathString: defaultL10nPathString,
1120 outputPathString: defaultL10nPathString,
1121 templateArbFileName: defaultTemplateArbFileName,
1122 outputFileString: defaultOutputFileString,
1123 classNameString: defaultClassNameString,
1124 logger: logger,
1125 )..loadResources();
1126
1127 expect(generator.arbPathStrings.first, fs.path.join('lib', 'l10n', 'app_en.arb'));
1128 expect(generator.arbPathStrings.elementAt(1), fs.path.join('lib', 'l10n', 'app_es.arb'));
1129 expect(generator.arbPathStrings.elementAt(2), fs.path.join('lib', 'l10n', 'app_zh.arb'));
1130 });
1131
1132 testWithoutContext('correctly parses @@locale property in arb file', () {
1133 const String arbFileWithEnLocale = '''
1134{
1135 "@@locale": "en",
1136 "title": "Title",
1137 "@title": {
1138 "description": "Title for the application"
1139 }
1140}''';
1141
1142 const String arbFileWithZhLocale = '''
1143{
1144 "@@locale": "zh",
1145 "title": "标题",
1146 "@title": {
1147 "description": "Title for the application"
1148 }
1149}''';
1150
1151 final Directory l10nDirectory = fs.currentDirectory
1152 .childDirectory('lib')
1153 .childDirectory('l10n')..createSync(recursive: true);
1154 l10nDirectory.childFile('first_file.arb').writeAsStringSync(arbFileWithEnLocale);
1155 l10nDirectory.childFile('second_file.arb').writeAsStringSync(arbFileWithZhLocale);
1156
1157 final LocalizationsGenerator generator = LocalizationsGenerator(
1158 fileSystem: fs,
1159 inputPathString: defaultL10nPathString,
1160 outputPathString: defaultL10nPathString,
1161 templateArbFileName: 'first_file.arb',
1162 outputFileString: defaultOutputFileString,
1163 classNameString: defaultClassNameString,
1164 logger: logger,
1165 )..loadResources();
1166
1167 expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true);
1168 expect(generator.supportedLocales.contains(LocaleInfo.fromString('zh')), true);
1169 });
1170
1171 testWithoutContext(
1172 'correctly requires @@locale property in arb file to match the filename locale suffix',
1173 () {
1174 const String arbFileWithEnLocale = '''
1175{
1176 "@@locale": "en",
1177 "title": "Stocks",
1178 "@title": {
1179 "description": "Title for the Stocks application"
1180 }
1181}''';
1182
1183 const String arbFileWithZhLocale = '''
1184{
1185 "@@locale": "zh",
1186 "title": "标题",
1187 "@title": {
1188 "description": "Title for the Stocks application"
1189 }
1190}''';
1191
1192 final Directory l10nDirectory = fs.currentDirectory
1193 .childDirectory('lib')
1194 .childDirectory('l10n')..createSync(recursive: true);
1195 l10nDirectory.childFile('app_es.arb').writeAsStringSync(arbFileWithEnLocale);
1196 l10nDirectory.childFile('app_am.arb').writeAsStringSync(arbFileWithZhLocale);
1197
1198 expect(
1199 () {
1200 LocalizationsGenerator(
1201 fileSystem: fs,
1202 inputPathString: defaultL10nPathString,
1203 outputPathString: defaultL10nPathString,
1204 templateArbFileName: 'app_es.arb',
1205 outputFileString: defaultOutputFileString,
1206 classNameString: defaultClassNameString,
1207 logger: logger,
1208 ).loadResources();
1209 },
1210 throwsA(
1211 isA<L10nException>().having(
1212 (L10nException e) => e.message,
1213 'message',
1214 contains('The locale specified in @@locale and the arb filename do not match.'),
1215 ),
1216 ),
1217 );
1218 },
1219 );
1220
1221 testWithoutContext("throws when arb file's locale could not be determined", () {
1222 fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
1223 ..createSync(recursive: true)
1224 ..childFile('app.arb').writeAsStringSync(singleMessageArbFileString);
1225 expect(
1226 () {
1227 LocalizationsGenerator(
1228 fileSystem: fs,
1229 inputPathString: defaultL10nPathString,
1230 outputPathString: defaultL10nPathString,
1231 templateArbFileName: 'app.arb',
1232 outputFileString: defaultOutputFileString,
1233 classNameString: defaultClassNameString,
1234 logger: logger,
1235 ).loadResources();
1236 },
1237 throwsA(
1238 isA<L10nException>().having(
1239 (L10nException e) => e.message,
1240 'message',
1241 contains('locale could not be determined'),
1242 ),
1243 ),
1244 );
1245 });
1246
1247 testWithoutContext('throws when an empty string is used as a key', () {
1248 const String arbFileStringWithEmptyResourceId = '''
1249{
1250 "market": "MARKET",
1251 "": {
1252 "description": "This key is invalid"
1253 }
1254}''';
1255
1256 final Directory l10nDirectory = fs.currentDirectory
1257 .childDirectory('lib')
1258 .childDirectory('l10n')..createSync(recursive: true);
1259 l10nDirectory.childFile('app_en.arb').writeAsStringSync(arbFileStringWithEmptyResourceId);
1260
1261 expect(
1262 () =>
1263 LocalizationsGenerator(
1264 fileSystem: fs,
1265 inputPathString: defaultL10nPathString,
1266 outputPathString: defaultL10nPathString,
1267 templateArbFileName: 'app_en.arb',
1268 outputFileString: defaultOutputFileString,
1269 classNameString: defaultClassNameString,
1270 logger: logger,
1271 ).loadResources(),
1272 throwsA(
1273 isA<L10nException>().having(
1274 (L10nException e) => e.message,
1275 'message',
1276 contains('Invalid ARB resource name ""'),
1277 ),
1278 ),
1279 );
1280 });
1281
1282 testWithoutContext('throws when the same locale is detected more than once', () {
1283 const String secondMessageArbFileString = '''
1284{
1285 "market": "MARKET",
1286 "@market": {
1287 "description": "Label for the Market tab"
1288 }
1289}''';
1290
1291 final Directory l10nDirectory = fs.currentDirectory
1292 .childDirectory('lib')
1293 .childDirectory('l10n')..createSync(recursive: true);
1294 l10nDirectory.childFile('app_en.arb').writeAsStringSync(singleMessageArbFileString);
1295 l10nDirectory.childFile('app2_en.arb').writeAsStringSync(secondMessageArbFileString);
1296
1297 expect(
1298 () {
1299 LocalizationsGenerator(
1300 fileSystem: fs,
1301 inputPathString: defaultL10nPathString,
1302 outputPathString: defaultL10nPathString,
1303 templateArbFileName: 'app_en.arb',
1304 outputFileString: defaultOutputFileString,
1305 classNameString: defaultClassNameString,
1306 logger: logger,
1307 ).loadResources();
1308 },
1309 throwsA(
1310 isA<L10nException>().having(
1311 (L10nException e) => e.message,
1312 'message',
1313 contains("Multiple arb files with the same 'en' locale detected"),
1314 ),
1315 ),
1316 );
1317 });
1318
1319 testWithoutContext('throws when the base locale does not exist', () {
1320 final Directory l10nDirectory = fs.currentDirectory
1321 .childDirectory('lib')
1322 .childDirectory('l10n')..createSync(recursive: true);
1323 l10nDirectory.childFile('app_en_US.arb').writeAsStringSync(singleMessageArbFileString);
1324
1325 expect(
1326 () {
1327 LocalizationsGenerator(
1328 fileSystem: fs,
1329 inputPathString: defaultL10nPathString,
1330 outputPathString: defaultL10nPathString,
1331 templateArbFileName: 'app_en_US.arb',
1332 outputFileString: defaultOutputFileString,
1333 classNameString: defaultClassNameString,
1334 logger: logger,
1335 ).loadResources();
1336 },
1337 throwsA(
1338 isA<L10nException>().having(
1339 (L10nException e) => e.message,
1340 'message',
1341 contains('Arb file for a fallback, en, does not exist'),
1342 ),
1343 ),
1344 );
1345 });
1346
1347 testWithoutContext('AppResourceBundle throws if file contains non-string value', () {
1348 const String inputPathString = 'lib/l10n';
1349 const String templateArbFileName = 'app_en.arb';
1350 const String outputFileString = 'app_localizations.dart';
1351 const String classNameString = 'AppLocalizations';
1352
1353 fs.file(fs.path.join(inputPathString, templateArbFileName))
1354 ..createSync(recursive: true)
1355 ..writeAsStringSync('{ "helloWorld": "Hello World!" }');
1356 fs.file(fs.path.join(inputPathString, 'app_es.arb'))
1357 ..createSync(recursive: true)
1358 ..writeAsStringSync('{ "helloWorld": {} }');
1359
1360 final LocalizationsGenerator generator = LocalizationsGenerator(
1361 fileSystem: fs,
1362 inputPathString: inputPathString,
1363 templateArbFileName: templateArbFileName,
1364 outputFileString: outputFileString,
1365 classNameString: classNameString,
1366 logger: logger,
1367 );
1368 expect(
1369 () => generator.loadResources(),
1370 throwsToolExit(
1371 message:
1372 'Localized message for key "helloWorld" in '
1373 '"lib/l10n/app_es.arb" is not a string.',
1374 ),
1375 );
1376 });
1377 });
1378
1379 group('writeOutputFiles', () {
1380 testWithoutContext('multiple messages with syntax error all log their errors', () {
1381 try {
1382 setupLocalizations(<String, String>{
1383 'en': r'''
1384{
1385 "msg1": "{",
1386 "msg2": "{ {"
1387}''',
1388 });
1389 } on L10nException catch (error) {
1390 expect(error.message, equals('Found syntax errors.'));
1391 expect(
1392 logger.errorText,
1393 contains('''
1394[app_en.arb:msg1] ICU Syntax Error: Expected "identifier" but found no tokens.
1395 {
1396 ^
1397[app_en.arb:msg2] ICU Syntax Error: Expected "identifier" but found "{".
1398 { {
1399 ^'''),
1400 );
1401 }
1402 });
1403
1404 testWithoutContext('no description generates generic comment', () {
1405 setupLocalizations(<String, String>{
1406 'en': r'''
1407{
1408 "helloWorld": "Hello world!"
1409}''',
1410 });
1411 expect(
1412 getSyntheticGeneratedFileContent(),
1413 contains('/// No description provided for @helloWorld.'),
1414 );
1415 });
1416
1417 testWithoutContext('multiline descriptions are correctly formatted as comments', () {
1418 setupLocalizations(<String, String>{
1419 'en': r'''
1420{
1421 "helloWorld": "Hello world!",
1422 "@helloWorld": {
1423 "description": "The generic example string in every language.\nUse this for tests!"
1424 }
1425}''',
1426 });
1427 expect(
1428 getSyntheticGeneratedFileContent(),
1429 contains('''
1430 /// The generic example string in every language.
1431 /// Use this for tests!'''),
1432 );
1433 });
1434
1435 testWithoutContext(
1436 'message without placeholders - should generate code comment with description and template message translation',
1437 () {
1438 setupLocalizations(<String, String>{
1439 'en': singleMessageArbFileString,
1440 'es': singleEsMessageArbFileString,
1441 });
1442 final String content = getSyntheticGeneratedFileContent();
1443 expect(content, contains('/// Title for the application.'));
1444 expect(
1445 content,
1446 contains('''
1447 /// In en, this message translates to:
1448 /// **'Title'**'''),
1449 );
1450 },
1451 );
1452
1453 testWithoutContext('template message translation handles newline characters', () {
1454 setupLocalizations(<String, String>{
1455 'en': r'''
1456{
1457 "title": "Title \n of the application",
1458 "@title": {
1459 "description": "Title for the application."
1460 }
1461}''',
1462 'es': singleEsMessageArbFileString,
1463 });
1464 final String content = getSyntheticGeneratedFileContent();
1465 expect(content, contains('/// Title for the application.'));
1466 expect(
1467 content,
1468 contains(r'''
1469 /// In en, this message translates to:
1470 /// **'Title \n of the application'**'''),
1471 );
1472 });
1473
1474 testWithoutContext(
1475 'message with placeholders - should generate code comment with description and template message translation',
1476 () {
1477 setupLocalizations(<String, String>{
1478 'en': r'''
1479{
1480 "price": "The price of this item is: ${price}",
1481 "@price": {
1482 "description": "The price of an online shopping cart item.",
1483 "placeholders": {
1484 "price": {
1485 "type": "double",
1486 "format": "decimalPattern"
1487 }
1488 }
1489 }
1490}''',
1491 'es': r'''
1492{
1493 "price": "El precio de este artículo es: ${price}"
1494}''',
1495 });
1496 final String content = getSyntheticGeneratedFileContent();
1497 expect(content, contains('/// The price of an online shopping cart item.'));
1498 expect(
1499 content,
1500 contains(r'''
1501 /// In en, this message translates to:
1502 /// **'The price of this item is: \${price}'**'''),
1503 );
1504 },
1505 );
1506
1507 testWithoutContext('should generate a file per language', () {
1508 setupLocalizations(<String, String>{
1509 'en': singleMessageArbFileString,
1510 'en_CA': '''
1511{
1512 "title": "Canadian Title"
1513}''',
1514 });
1515 expect(
1516 getSyntheticGeneratedFileContent(locale: 'en'),
1517 contains('class AppLocalizationsEn extends AppLocalizations'),
1518 );
1519 expect(
1520 getSyntheticGeneratedFileContent(locale: 'en'),
1521 contains('class AppLocalizationsEnCa extends AppLocalizationsEn'),
1522 );
1523 expect(() => getSyntheticGeneratedFileContent(locale: 'en_US'), throwsException);
1524 });
1525
1526 testWithoutContext(
1527 'language imports are sorted when preferredSupportedLocaleString is given',
1528 () {
1529 const List<String> preferredSupportedLocales = <String>['zh'];
1530 setupLocalizations(<String, String>{
1531 'en': singleMessageArbFileString,
1532 'zh': singleZhMessageArbFileString,
1533 'es': singleEsMessageArbFileString,
1534 }, preferredSupportedLocales: preferredSupportedLocales);
1535 final String content = getSyntheticGeneratedFileContent();
1536 expect(
1537 content,
1538 contains('''
1539import 'output-localization-file_en.dart';
1540import 'output-localization-file_es.dart';
1541import 'output-localization-file_zh.dart';
1542'''),
1543 );
1544 },
1545 );
1546
1547 // Regression test for https://github.com/flutter/flutter/issues/88356
1548 testWithoutContext('full output file suffix is retained', () {
1549 setupLocalizations(<String, String>{
1550 'en': singleMessageArbFileString,
1551 }, outputFileString: 'output-localization-file.g.dart');
1552 final String baseLocalizationsFile =
1553 fs
1554 .file(fs.path.join(syntheticL10nPackagePath, 'output-localization-file.g.dart'))
1555 .readAsStringSync();
1556 expect(
1557 baseLocalizationsFile,
1558 contains('''
1559import 'output-localization-file_en.g.dart';
1560'''),
1561 );
1562
1563 final String englishLocalizationsFile =
1564 fs
1565 .file(fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.g.dart'))
1566 .readAsStringSync();
1567 expect(
1568 englishLocalizationsFile,
1569 contains('''
1570import 'output-localization-file.g.dart';
1571'''),
1572 );
1573 });
1574
1575 testWithoutContext('throws an exception when invalid output file name is passed in', () {
1576 expect(
1577 () {
1578 setupLocalizations(<String, String>{
1579 'en': singleMessageArbFileString,
1580 }, outputFileString: 'asdf');
1581 },
1582 throwsA(
1583 isA<L10nException>().having(
1584 (L10nException e) => e.message,
1585 'message',
1586 allOf(
1587 contains('output-localization-file'),
1588 contains('asdf'),
1589 contains('is invalid'),
1590 contains('The file name must have a .dart extension.'),
1591 ),
1592 ),
1593 ),
1594 );
1595 expect(
1596 () {
1597 setupLocalizations(<String, String>{
1598 'en': singleMessageArbFileString,
1599 }, outputFileString: '.g.dart');
1600 },
1601 throwsA(
1602 isA<L10nException>().having(
1603 (L10nException e) => e.message,
1604 'message',
1605 allOf(
1606 contains('output-localization-file'),
1607 contains('.g.dart'),
1608 contains('is invalid'),
1609 contains('The base name cannot be empty.'),
1610 ),
1611 ),
1612 ),
1613 );
1614 });
1615
1616 testWithoutContext('imports are deferred and loaded when useDeferredImports are set', () {
1617 setupLocalizations(<String, String>{
1618 'en': singleMessageArbFileString,
1619 }, useDeferredLoading: true);
1620 final String content = getSyntheticGeneratedFileContent();
1621 expect(
1622 content,
1623 contains('''
1624import 'output-localization-file_en.dart' deferred as output-localization-file_en;
1625'''),
1626 );
1627 expect(content, contains('output-localization-file_en.loadLibrary()'));
1628 });
1629
1630 group('placeholder tests', () {
1631 testWithoutContext(
1632 'should automatically infer placeholders that are not explicitly defined',
1633 () {
1634 setupLocalizations(<String, String>{
1635 'en': '''
1636{
1637 "helloWorld": "Hello {name}"
1638}''',
1639 });
1640 final String content = getSyntheticGeneratedFileContent(locale: 'en');
1641 expect(content, contains('String helloWorld(Object name) {'));
1642 },
1643 );
1644
1645 testWithoutContext('placeholder parameter list should be consistent between languages', () {
1646 setupLocalizations(<String, String>{
1647 'en': '''
1648{
1649 "helloWorld": "Hello {name}",
1650 "@helloWorld": {
1651 "placeholders": {
1652 "name": {}
1653 }
1654 }
1655}''',
1656 'es': '''
1657{
1658 "helloWorld": "Hola"
1659}
1660''',
1661 });
1662 expect(
1663 getSyntheticGeneratedFileContent(locale: 'en'),
1664 contains('String helloWorld(Object name) {'),
1665 );
1666 expect(
1667 getSyntheticGeneratedFileContent(locale: 'es'),
1668 contains('String helloWorld(Object name) {'),
1669 );
1670 });
1671
1672 testWithoutContext(
1673 'braces are ignored as special characters if placeholder does not exist',
1674 () {
1675 setupLocalizations(<String, String>{
1676 'en': '''
1677{
1678 "helloWorld": "Hello {name}",
1679 "@@helloWorld": {
1680 "placeholders": {
1681 "names": {}
1682 }
1683 }
1684}''',
1685 }, relaxSyntax: true);
1686 final String content = getSyntheticGeneratedFileContent(locale: 'en');
1687 expect(content, contains("String get helloWorld => 'Hello {name}'"));
1688 },
1689 );
1690
1691 // Regression test for https://github.com/flutter/flutter/issues/163627
1692 //
1693 // If placeholders have no explicit type (like `int` or `String`) set
1694 // their type can be inferred.
1695 //
1696 // Later in the pipeline it is ensured that each locales placeholder types
1697 // matches the definitions in the template.
1698 //
1699 // If only the types of the template had been inferred,
1700 // and not for the translation there would be a mismatch:
1701 // in this case `num` for count and `null` (the default), which is incompatible
1702 // and `getSyntheticGeneratedFileContent` would throw an exception.
1703 //
1704 // This test ensures that both template and locale can be equally partially defined
1705 // in the arb.
1706 testWithoutContext(
1707 'translation placeholder type definitions can be inferred for plurals',
1708 () {
1709 setupLocalizations(<String, String>{
1710 'en': '''
1711{
1712 "helloWorld": "{count, plural, one{Hello World!} other{Hello Worlds!}}",
1713 "@helloWorld": {
1714 "description": "The conventional newborn programmer greeting",
1715 "placeholders": {
1716 "count": {}
1717 }
1718 }
1719}''',
1720 'de': '''
1721{
1722 "helloWorld": "{count, plural, one{Hallo Welt!} other{Hallo Welten!}}",
1723 "@helloWorld": {
1724 "description": "The conventional newborn programmer greeting",
1725 "placeholders": {
1726 "count": {}
1727 }
1728 }
1729}''',
1730 });
1731 expect(getSyntheticGeneratedFileContent(locale: 'en'), isA<String>());
1732 },
1733 );
1734 });
1735
1736 group('DateTime tests', () {
1737 testWithoutContext('imports package:intl', () {
1738 setupLocalizations(<String, String>{
1739 'en': '''
1740{
1741 "@@locale": "en",
1742 "springBegins": "Spring begins on {springStartDate}",
1743 "@springBegins": {
1744 "description": "The first day of spring",
1745 "placeholders": {
1746 "springStartDate": {
1747 "type": "DateTime",
1748 "format": "yMd"
1749 }
1750 }
1751 }
1752}''',
1753 });
1754 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
1755 });
1756
1757 testWithoutContext('throws an exception when improperly formatted date is passed in', () {
1758 expect(
1759 () {
1760 setupLocalizations(<String, String>{
1761 'en': '''
1762{
1763 "springBegins": "Spring begins on {springStartDate}",
1764 "@springBegins": {
1765 "placeholders": {
1766 "springStartDate": {
1767 "type": "DateTime",
1768 "format": "asdf"
1769 }
1770 }
1771 }
1772}''',
1773 });
1774 },
1775 throwsA(
1776 isA<L10nException>().having(
1777 (L10nException e) => e.message,
1778 'message',
1779 allOf(
1780 contains('message "springBegins"'),
1781 contains('locale "en"'),
1782 contains('asdf'),
1783 contains('springStartDate'),
1784 contains('does not have a corresponding DateFormat'),
1785 ),
1786 ),
1787 ),
1788 );
1789 });
1790
1791 testWithoutContext('use standard date format whenever possible', () {
1792 setupLocalizations(<String, String>{
1793 'en': '''
1794{
1795 "springBegins": "Spring begins on {springStartDate}",
1796 "@springBegins": {
1797 "placeholders": {
1798 "springStartDate": {
1799 "type": "DateTime",
1800 "format": "yMd",
1801 "isCustomDateFormat": "true"
1802 }
1803 }
1804 }
1805}''',
1806 });
1807 final String content = getSyntheticGeneratedFileContent(locale: 'en');
1808 expect(content, contains('DateFormat.yMd(localeName)'));
1809 });
1810
1811 testWithoutContext('handle arbitrary formatted date', () {
1812 setupLocalizations(<String, String>{
1813 'en': '''
1814{
1815 "@@locale": "en",
1816 "springBegins": "Spring begins on {springStartDate}",
1817 "@springBegins": {
1818 "description": "The first day of spring",
1819 "placeholders": {
1820 "springStartDate": {
1821 "type": "DateTime",
1822 "format": "asdf o'clock",
1823 "isCustomDateFormat": "true"
1824 }
1825 }
1826 }
1827}''',
1828 });
1829 final String content = getSyntheticGeneratedFileContent(locale: 'en');
1830 expect(content, contains(r"DateFormat('asdf o\'clock', localeName)"));
1831 });
1832
1833 testWithoutContext('handle arbitrary formatted date with actual boolean', () {
1834 setupLocalizations(<String, String>{
1835 'en': '''
1836{
1837 "@@locale": "en",
1838 "springBegins": "Spring begins on {springStartDate}",
1839 "@springBegins": {
1840 "description": "The first day of spring",
1841 "placeholders": {
1842 "springStartDate": {
1843 "type": "DateTime",
1844 "format": "asdf o'clock",
1845 "isCustomDateFormat": true
1846 }
1847 }
1848 }
1849}''',
1850 });
1851 final String content = getSyntheticGeneratedFileContent(locale: 'en');
1852 expect(content, contains(r"DateFormat('asdf o\'clock', localeName)"));
1853 });
1854
1855 testWithoutContext('handles adding two valid formats', () {
1856 setupLocalizations(<String, String>{
1857 'en': '''
1858{
1859 "loggedIn": "Last logged in on {lastLoginDate}",
1860 "@loggedIn": {
1861 "placeholders": {
1862 "lastLoginDate": {
1863 "type": "DateTime",
1864 "format": "yMd+jms"
1865 }
1866 }
1867 }
1868}''',
1869 });
1870 final String content = getSyntheticGeneratedFileContent(locale: 'en');
1871 expect(content, contains(r'DateFormat.yMd(localeName).add_jms()'));
1872 });
1873
1874 testWithoutContext('handles adding three valid formats', () {
1875 setupLocalizations(<String, String>{
1876 'en': '''
1877{
1878 "loggedIn": "Last logged in on {lastLoginDate}",
1879 "@loggedIn": {
1880 "placeholders": {
1881 "lastLoginDate": {
1882 "type": "DateTime",
1883 "format": "yMMMMEEEEd+QQQQ+Hm"
1884 }
1885 }
1886 }
1887}''',
1888 });
1889 final String content = getSyntheticGeneratedFileContent(locale: 'en');
1890 expect(content, contains(r'DateFormat.yMMMMEEEEd(localeName).add_QQQQ().add_Hm()'));
1891 });
1892
1893 testWithoutContext('throws an exception when adding invalid formats', () {
1894 expect(
1895 () {
1896 setupLocalizations(<String, String>{
1897 'en': '''
1898{
1899 "loggedIn": "Last logged in on {lastLoginDate}",
1900 "@loggedIn": {
1901 "placeholders": {
1902 "lastLoginDate": {
1903 "type": "DateTime",
1904 "format": "foo+bar+baz"
1905 }
1906 }
1907 }
1908}''',
1909 });
1910 },
1911 throwsA(
1912 isA<L10nException>().having(
1913 (L10nException e) => e.message,
1914 'message',
1915 allOf(
1916 contains('message "loggedIn"'),
1917 contains('locale "en"'),
1918 contains('"foo+bar+baz"'),
1919 contains('lastLoginDate'),
1920 contains('contains at least one invalid date format'),
1921 ),
1922 ),
1923 ),
1924 );
1925 });
1926
1927 testWithoutContext('throws an exception when adding formats and trailing plus sign', () {
1928 expect(
1929 () {
1930 setupLocalizations(<String, String>{
1931 'en': '''
1932{
1933 "loggedIn": "Last logged in on {lastLoginDate}",
1934 "@loggedIn": {
1935 "placeholders": {
1936 "lastLoginDate": {
1937 "type": "DateTime",
1938 "format": "yMd+Hm+"
1939 }
1940 }
1941 }
1942}''',
1943 });
1944 },
1945 throwsA(
1946 isA<L10nException>().having(
1947 (L10nException e) => e.message,
1948 'message',
1949 allOf(
1950 contains('message "loggedIn"'),
1951 contains('locale "en"'),
1952 contains('"yMd+Hm+"'),
1953 contains('lastLoginDate'),
1954 contains('contains at least one invalid date format'),
1955 ),
1956 ),
1957 ),
1958 );
1959 });
1960
1961 testWithoutContext('throws an exception when no format attribute is passed in', () {
1962 expect(
1963 () {
1964 setupLocalizations(<String, String>{
1965 'en': '''
1966{
1967 "springBegins": "Spring begins on {springStartDate}",
1968 "@springBegins": {
1969 "description": "The first day of spring",
1970 "placeholders": {
1971 "springStartDate": {
1972 "type": "DateTime"
1973 }
1974 }
1975 }
1976}''',
1977 });
1978 },
1979 throwsA(
1980 isA<L10nException>().having(
1981 (L10nException e) => e.message,
1982 'message',
1983 allOf(
1984 contains('message "springBegins"'),
1985 contains('locale "en"'),
1986 contains('the "format" attribute needs to be set'),
1987 ),
1988 ),
1989 ),
1990 );
1991 });
1992
1993 testWithoutContext('handle date with multiple locale', () {
1994 setupLocalizations(<String, String>{
1995 'en': '''
1996{
1997 "@@locale": "en",
1998 "springBegins": "Spring begins on {springStartDate}",
1999 "@springBegins": {
2000 "description": "The first day of spring",
2001 "placeholders": {
2002 "springStartDate": {
2003 "type": "DateTime",
2004 "format": "MMMd"
2005 }
2006 }
2007 }
2008}''',
2009 'ja': '''
2010{
2011 "@@locale": "ja",
2012 "springBegins": "春が始まるのは{springStartDate}",
2013 "@springBegins": {
2014 "placeholders": {
2015 "springStartDate": {
2016 "type": "DateTime",
2017 "format": "MMMMd"
2018 }
2019 }
2020 }
2021}''',
2022 });
2023
2024 expect(
2025 getSyntheticGeneratedFileContent(locale: 'en'),
2026 contains('intl.DateFormat.MMMd(localeName)'),
2027 );
2028 expect(
2029 getSyntheticGeneratedFileContent(locale: 'ja'),
2030 contains('intl.DateFormat.MMMMd(localeName)'),
2031 );
2032 expect(
2033 getSyntheticGeneratedFileContent(locale: 'en'),
2034 contains('String springBegins(DateTime springStartDate)'),
2035 );
2036 expect(
2037 getSyntheticGeneratedFileContent(locale: 'ja'),
2038 contains('String springBegins(DateTime springStartDate)'),
2039 );
2040 });
2041
2042 testWithoutContext(
2043 'handle date with multiple locale when only template has placeholders',
2044 () {
2045 setupLocalizations(<String, String>{
2046 'en': '''
2047{
2048 "@@locale": "en",
2049 "springBegins": "Spring begins on {springStartDate}",
2050 "@springBegins": {
2051 "description": "The first day of spring",
2052 "placeholders": {
2053 "springStartDate": {
2054 "type": "DateTime",
2055 "format": "MMMd"
2056 }
2057 }
2058 }
2059}''',
2060 'ja': '''
2061{
2062 "@@locale": "ja",
2063 "springBegins": "春が始まるのは{springStartDate}"
2064}''',
2065 });
2066
2067 expect(
2068 getSyntheticGeneratedFileContent(locale: 'en'),
2069 contains('intl.DateFormat.MMMd(localeName)'),
2070 );
2071 expect(
2072 getSyntheticGeneratedFileContent(locale: 'ja'),
2073 contains('intl.DateFormat.MMMd(localeName)'),
2074 );
2075 expect(
2076 getSyntheticGeneratedFileContent(locale: 'en'),
2077 contains('String springBegins(DateTime springStartDate)'),
2078 );
2079 expect(
2080 getSyntheticGeneratedFileContent(locale: 'ja'),
2081 contains('String springBegins(DateTime springStartDate)'),
2082 );
2083 },
2084 );
2085
2086 testWithoutContext('handle date with multiple locale when there is unused placeholder', () {
2087 setupLocalizations(<String, String>{
2088 'en': '''
2089{
2090 "@@locale": "en",
2091 "springBegins": "Spring begins on {springStartDate}",
2092 "@springBegins": {
2093 "description": "The first day of spring",
2094 "placeholders": {
2095 "springStartDate": {
2096 "type": "DateTime",
2097 "format": "MMMd"
2098 }
2099 }
2100 }
2101}''',
2102 'ja': '''
2103{
2104 "@@locale": "ja",
2105 "springBegins": "春が始まるのは{springStartDate}",
2106 "@springBegins": {
2107 "description": "The first day of spring",
2108 "placeholders": {
2109 "notUsed": {
2110 "type": "DateTime",
2111 "format": "MMMMd"
2112 }
2113 }
2114 }
2115}''',
2116 });
2117
2118 expect(
2119 getSyntheticGeneratedFileContent(locale: 'en'),
2120 contains('intl.DateFormat.MMMd(localeName)'),
2121 );
2122 expect(
2123 getSyntheticGeneratedFileContent(locale: 'ja'),
2124 contains('intl.DateFormat.MMMd(localeName)'),
2125 );
2126 expect(
2127 getSyntheticGeneratedFileContent(locale: 'en'),
2128 contains('String springBegins(DateTime springStartDate)'),
2129 );
2130 expect(
2131 getSyntheticGeneratedFileContent(locale: 'ja'),
2132 contains('String springBegins(DateTime springStartDate)'),
2133 );
2134 expect(getSyntheticGeneratedFileContent(locale: 'ja'), isNot(contains('notUsed')));
2135 });
2136
2137 testWithoutContext('handle date with multiple locale when placeholders are incompatible', () {
2138 expect(
2139 () {
2140 setupLocalizations(<String, String>{
2141 'en': '''
2142 {
2143 "@@locale": "en",
2144 "springBegins": "Spring begins on {springStartDate}",
2145 "@springBegins": {
2146 "description": "The first day of spring",
2147 "placeholders": {
2148 "springStartDate": {
2149 "type": "DateTime",
2150 "format": "MMMd"
2151 }
2152 }
2153 }
2154 }''',
2155 'ja': '''
2156 {
2157 "@@locale": "ja",
2158 "springBegins": "春が始まるのは{springStartDate}",
2159 "@springBegins": {
2160 "description": "The first day of spring",
2161 "placeholders": {
2162 "springStartDate": {
2163 "type": "String"
2164 }
2165 }
2166 }
2167 }''',
2168 });
2169 },
2170 throwsA(
2171 isA<L10nException>().having(
2172 (L10nException e) => e.message,
2173 'message',
2174 allOf(
2175 contains('placeholder "springStartDate"'),
2176 contains('locale "ja"'),
2177 contains(
2178 '"type" resource attribute set to the type "String" in locale "ja", but it is "DateTime" in the template placeholder.',
2179 ),
2180 ),
2181 ),
2182 ),
2183 );
2184 });
2185
2186 testWithoutContext(
2187 'handle date with multiple locale when non-template placeholder does not specify type',
2188 () {
2189 expect(
2190 () {
2191 setupLocalizations(<String, String>{
2192 'en': '''
2193 {
2194 "@@locale": "en",
2195 "springBegins": "Spring begins on {springStartDate}",
2196 "@springBegins": {
2197 "description": "The first day of spring",
2198 "placeholders": {
2199 "springStartDate": {
2200 "type": "DateTime",
2201 "format": "MMMd"
2202 }
2203 }
2204 }
2205 }''',
2206 'ja': '''
2207 {
2208 "@@locale": "ja",
2209 "springBegins": "春が始まるのは{springStartDate}",
2210 "@springBegins": {
2211 "description": "The first day of spring",
2212 "placeholders": {
2213 "springStartDate": {
2214 "format": "MMMMd"
2215 }
2216 }
2217 }
2218 }''',
2219 });
2220 },
2221 throwsA(
2222 isA<L10nException>().having(
2223 (L10nException e) => e.message,
2224 'message',
2225 allOf(
2226 contains('placeholder "springStartDate"'),
2227 contains('locale "ja"'),
2228 contains(
2229 'has its "type" resource attribute set to the type "Object" in locale "ja", but it is "DateTime" in the template placeholder.',
2230 ),
2231 ),
2232 ),
2233 ),
2234 );
2235 },
2236 );
2237
2238 testWithoutContext('handle ordinary formatted date and arbitrary formatted date', () {
2239 setupLocalizations(<String, String>{
2240 'en': '''
2241{
2242 "@@locale": "en",
2243 "springBegins": "Spring begins on {springStartDate}",
2244 "@springBegins": {
2245 "description": "The first day of spring",
2246 "placeholders": {
2247 "springStartDate": {
2248 "type": "DateTime",
2249 "format": "MMMd"
2250 }
2251 }
2252 }
2253}''',
2254 'ja': '''
2255{
2256 "@@locale": "ja",
2257 "springBegins": "春が始まるのは{springStartDate}",
2258 "@springBegins": {
2259 "placeholders": {
2260 "springStartDate": {
2261 "type": "DateTime",
2262 "format": "立春",
2263 "isCustomDateFormat": "true"
2264 }
2265 }
2266 }
2267}''',
2268 });
2269
2270 expect(
2271 getSyntheticGeneratedFileContent(locale: 'en'),
2272 contains('intl.DateFormat.MMMd(localeName)'),
2273 );
2274 expect(
2275 getSyntheticGeneratedFileContent(locale: 'ja'),
2276 contains(r"DateFormat('立春', localeName)"),
2277 );
2278 expect(
2279 getSyntheticGeneratedFileContent(locale: 'en'),
2280 contains('String springBegins(DateTime springStartDate)'),
2281 );
2282 expect(
2283 getSyntheticGeneratedFileContent(locale: 'ja'),
2284 contains('String springBegins(DateTime springStartDate)'),
2285 );
2286 });
2287
2288 testWithoutContext('handle arbitrary formatted date with multiple locale', () {
2289 setupLocalizations(<String, String>{
2290 'en': '''
2291{
2292 "@@locale": "en",
2293 "springBegins": "Spring begins on {springStartDate}",
2294 "@springBegins": {
2295 "description": "The first day of spring",
2296 "placeholders": {
2297 "springStartDate": {
2298 "type": "DateTime",
2299 "format": "asdf o'clock",
2300 "isCustomDateFormat": "true"
2301 }
2302 }
2303 }
2304}''',
2305 'ja': '''
2306{
2307 "@@locale": "ja",
2308 "springBegins": "春が始まるのは{springStartDate}",
2309 "@springBegins": {
2310 "placeholders": {
2311 "springStartDate": {
2312 "type": "DateTime",
2313 "format": "立春",
2314 "isCustomDateFormat": "true"
2315 }
2316 }
2317 }
2318}''',
2319 });
2320
2321 expect(
2322 getSyntheticGeneratedFileContent(locale: 'en'),
2323 contains(r"DateFormat('asdf o\'clock', localeName)"),
2324 );
2325 expect(
2326 getSyntheticGeneratedFileContent(locale: 'ja'),
2327 contains(r"DateFormat('立春', localeName)"),
2328 );
2329 expect(
2330 getSyntheticGeneratedFileContent(locale: 'en'),
2331 contains('String springBegins(DateTime springStartDate)'),
2332 );
2333 expect(
2334 getSyntheticGeneratedFileContent(locale: 'ja'),
2335 contains('String springBegins(DateTime springStartDate)'),
2336 );
2337 });
2338 });
2339
2340 group('NumberFormat tests', () {
2341 testWithoutContext('imports package:intl', () {
2342 setupLocalizations(<String, String>{
2343 'en': '''
2344{
2345 "courseCompletion": "You have completed {progress} of the course.",
2346 "@courseCompletion": {
2347 "description": "The amount of progress the student has made in their class.",
2348 "placeholders": {
2349 "progress": {
2350 "type": "double",
2351 "format": "percentPattern"
2352 }
2353 }
2354 }
2355}''',
2356 });
2357 final String content = getSyntheticGeneratedFileContent(locale: 'en');
2358 expect(content, contains(intlImportDartCode));
2359 });
2360
2361 testWithoutContext('throws an exception when improperly formatted number is passed in', () {
2362 expect(
2363 () {
2364 setupLocalizations(<String, String>{
2365 'en': '''
2366{
2367 "courseCompletion": "You have completed {progress} of the course.",
2368 "@courseCompletion": {
2369 "description": "The amount of progress the student has made in their class.",
2370 "placeholders": {
2371 "progress": {
2372 "type": "double",
2373 "format": "asdf"
2374 }
2375 }
2376 }
2377}''',
2378 });
2379 },
2380 throwsA(
2381 isA<L10nException>().having(
2382 (L10nException e) => e.message,
2383 'message',
2384 allOf(
2385 contains('message "courseCompletion"'),
2386 contains('locale "en"'),
2387 contains('asdf'),
2388 contains('progress'),
2389 contains('does not have a corresponding NumberFormat'),
2390 ),
2391 ),
2392 ),
2393 );
2394 });
2395 });
2396
2397 group('plural messages', () {
2398 testWithoutContext(
2399 'intl package import should be omitted in subclass files when no plurals are included',
2400 () {
2401 setupLocalizations(<String, String>{
2402 'en': singleMessageArbFileString,
2403 'es': singleEsMessageArbFileString,
2404 });
2405 expect(getSyntheticGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
2406 },
2407 );
2408
2409 testWithoutContext('warnings are generated when plural parts are repeated', () {
2410 setupLocalizations(<String, String>{
2411 'en': '''
2412{
2413 "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
2414 "@helloWorlds": {
2415 "description": "Properly formatted but has redundant zero cases."
2416 }
2417}''',
2418 });
2419 expect(logger.hadWarningOutput, isTrue);
2420 expect(
2421 logger.warningText,
2422 contains('''
2423[app_en.arb:helloWorlds] ICU Syntax Warning: The plural part specified below is overridden by a later plural part.
2424 {count,plural, =0{Hello}zero{hello} other{hi}}
2425 ^'''),
2426 );
2427 });
2428
2429 testWithoutContext('undefined plural cases throws syntax error', () {
2430 try {
2431 setupLocalizations(<String, String>{
2432 'en': '''
2433{
2434 "count": "{count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}}"
2435}''',
2436 });
2437 } on L10nException catch (error) {
2438 expect(error.message, contains('Found syntax errors.'));
2439 expect(logger.hadErrorOutput, isTrue);
2440 expect(
2441 logger.errorText,
2442 contains('''
2443[app_en.arb:count] The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "many", or "other.
2444 3 is not a valid plural case.
2445 {count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}}
2446 ^'''),
2447 );
2448 }
2449 });
2450
2451 testWithoutContext(
2452 'should automatically infer plural placeholders that are not explicitly defined',
2453 () {
2454 setupLocalizations(<String, String>{
2455 'en': '''
2456{
2457 "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}}",
2458 "@helloWorlds": {
2459 "description": "Improperly formatted since it has no placeholder attribute."
2460 }
2461}''',
2462 });
2463 expect(
2464 getSyntheticGeneratedFileContent(locale: 'en'),
2465 contains('String helloWorlds(num count) {'),
2466 );
2467 },
2468 );
2469
2470 testWithoutContext(
2471 'should throw attempting to generate a plural message with incorrect format for placeholders',
2472 () {
2473 expect(
2474 () {
2475 setupLocalizations(<String, String>{
2476 'en': '''
2477{
2478 "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}}",
2479 "@helloWorlds": {
2480 "placeholders": "Incorrectly a string, should be a map."
2481 }
2482}''',
2483 });
2484 },
2485 throwsA(
2486 isA<L10nException>().having(
2487 (L10nException e) => e.message,
2488 'message',
2489 allOf(
2490 contains('message "helloWorlds"'),
2491 contains('is not properly formatted'),
2492 contains('Ensure that it is a map with string valued keys'),
2493 ),
2494 ),
2495 ),
2496 );
2497 },
2498 );
2499 });
2500
2501 group('select messages', () {
2502 testWithoutContext(
2503 'should automatically infer select placeholders that are not explicitly defined',
2504 () {
2505 setupLocalizations(<String, String>{
2506 'en': '''
2507{
2508 "genderSelect": "{gender, select, female {She} male {He} other {they} }",
2509 "@genderSelect": {
2510 "description": "Improperly formatted since it has no placeholder attribute."
2511 }
2512}''',
2513 });
2514 expect(
2515 getSyntheticGeneratedFileContent(locale: 'en'),
2516 contains('String genderSelect(String gender) {'),
2517 );
2518 },
2519 );
2520
2521 testWithoutContext(
2522 'should throw attempting to generate a select message with incorrect format for placeholders',
2523 () {
2524 expect(
2525 () {
2526 setupLocalizations(<String, String>{
2527 'en': '''
2528{
2529 "genderSelect": "{gender, select, female {She} male {He} other {they} }",
2530 "@genderSelect": {
2531 "placeholders": "Incorrectly a string, should be a map."
2532 }
2533}''',
2534 });
2535 },
2536 throwsA(
2537 isA<L10nException>().having(
2538 (L10nException e) => e.message,
2539 'message',
2540 allOf(
2541 contains('message "genderSelect"'),
2542 contains('is not properly formatted'),
2543 contains('Ensure that it is a map with string valued keys'),
2544 ),
2545 ),
2546 ),
2547 );
2548 },
2549 );
2550
2551 testWithoutContext(
2552 'should throw attempting to generate a select message with an incorrect message',
2553 () {
2554 try {
2555 setupLocalizations(<String, String>{
2556 'en': '''
2557{
2558 "genderSelect": "{gender, select,}",
2559 "@genderSelect": {
2560 "placeholders": {
2561 "gender": {}
2562 }
2563 }
2564}''',
2565 });
2566 } on L10nException {
2567 expect(
2568 logger.errorText,
2569 contains('''
2570[app_en.arb:genderSelect] ICU Syntax Error: Select expressions must have an "other" case.
2571 {gender, select,}
2572 ^'''),
2573 );
2574 }
2575 },
2576 );
2577 });
2578
2579 group('argument messages', () {
2580 testWithoutContext('should generate proper calls to intl.DateFormat', () {
2581 setupLocalizations(<String, String>{
2582 'en': '''
2583{
2584 "datetime": "{today, date, ::yMd}"
2585}''',
2586 });
2587 expect(
2588 getSyntheticGeneratedFileContent(locale: 'en'),
2589 contains('intl.DateFormat.yMd(localeName).format(today)'),
2590 );
2591 });
2592
2593 testWithoutContext('should generate proper calls to intl.DateFormat when using time', () {
2594 setupLocalizations(<String, String>{
2595 'en': '''
2596{
2597 "datetime": "{current, time, ::jms}"
2598}''',
2599 });
2600 expect(
2601 getSyntheticGeneratedFileContent(locale: 'en'),
2602 contains('intl.DateFormat.jms(localeName).format(current)'),
2603 );
2604 });
2605
2606 testWithoutContext(
2607 'should not complain when placeholders are explicitly typed to DateTime',
2608 () {
2609 setupLocalizations(<String, String>{
2610 'en': '''
2611{
2612 "datetime": "{today, date, ::yMd}",
2613 "@datetime": {
2614 "placeholders": {
2615 "today": { "type": "DateTime" }
2616 }
2617 }
2618}''',
2619 });
2620 expect(
2621 getSyntheticGeneratedFileContent(locale: 'en'),
2622 contains('String datetime(DateTime today) {'),
2623 );
2624 },
2625 );
2626
2627 testWithoutContext(
2628 'should automatically infer date time placeholders that are not explicitly defined',
2629 () {
2630 setupLocalizations(<String, String>{
2631 'en': '''
2632{
2633 "datetime": "{today, date, ::yMd}"
2634}''',
2635 });
2636 expect(
2637 getSyntheticGeneratedFileContent(locale: 'en'),
2638 contains('String datetime(DateTime today) {'),
2639 );
2640 },
2641 );
2642
2643 testWithoutContext('should throw on invalid DateFormat', () {
2644 try {
2645 setupLocalizations(<String, String>{
2646 'en': '''
2647{
2648 "datetime": "{today, date, ::yMMMMMd}"
2649}''',
2650 });
2651 assert(false);
2652 } on L10nException {
2653 expect(
2654 logger.errorText,
2655 allOf(
2656 contains('message "datetime"'),
2657 contains('locale "en"'),
2658 contains(
2659 'date format "yMMMMMd" for placeholder today does not have a corresponding DateFormat constructor',
2660 ),
2661 ),
2662 );
2663 }
2664 });
2665 });
2666
2667 // All error handling for messages should collect errors on a per-error
2668 // basis and log them out individually. Then, it will throw an L10nException.
2669 group('error handling tests', () {
2670 testWithoutContext('syntax/code-gen errors properly logs errors per message', () {
2671 // TODO(thkim1011): Fix error handling so that long indents don't get truncated.
2672 // See https://github.com/flutter/flutter/issues/120490.
2673 try {
2674 setupLocalizations(<String, String>{
2675 'en': '''
2676{
2677 "hello": "Hello { name",
2678 "plural": "This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}",
2679 "explanationWithLexingError": "The 'string above is incorrect as it forgets to close the brace",
2680 "pluralWithInvalidCase": "{ count, plural, woohoo{huh?} other{lol} }"
2681}''',
2682 }, useEscaping: true);
2683 } on L10nException {
2684 expect(
2685 logger.errorText,
2686 contains('''
2687[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2688 Hello { name
2689 ^
2690[app_en.arb:plural] ICU Syntax Error: Expected "}" but found no tokens.
2691 This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}
2692 ^
2693[app_en.arb:explanationWithLexingError] ICU Lexing Error: Unmatched single quotes.
2694 The 'string above is incorrect as it forgets to close the brace
2695 ^
2696[app_en.arb:pluralWithInvalidCase] ICU Syntax Error: Plural expressions case must be one of "zero", "one", "two", "few", "many", or "other".
2697 { count, plural, woohoo{huh?} other{lol} }
2698 ^'''),
2699 );
2700 }
2701 });
2702
2703 testWithoutContext('errors thrown in multiple languages are all shown', () {
2704 try {
2705 setupLocalizations(<String, String>{
2706 'en': '{ "hello": "Hello { name" }',
2707 'es': '{ "hello": "Hola { name" }',
2708 });
2709 } on L10nException {
2710 expect(
2711 logger.errorText,
2712 contains('''
2713[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2714 Hello { name
2715 ^
2716[app_es.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
2717 Hola { name
2718 ^'''),
2719 );
2720 }
2721 });
2722 });
2723
2724 testWithoutContext(
2725 'intl package import should be kept in subclass files when plurals are included',
2726 () {
2727 const String pluralMessageArb = '''
2728{
2729 "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}}",
2730 "@helloWorlds": {
2731 "description": "A plural message",
2732 "placeholders": {
2733 "count": {}
2734 }
2735 }
2736}
2737''';
2738 const String pluralMessageEsArb = '''
2739{
2740 "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}}"
2741}
2742''';
2743 setupLocalizations(<String, String>{'en': pluralMessageArb, 'es': pluralMessageEsArb});
2744 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
2745 expect(getSyntheticGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
2746 },
2747 );
2748
2749 testWithoutContext(
2750 'intl package import should be kept in subclass files when select is included',
2751 () {
2752 const String selectMessageArb = '''
2753{
2754 "genderSelect": "{gender, select, female {She} male {He} other {they} }",
2755 "@genderSelect": {
2756 "description": "A select message",
2757 "placeholders": {
2758 "gender": {}
2759 }
2760 }
2761}
2762''';
2763 const String selectMessageEsArb = '''
2764{
2765 "genderSelect": "{gender, select, female {ES - She} male {ES - He} other {ES - they} }"
2766}
2767''';
2768 setupLocalizations(<String, String>{'en': selectMessageArb, 'es': selectMessageEsArb});
2769 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
2770 expect(getSyntheticGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
2771 },
2772 );
2773
2774 testWithoutContext('check indentation on generated files', () {
2775 setupLocalizations(<String, String>{
2776 'en': singleMessageArbFileString,
2777 'es': singleEsMessageArbFileString,
2778 });
2779 // Tests a few of the lines in the generated code.
2780 // Localizations lookup code
2781 final String localizationsFile = getSyntheticGeneratedFileContent();
2782 expect(localizationsFile.contains(' switch (locale.languageCode) {'), true);
2783 expect(localizationsFile.contains(" case 'en': return AppLocalizationsEn();"), true);
2784 expect(localizationsFile.contains(" case 'es': return AppLocalizationsEs();"), true);
2785 expect(localizationsFile.contains(' }'), true);
2786
2787 // Supported locales list
2788 expect(
2789 localizationsFile.contains(' static const List<Locale> supportedLocales = <Locale>['),
2790 true,
2791 );
2792 expect(localizationsFile.contains(" Locale('en'),"), true);
2793 expect(localizationsFile.contains(" Locale('es')"), true);
2794 expect(localizationsFile.contains(' ];'), true);
2795 });
2796
2797 testWithoutContext(
2798 'foundation package import should be omitted from file template when deferred loading = true',
2799 () {
2800 setupLocalizations(<String, String>{
2801 'en': singleMessageArbFileString,
2802 'es': singleEsMessageArbFileString,
2803 }, useDeferredLoading: true);
2804 expect(getSyntheticGeneratedFileContent(), isNot(contains(foundationImportDartCode)));
2805 },
2806 );
2807
2808 testWithoutContext(
2809 'foundation package import should be kept in file template when deferred loading = false',
2810 () {
2811 setupLocalizations(<String, String>{
2812 'en': singleMessageArbFileString,
2813 'es': singleEsMessageArbFileString,
2814 });
2815 expect(getSyntheticGeneratedFileContent(), contains(foundationImportDartCode));
2816 },
2817 );
2818
2819 testWithoutContext('check for string interpolation rules', () {
2820 const String enArbCheckList = '''
2821{
2822 "one": "The number of {one} elapsed is: 44",
2823 "@one": {
2824 "description": "test one",
2825 "placeholders": {
2826 "one": {
2827 "type": "String"
2828 }
2829 }
2830 },
2831 "two": "哈{two}哈",
2832 "@two": {
2833 "description": "test two",
2834 "placeholders": {
2835 "two": {
2836 "type": "String"
2837 }
2838 }
2839 },
2840 "three": "m{three}m",
2841 "@three": {
2842 "description": "test three",
2843 "placeholders": {
2844 "three": {
2845 "type": "String"
2846 }
2847 }
2848 },
2849 "four": "I have to work _{four}_ sometimes.",
2850 "@four": {
2851 "description": "test four",
2852 "placeholders": {
2853 "four": {
2854 "type": "String"
2855 }
2856 }
2857 },
2858 "five": "{five} elapsed.",
2859 "@five": {
2860 "description": "test five",
2861 "placeholders": {
2862 "five": {
2863 "type": "String"
2864 }
2865 }
2866 },
2867 "six": "{six}m",
2868 "@six": {
2869 "description": "test six",
2870 "placeholders": {
2871 "six": {
2872 "type": "String"
2873 }
2874 }
2875 },
2876 "seven": "hours elapsed: {seven}",
2877 "@seven": {
2878 "description": "test seven",
2879 "placeholders": {
2880 "seven": {
2881 "type": "String"
2882 }
2883 }
2884 },
2885 "eight": " {eight}",
2886 "@eight": {
2887 "description": "test eight",
2888 "placeholders": {
2889 "eight": {
2890 "type": "String"
2891 }
2892 }
2893 },
2894 "nine": "m{nine}",
2895 "@nine": {
2896 "description": "test nine",
2897 "placeholders": {
2898 "nine": {
2899 "type": "String"
2900 }
2901 }
2902 }
2903}
2904''';
2905
2906 // It's fine that the arb is identical -- Just checking
2907 // generated code for use of '${variable}' vs '$variable'
2908 const String esArbCheckList = '''
2909{
2910 "one": "The number of {one} elapsed is: 44",
2911 "two": "哈{two}哈",
2912 "three": "m{three}m",
2913 "four": "I have to work _{four}_ sometimes.",
2914 "five": "{five} elapsed.",
2915 "six": "{six}m",
2916 "seven": "hours elapsed: {seven}",
2917 "eight": " {eight}",
2918 "nine": "m{nine}"
2919}
2920''';
2921 setupLocalizations(<String, String>{'en': enArbCheckList, 'es': esArbCheckList});
2922 final String localizationsFile = getSyntheticGeneratedFileContent(locale: 'es');
2923 expect(localizationsFile, contains(r'$one'));
2924 expect(localizationsFile, contains(r'$two'));
2925 expect(localizationsFile, contains(r'${three}'));
2926 expect(localizationsFile, contains(r'${four}'));
2927 expect(localizationsFile, contains(r'$five'));
2928 expect(localizationsFile, contains(r'${six}m'));
2929 expect(localizationsFile, contains(r'$seven'));
2930 expect(localizationsFile, contains(r'$eight'));
2931 expect(localizationsFile, contains(r'$nine'));
2932 });
2933
2934 testWithoutContext('check for string interpolation rules - plurals', () {
2935 const String enArbCheckList = '''
2936{
2937 "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
2938 "@first": {
2939 "description": "First set of plural messages to test.",
2940 "placeholders": {
2941 "count": {}
2942 }
2943 },
2944 "second": "{count,plural, =0{test {count}} other{ {count}}}",
2945 "@second": {
2946 "description": "Second set of plural messages to test.",
2947 "placeholders": {
2948 "count": {}
2949 }
2950 },
2951 "third": "{total,plural, =0{test {total}} other{ {total}}}",
2952 "@third": {
2953 "description": "Third set of plural messages to test, for number.",
2954 "placeholders": {
2955 "total": {
2956 "type": "int",
2957 "format": "compactLong"
2958 }
2959 }
2960 }
2961}
2962''';
2963
2964 // It's fine that the arb is identical -- Just checking
2965 // generated code for use of '${variable}' vs '$variable'
2966 const String esArbCheckList = '''
2967{
2968 "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
2969 "second": "{count,plural, =0{test {count}} other{ {count}}}"
2970}
2971''';
2972 setupLocalizations(<String, String>{'en': enArbCheckList, 'es': esArbCheckList});
2973 final String localizationsFile = getSyntheticGeneratedFileContent(locale: 'es');
2974 expect(localizationsFile, contains(r'test $count test'));
2975 expect(localizationsFile, contains(r'哈$count哈'));
2976 expect(localizationsFile, contains(r'm${count}m'));
2977 expect(localizationsFile, contains(r'_${count}_'));
2978 expect(localizationsFile, contains(r'$count test'));
2979 expect(localizationsFile, contains(r'${count}m'));
2980 expect(localizationsFile, contains(r'test $count'));
2981 expect(localizationsFile, contains(r' $count'));
2982 expect(localizationsFile, contains(r'String totalString = totalNumberFormat'));
2983 expect(localizationsFile, contains(r'totalString'));
2984 expect(localizationsFile, contains(r'totalString'));
2985 });
2986
2987 testWithoutContext('should throw with descriptive error message when failing to parse the '
2988 'arb file', () {
2989 const String arbFileWithTrailingComma = '''
2990{
2991 "title": "Stocks",
2992 "@title": {
2993 "description": "Title for the Stocks application"
2994 },
2995}''';
2996 expect(
2997 () {
2998 setupLocalizations(<String, String>{'en': arbFileWithTrailingComma});
2999 },
3000 throwsA(
3001 isA<L10nException>().having(
3002 (L10nException e) => e.message,
3003 'message',
3004 allOf(
3005 contains('app_en.arb'),
3006 contains('FormatException'),
3007 contains('Unexpected character'),
3008 ),
3009 ),
3010 ),
3011 );
3012 });
3013
3014 testWithoutContext(
3015 'should throw when resource is missing resource attribute (isResourceAttributeRequired = true)',
3016 () {
3017 const String arbFileWithMissingResourceAttribute = '''
3018{
3019 "title": "Stocks"
3020}''';
3021 expect(
3022 () {
3023 setupLocalizations(<String, String>{
3024 'en': arbFileWithMissingResourceAttribute,
3025 }, areResourceAttributeRequired: true);
3026 },
3027 throwsA(
3028 isA<L10nException>().having(
3029 (L10nException e) => e.message,
3030 'message',
3031 contains('Resource attribute "@title" was not found'),
3032 ),
3033 ),
3034 );
3035 },
3036 );
3037
3038 group('checks for method/getter formatting', () {
3039 testWithoutContext('cannot contain non-alphanumeric symbols', () {
3040 const String nonAlphaNumericArbFile = '''
3041{
3042 "title!!": "Stocks",
3043 "@title!!": {
3044 "description": "Title for the Stocks application"
3045 }
3046}''';
3047 expect(
3048 () => setupLocalizations(<String, String>{'en': nonAlphaNumericArbFile}),
3049 throwsA(
3050 isA<L10nException>().having(
3051 (L10nException e) => e.message,
3052 'message',
3053 contains('Invalid ARB resource name'),
3054 ),
3055 ),
3056 );
3057 });
3058
3059 testWithoutContext('must start with lowercase character', () {
3060 const String nonAlphaNumericArbFile = '''
3061{
3062 "Title": "Stocks",
3063 "@Title": {
3064 "description": "Title for the Stocks application"
3065 }
3066}''';
3067 expect(
3068 () => setupLocalizations(<String, String>{'en': nonAlphaNumericArbFile}),
3069 throwsA(
3070 isA<L10nException>().having(
3071 (L10nException e) => e.message,
3072 'message',
3073 contains('Invalid ARB resource name'),
3074 ),
3075 ),
3076 );
3077 });
3078
3079 testWithoutContext('cannot start with a number', () {
3080 const String nonAlphaNumericArbFile = '''
3081{
3082 "123title": "Stocks",
3083 "@123title": {
3084 "description": "Title for the Stocks application"
3085 }
3086}''';
3087 expect(
3088 () => setupLocalizations(<String, String>{'en': nonAlphaNumericArbFile}),
3089 throwsA(
3090 isA<L10nException>().having(
3091 (L10nException e) => e.message,
3092 'message',
3093 contains('Invalid ARB resource name'),
3094 ),
3095 ),
3096 );
3097 });
3098
3099 testWithoutContext('can start with and contain a dollar sign', () {
3100 const String dollarArbFile = r'''
3101{
3102 "$title$": "Stocks",
3103 "@$title$": {
3104 "description": "Title for the application"
3105 }
3106}''';
3107 setupLocalizations(<String, String>{'en': dollarArbFile});
3108 });
3109 });
3110
3111 testWithoutContext('throws when the language code is not supported', () {
3112 const String arbFileWithInvalidCode = '''
3113{
3114 "@@locale": "invalid",
3115 "title": "invalid"
3116}''';
3117
3118 final Directory l10nDirectory = fs.currentDirectory
3119 .childDirectory('lib')
3120 .childDirectory('l10n')..createSync(recursive: true);
3121 l10nDirectory.childFile('app_invalid.arb').writeAsStringSync(arbFileWithInvalidCode);
3122
3123 expect(
3124 () {
3125 LocalizationsGenerator(
3126 fileSystem: fs,
3127 inputPathString: defaultL10nPathString,
3128 outputPathString: defaultL10nPathString,
3129 templateArbFileName: 'app_invalid.arb',
3130 outputFileString: defaultOutputFileString,
3131 classNameString: defaultClassNameString,
3132 logger: logger,
3133 )
3134 ..loadResources()
3135 ..writeOutputFiles();
3136 },
3137 throwsA(
3138 isA<L10nException>().having(
3139 (L10nException e) => e.message,
3140 'message',
3141 contains('"invalid" is not a supported language code.'),
3142 ),
3143 ),
3144 );
3145 });
3146
3147 testWithoutContext('handle number with multiple locale', () {
3148 setupLocalizations(<String, String>{
3149 'en': '''
3150{
3151"@@locale": "en",
3152"money": "Sum {number}",
3153"@money": {
3154 "placeholders": {
3155 "number": {
3156 "type": "int",
3157 "format": "currency"
3158 }
3159 }
3160}
3161}''',
3162 'ja': '''
3163{
3164"@@locale": "ja",
3165"money": "合計 {number}",
3166"@money": {
3167 "placeholders": {
3168 "number": {
3169 "type": "int",
3170 "format": "decimalPatternDigits",
3171 "optionalParameters": {
3172 "decimalDigits": 3
3173 }
3174 }
3175 }
3176}
3177}''',
3178 });
3179
3180 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains('String money(int number)'));
3181 expect(getSyntheticGeneratedFileContent(locale: 'ja'), contains('String money(int number)'));
3182 expect(
3183 getSyntheticGeneratedFileContent(locale: 'en'),
3184 contains('intl.NumberFormat.currency('),
3185 );
3186 expect(
3187 getSyntheticGeneratedFileContent(locale: 'ja'),
3188 contains('intl.NumberFormat.decimalPatternDigits('),
3189 );
3190 expect(getSyntheticGeneratedFileContent(locale: 'ja'), contains('decimalDigits: 3'));
3191 });
3192
3193 testWithoutContext(
3194 'handle number with multiple locale specifying a format only in template',
3195 () {
3196 setupLocalizations(<String, String>{
3197 'en': '''
3198{
3199"@@locale": "en",
3200"money": "Sum {number}",
3201"@money": {
3202 "placeholders": {
3203 "number": {
3204 "type": "int",
3205 "format": "decimalPatternDigits",
3206 "optionalParameters": {
3207 "decimalDigits": 3
3208 }
3209 }
3210 }
3211}
3212}''',
3213 'ja': '''
3214{
3215"@@locale": "ja",
3216"money": "合計 {number}",
3217"@money": {
3218 "placeholders": {
3219 "number": {
3220 "type": "int"
3221 }
3222 }
3223}
3224}''',
3225 });
3226
3227 expect(
3228 getSyntheticGeneratedFileContent(locale: 'en'),
3229 contains('String money(int number)'),
3230 );
3231 expect(
3232 getSyntheticGeneratedFileContent(locale: 'ja'),
3233 contains('String money(int number)'),
3234 );
3235 expect(
3236 getSyntheticGeneratedFileContent(locale: 'en'),
3237 contains('intl.NumberFormat.decimalPatternDigits('),
3238 );
3239 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains('decimalDigits: 3'));
3240 expect(
3241 getSyntheticGeneratedFileContent(locale: 'en'),
3242 contains(r"return 'Sum $numberString'"),
3243 );
3244 expect(
3245 getSyntheticGeneratedFileContent(locale: 'ja'),
3246 isNot(contains('intl.NumberFormat')),
3247 );
3248 expect(getSyntheticGeneratedFileContent(locale: 'ja'), contains(r"return '合計 $number'"));
3249 },
3250 );
3251
3252 testWithoutContext(
3253 'handle number with multiple locale specifying a format only in non-template',
3254 () {
3255 setupLocalizations(<String, String>{
3256 'en': '''
3257{
3258"@@locale": "en",
3259"money": "Sum {number}",
3260"@money": {
3261 "placeholders": {
3262 "number": {
3263 "type": "int"
3264 }
3265 }
3266}
3267}''',
3268 'ja': '''
3269{
3270"@@locale": "ja",
3271"money": "合計 {number}",
3272"@money": {
3273 "placeholders": {
3274 "number": {
3275 "type": "int",
3276 "format": "decimalPatternDigits",
3277 "optionalParameters": {
3278 "decimalDigits": 3
3279 }
3280 }
3281 }
3282}
3283}''',
3284 });
3285
3286 expect(
3287 getSyntheticGeneratedFileContent(locale: 'en'),
3288 contains('String money(int number)'),
3289 );
3290 expect(
3291 getSyntheticGeneratedFileContent(locale: 'ja'),
3292 contains('String money(int number)'),
3293 );
3294 expect(
3295 getSyntheticGeneratedFileContent(locale: 'en'),
3296 isNot(contains('intl.NumberFormat')),
3297 );
3298 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains(r"return 'Sum $number'"));
3299 expect(
3300 getSyntheticGeneratedFileContent(locale: 'ja'),
3301 contains('intl.NumberFormat.decimalPatternDigits('),
3302 );
3303 expect(getSyntheticGeneratedFileContent(locale: 'ja'), contains('decimalDigits: 3'));
3304 expect(
3305 getSyntheticGeneratedFileContent(locale: 'ja'),
3306 contains(r"return '合計 $numberString'"),
3307 );
3308 },
3309 );
3310 });
3311
3312 testWithoutContext(
3313 'should generate a valid pubspec.yaml file when using synthetic package if it does not already exist',
3314 () {
3315 setupLocalizations(<String, String>{'en': singleMessageArbFileString});
3316 final Directory outputDirectory = fs.directory(syntheticPackagePath);
3317 final File pubspecFile = outputDirectory.childFile('pubspec.yaml');
3318 expect(pubspecFile.existsSync(), isTrue);
3319
3320 final YamlNode yamlNode = loadYamlNode(pubspecFile.readAsStringSync());
3321 expect(yamlNode, isA<YamlMap>());
3322
3323 final YamlMap yamlMap = yamlNode as YamlMap;
3324 final String pubspecName = yamlMap['name'] as String;
3325 final String pubspecDescription = yamlMap['description'] as String;
3326 expect(pubspecName, 'synthetic_package');
3327 expect(pubspecDescription, "The Flutter application's synthetic package.");
3328 },
3329 );
3330
3331 testWithoutContext(
3332 'should not overwrite existing pubspec.yaml file when using synthetic package',
3333 () {
3334 final File pubspecFile =
3335 fs.file(fs.path.join(syntheticPackagePath, 'pubspec.yaml'))
3336 ..createSync(recursive: true)
3337 ..writeAsStringSync('abcd');
3338 setupLocalizations(<String, String>{'en': singleMessageArbFileString});
3339 // The original pubspec file should not be overwritten.
3340 expect(pubspecFile.readAsStringSync(), 'abcd');
3341 },
3342 );
3343
3344 testWithoutContext('can use type: int without specifying a format', () {
3345 const String arbFile = '''
3346{
3347 "orderNumber": "This is order #{number}.",
3348 "@orderNumber": {
3349 "description": "The title for an order with a given number.",
3350 "placeholders": {
3351 "number": {
3352 "type": "int"
3353 }
3354 }
3355 }
3356}''';
3357 setupLocalizations(<String, String>{'en': arbFile});
3358 expect(
3359 getSyntheticGeneratedFileContent(locale: 'en'),
3360 containsIgnoringWhitespace(r'''
3361String orderNumber(int number) {
3362 return 'This is order #$number.';
3363}
3364'''),
3365 );
3366 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
3367 });
3368
3369 testWithoutContext('app localizations lookup is a public method', () {
3370 setupLocalizations(<String, String>{'en': singleMessageArbFileString});
3371 expect(
3372 getSyntheticGeneratedFileContent(),
3373 containsIgnoringWhitespace(r'''
3374AppLocalizations lookupAppLocalizations(Locale locale) {
3375'''),
3376 );
3377 });
3378
3379 testWithoutContext('escaping with single quotes', () {
3380 const String arbFile = '''
3381{
3382 "singleQuote": "Flutter''s amazing!",
3383 "@singleQuote": {
3384 "description": "A message with a single quote."
3385 }
3386}''';
3387 setupLocalizations(<String, String>{'en': arbFile}, useEscaping: true);
3388 expect(getSyntheticGeneratedFileContent(locale: 'en'), contains(r"Flutter\'s amazing"));
3389 });
3390
3391 testWithoutContext('suppress warnings flag actually suppresses warnings', () {
3392 const String pluralMessageWithOverriddenParts = '''
3393{
3394 "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
3395 "@helloWorlds": {
3396 "description": "Properly formatted but has redundant zero cases.",
3397 "placeholders": {
3398 "count": {}
3399 }
3400 }
3401}''';
3402 setupLocalizations(<String, String>{
3403 'en': pluralMessageWithOverriddenParts,
3404 }, suppressWarnings: true);
3405 expect(logger.hadWarningOutput, isFalse);
3406 });
3407
3408 testWithoutContext('can use decimalPatternDigits with decimalDigits optional parameter', () {
3409 const String arbFile = '''
3410{
3411 "treeHeight": "Tree height is {height}m.",
3412 "@treeHeight": {
3413 "placeholders": {
3414 "height": {
3415 "type": "double",
3416 "format": "decimalPatternDigits",
3417 "optionalParameters": {
3418 "decimalDigits": 3
3419 }
3420 }
3421 }
3422 }
3423}''';
3424 setupLocalizations(<String, String>{'en': arbFile});
3425 final String localizationsFile = getSyntheticGeneratedFileContent(locale: 'en');
3426 expect(
3427 localizationsFile,
3428 containsIgnoringWhitespace(r'''
3429String treeHeight(double height) {
3430'''),
3431 );
3432 expect(
3433 localizationsFile,
3434 containsIgnoringWhitespace(r'''
3435NumberFormat.decimalPatternDigits(
3436 locale: localeName,
3437 decimalDigits: 3
3438);
3439'''),
3440 );
3441 });
3442
3443 // Regression test for https://github.com/flutter/flutter/issues/125461.
3444 testWithoutContext('dollar signs are escaped properly when there is a select clause', () {
3445 const String dollarSignWithSelect = r'''
3446{
3447 "dollarSignWithSelect": "$nice_bug\nHello Bug! Manifestation #1 {selectPlaceholder, select, case{message} other{messageOther}}"
3448}''';
3449 setupLocalizations(<String, String>{'en': dollarSignWithSelect});
3450 expect(
3451 getSyntheticGeneratedFileContent(locale: 'en'),
3452 contains(r'\$nice_bug\nHello Bug! Manifestation #1 $_temp0'),
3453 );
3454 });
3455
3456 testWithoutContext('can generate method with named parameter', () {
3457 const String arbFile = '''
3458{
3459 "helloName": "Hello {name}!",
3460 "@helloName": {
3461 "description": "A more personal greeting",
3462 "placeholders": {
3463 "name": {
3464 "type": "String",
3465 "description": "The name of the person to greet"
3466 }
3467 }
3468 },
3469 "helloNameAndAge": "Hello {name}! You are {age} years old.",
3470 "@helloNameAndAge": {
3471 "description": "A more personal greeting",
3472 "placeholders": {
3473 "name": {
3474 "type": "String",
3475 "description": "The name of the person to greet"
3476 },
3477 "age": {
3478 "type": "int",
3479 "description": "The age of the person to greet"
3480 }
3481 }
3482 }
3483}
3484 ''';
3485 setupLocalizations(<String, String>{'en': arbFile}, useNamedParameters: true);
3486 final String localizationsFile = getSyntheticGeneratedFileContent(locale: 'en');
3487 expect(
3488 localizationsFile,
3489 containsIgnoringWhitespace(r'''
3490String helloName({required String name}) {
3491 '''),
3492 );
3493 expect(
3494 localizationsFile,
3495 containsIgnoringWhitespace(r'''
3496String helloNameAndAge({required String name, required int age}) {
3497 '''),
3498 );
3499 });
3500}
3501

Provided by KDAB

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