blob: ab2897199dfca7248f40a88742b23e9f94565252 [file] [log] [blame] [edit]
// 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:typed_data';
import 'package:build/build.dart';
import 'package:glob/glob.dart';
import 'package:intl/date_symbol_data_file.dart';
import 'package:messages_serializer/messages_serializer.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import 'arb_parser.dart';
import 'code_generation/code.dart';
import 'generation_options.dart';
import 'message_with_metadata.dart';
Builder carbBuilder(BuilderOptions options) =>
GenerateCarbBuilder(options.config);
class GenerateCarbBuilder implements Builder {
final Map<String, dynamic> config;
late final List<String> extensionsForArb;
GenerateCarbBuilder(this.config) {
final locales = availableLocalesForDateFormatting;
final contextYamlList = config['contexts'] as YamlList?;
final contexts = contextYamlList?.value.cast() ?? ['msg'];
extensionsForArb = [
...contexts.expand((context) => locales.map((locale) => '.carb')).toSet(),
...contexts.expand((context) => locales.map((locale) => '.json')).toSet(),
...contexts.map((context) => '.g.dart').toSet(),
].toList();
}
@override
Map<String, List<String>> get buildExtensions => {
'.arb': extensionsForArb,
'^pubspec.yaml': [],
};
@override
Future<void> build(BuildStep buildStep) async {
final generationOptions = await GenerationOptions.fromPubspec(buildStep);
await BuildStepGenerator(buildStep, generationOptions).build();
}
}
class BuildStepGenerator {
final BuildStep buildStep;
final GenerationOptions options;
BuildStepGenerator(this.buildStep, this.options);
Serializer get serializer => getSerializer(options);
Future<void> build() async {
final parsedMessageFiles = await getParsedMessageFiles();
assertFilesAreValid(parsedMessageFiles);
final currentMessageFile = parsedMessageFiles
.singleWhere((element) => element.assetId == buildStep.inputId);
await writeDataFile(currentMessageFile);
if (shouldGenerateDartLib(parsedMessageFiles, currentMessageFile)) {
await writeDartLibrary(parsedMessageFiles, currentMessageFile);
}
}
/// Generates the Dart library which extracts the messages from their file
/// format and makes the available to the user in a way specified through the
/// `GenerationOptions`.
Future<void> writeDartLibrary(
List<MessageFileResource> assetList,
MessageFileResource arb,
) async {
final resourcesInContext =
assetList.where((element) => element.context == arb.context);
final localeToResource = Map.fromEntries(resourcesInContext.map(
(resource) => MapEntry(
resource.locale,
resource.assetId
.changeExtension(getDataFileExtension())
.path
.split(path.separator)
.last)));
printIncludeFilesNotification(arb.context, localeToResource);
final resourceToHash = Map.fromEntries(
resourcesInContext.map((resource) => MapEntry(
localeToResource[resource.locale]!,
resource.hash,
)),
);
final libraryCode = CodeGenerator(
options,
arb.messageList,
localeToResource,
resourceToHash,
).generate();
final generatedMessageFile = buildStep.inputId.changeExtension('.g.dart');
await buildStep.writeAsString(generatedMessageFile, libraryCode);
}
String getDataFileExtension() => '.json';
Serializer<dynamic> getSerializer(GenerationOptions generationOptions) {
return JsonSerializer(generationOptions.findById);
}
void assertFilesAreValid(List<MessageFileResource> arbFiles) {
final contexts = arbFiles.map((e) => e.context).whereType<String>().toSet();
for (var context in contexts) {
final filesWithContext = arbFiles.where((arb) => arb.context == context);
if (filesWithContext
.where((element) => element.isTemplateResource)
.length >
1) {
throw ArgumentError('Multiple arb files are marked as templates.');
}
final localesInContext = filesWithContext.map((e) => e.locale).toList();
if (localesInContext.length != localesInContext.toSet().length) {
throw ArgumentError(
'Multiple arb files for the same context have the same locale.');
}
}
}
Future<List<MessageFileResource>> getParsedMessageFiles() async {
return buildStep.findAssets(Glob('**.arb')).asyncMap((assetId) async {
final arbFile = await buildStep.readAsString(assetId);
final decoded = jsonDecode(arbFile) as Map;
final arb = Map.castFrom<dynamic, dynamic, String, dynamic>(decoded);
final messageList = ArbParser(options.findById).parseMessageFile(arb);
return MessageFileResource(
assetId,
messageList,
arbFile.hashCode.toRadixString(32),
);
}).toList();
}
bool shouldGenerateDartLib(
List<MessageFileResource> arbResources, MessageFileResource arb) {
final isOnlyResourceForContext = arbResources
.where((element) => element.context == arb.context)
.length ==
1;
final shouldGenerateDartLib =
arb.isTemplateResource || isOnlyResourceForContext;
return shouldGenerateDartLib;
}
/// This writes the file containing the messages, which can be either a binary
/// `.carb` file or a JSON file, depending on the serializer.
///
/// This message data file must be shipped with the application, it is
/// unpacked at runtime so that the messages can be read from it.
///
/// Returns the list of indices of the messages which are visible to the user.
Future<void> writeDataFile<T>(MessageFileResource currentMessageFile) async {
final serialization = serializer.serialize(
currentMessageFile.hash,
currentMessageFile.locale,
currentMessageFile.messages.map((e) => e.message).toList(),
);
final carbFile =
currentMessageFile.assetId.changeExtension(getDataFileExtension());
final data = serialization.data;
if (data is Uint8List) {
await buildStep.writeAsBytes(carbFile, data);
} else if (data is String) {
await buildStep.writeAsString(carbFile, data);
}
}
void printIncludeFilesNotification(
String? context, Map<String, String> localeToResource) {
var contextMessage = 'The';
if (context != null) {
contextMessage = 'For the messages in $context, the';
}
final fileList =
localeToResource.entries.map((e) => '\t${e.value}').join('\n');
print(
'''$contextMessage following files need to be declared in your assets:\n$fileList''');
}
}
class MessageFileResource {
final AssetId assetId;
final String hash;
final MessageListWithMetadata messageList;
MessageFileResource(this.assetId, this.messageList, this.hash);
String get locale => messageList.locale!;
bool get isTemplateResource => messageList.isTemplate;
List<MessageWithMetadata> get messages => messageList.messages;
String? get context => messageList.context;
}