| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'arb_parser.dart'; | |
| import 'code_generation/classes_generation.dart'; | |
| import 'code_generation/code_generation.dart'; | |
| import 'generation_options.dart'; | |
| import 'located_message_file.dart'; | |
| import 'message_file.dart'; | |
| class MessageCallingCodeGenerator { | |
| final GenerationOptions options; | |
| final Map<String, String> mapping; | |
| MessageCallingCodeGenerator({ | |
| required this.options, | |
| required this.mapping, | |
| }); | |
| Future<void> build() async { | |
| final messageFiles = await _parseMessageFiles(); | |
| final families = messageFiles | |
| .groupListsBy((messageFile) => getParentFile(messageFiles, messageFile)) | |
| .map((key, value) => | |
| MapEntry(key, value.sortedBy((messageFile) => messageFile.locale))); | |
| var counter = 0; | |
| for (final MapEntry(key: parent, value: children) in families.entries) { | |
| final context = parent.file.context; | |
| printIncludeFilesNotification(context, children.map((f) => f.path)); | |
| final dummyFilePaths = Map.fromEntries(children | |
| .map((e) => e.locale) | |
| .map((e) => MapEntry(e, [context, e, 'empty'].join('_')))); | |
| final library = ClassesGeneration( | |
| options: options, | |
| context: context, | |
| parent: parent, | |
| children: children, | |
| emptyFiles: dummyFilePaths, | |
| ).generate(); | |
| final code = CodeGenerator( | |
| options: options, | |
| classes: library, | |
| emptyFilePaths: dummyFilePaths.values, | |
| ).generate(); | |
| final parentPath = Directory(options.generatedCodeFiles.path); | |
| final mainFile = File(path.join( | |
| parentPath.path, '${context ?? 'm${counter++}'}_messages.g.dart')); | |
| await mainFile.create(recursive: true); | |
| await mainFile.writeAsString(code); | |
| for (final MapEntry(key: locale, value: emptyFilePath) | |
| in dummyFilePaths.entries) { | |
| final file = File(path.join(parentPath.path, '$emptyFilePath.g.dart')); | |
| await file.create(); | |
| await file.writeAsString(''' | |
| // This is a helper file for deferred loading of the messages for locale $locale, | |
| // generated by `dart run messages`. | |
| '''); | |
| } | |
| } | |
| } | |
| Future<List<LocatedMessageFile>> _parseMessageFiles() async => | |
| Future.wait(mapping.entries | |
| .map((p) async => LocatedMessageFile( | |
| path: path.relative(p.value, from: Directory.current.path), | |
| file: await parseMessageFile(await getArbfile(p.key), options), | |
| )) | |
| .toList()); | |
| Future<String> getArbfile(String path) async => | |
| await File(path).readAsString(); | |
| /// Either get the referenced parent file, or try to infer which it might be. | |
| static LocatedMessageFile getParentFile( | |
| List<LocatedMessageFile> messageFiles, | |
| LocatedMessageFile currentFile, | |
| ) { | |
| /// Try to infer by looking at which files contain metadata, which is a sign | |
| /// they might be the references for others in the same context. | |
| final filesInContext = messageFiles.where( | |
| (messageFile) => messageFile.file.context == currentFile.file.context); | |
| final potentialParent = | |
| filesInContext.firstWhereOrNull((element) => element.file.hasMetadata); | |
| if (potentialParent == null && filesInContext.length > 1) { | |
| throw ArgumentError(''' | |
| The files $filesInContext have no metadata, so it is not clear which one is the main source of truth.'''); | |
| } | |
| return potentialParent ?? currentFile; | |
| } | |
| /// Display a notification to the user to include the newly generated files | |
| /// in their assets. | |
| void printIncludeFilesNotification( | |
| String? context, | |
| Iterable<String> fileList, | |
| ) { | |
| final contextMessage = | |
| context != null ? 'For the messages in $context, the' : 'The'; | |
| final fileListJoined = fileList.map((e) => '\t$e').join('\n'); | |
| print( | |
| '''$contextMessage following files need to be declared in your assets:\n$fileListJoined'''); | |
| } | |
| } | |
| Future<MessageFile> parseMessageFile( | |
| String arbFile, | |
| GenerationOptions options, | |
| ) async { | |
| final decoded = jsonDecode(arbFile) as Map; | |
| final arb = Map.castFrom<dynamic, dynamic, String, dynamic>(decoded); | |
| return ArbParser(options.findById).parseMessageFile(arb); | |
| } |