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