build static method
BuildModelResult
build({
- required BuildModelConfig buildConfig,
- required Map<
String, dynamic> map, - BuildModelResult? baseData,
- bool handleLinks = true,
- bool shouldEscapeText = true,
- required String localeDebug,
Builds the i18n model for ONE locale
The map
must be of type Map<String, dynamic> and all children may of type
String, num, List
If baseData
is set and BuildModelConfig.fallbackStrategy is FallbackStrategy.baseLocale,
then the base translations will be added to contexts where the translation is missing.
handleLinks
can be set false to ignore links and leave them as is
e.g. ${_root.greet(name: name} will be ${_root.greet}
This is used for "Translation Overrides" where the links are resolved
on invocation.
shouldEscapeText
can be set false to ignore escaping of text nodes
e.g. "Let's go" will be "Let's go" instead of "Let's go".
Similar to handleLinks
, this is used for "Translation Overrides".
Implementation
static BuildModelResult build({
required BuildModelConfig buildConfig,
required Map<String, dynamic> map,
BuildModelResult? baseData,
bool handleLinks = true,
bool shouldEscapeText = true,
required String localeDebug,
}) {
// flat map for leaves (TextNode, PluralNode, ContextNode)
final Map<String, LeafNode> leavesMap = {};
// base contexts to be used for fallback
final Map<String, PopulatedContextType>? baseContexts = baseData == null ||
baseData.contexts.isEmpty ||
buildConfig.fallbackStrategy == FallbackStrategy.none
? null
: {
for (final c in baseData.contexts)
c.enumName: PopulatedContextType(
enumName: c.enumName,
enumValues: c.enumValues,
generateEnum: c.generateEnum,
),
};
final contextCollection = {
for (final context in buildConfig.contexts) context.enumName: context,
};
// 1st iteration: Build nodes according to given map
//
// Linked Translations:
// They will be tracked but not handled
// Assumption: They are basic linked translations without parameters
// Reason: Not all TextNodes are built, so final parameters are unknown
final resultNodeTree = _parseMapNode(
localeDebug: localeDebug,
parentPath: '',
parentRawPath: '',
curr: map,
config: buildConfig,
keyCase: buildConfig.keyCase,
leavesMap: leavesMap,
contextCollection: contextCollection,
baseData: baseData,
baseContexts: baseContexts,
shouldEscapeText: shouldEscapeText,
);
// 2nd iteration: Handle parameterized linked translations
//
// TextNodes with parameterized linked translations are rebuilt with correct parameters.
if (handleLinks) {
leavesMap.entries
.where((entry) => entry.value is TextNode)
.forEach((entry) {
final key = entry.key;
final value = entry.value as TextNode;
final linkParamMap = <String, Set<String>>{};
final paramTypeMap = <String, String>{};
for (final link in value.links) {
final paramSet = <String>{};
final visitedLinks = <String>{};
final pathQueue = Queue<String>();
pathQueue.add(link);
while (pathQueue.isNotEmpty) {
final currLink = pathQueue.removeFirst();
final linkedNode = leavesMap[currLink];
if (linkedNode == null) {
throw '"$key" in <$localeDebug> is linked to "$currLink" but "$currLink" is undefined.';
}
visitedLinks.add(currLink);
if (linkedNode is TextNode) {
paramSet.addAll(linkedNode.params);
paramTypeMap.addAll(linkedNode.paramTypeMap);
// lookup links
for (final child in linkedNode.links) {
if (!visitedLinks.contains(child)) {
pathQueue.add(child);
}
}
} else if (linkedNode is PluralNode || linkedNode is ContextNode) {
final Iterable<TextNode> textNodes = linkedNode is PluralNode
? linkedNode.quantities.values
: (linkedNode as ContextNode).entries.values;
for (final textNode in textNodes) {
paramSet.addAll(textNode.params);
paramTypeMap.addAll(textNode.paramTypeMap);
}
if (linkedNode is PluralNode) {
if (linkedNode.rich) {
final builderParam = '${linkedNode.paramName}Builder';
paramSet.add(builderParam);
paramTypeMap[builderParam] =
'InlineSpan Function(${linkedNode.paramType})';
}
paramSet.add(linkedNode.paramName);
paramTypeMap[linkedNode.paramName] = linkedNode.paramType;
} else if (linkedNode is ContextNode) {
if (linkedNode.rich) {
final builderParam = '${linkedNode.paramName}Builder';
paramSet.add(builderParam);
paramTypeMap[builderParam] =
'InlineSpan Function(${linkedNode.context.enumName})';
}
paramSet.add(linkedNode.paramName);
paramTypeMap[linkedNode.paramName] =
linkedNode.context.enumName;
}
// lookup links of children
for (final element in textNodes) {
for (final child in element.links) {
if (!visitedLinks.contains(child)) {
pathQueue.add(child);
}
}
}
} else {
throw '"$key" is linked to "$currLink" which is a ${linkedNode.runtimeType} (must be $TextNode or $ObjectNode).';
}
}
linkParamMap[link] = paramSet;
}
if (linkParamMap.values.any((params) => params.isNotEmpty)) {
// rebuild TextNode because its linked translations have parameters
value.updateWithLinkParams(
linkParamMap: linkParamMap,
paramTypeMap: paramTypeMap,
);
}
});
}
// imaginary root node
final root = ObjectNode(
path: '',
rawPath: '',
comment: null,
modifiers: {},
entries: resultNodeTree,
isMap: false,
);
final interfaceCollection = buildConfig.buildInterfaceCollection();
// 3rd iteration: Add interfaces
_applyInterfaceAndGenericsRecursive(
curr: root,
interfaceCollection: interfaceCollection,
);
return BuildModelResult(
root: root,
interfaces: interfaceCollection.resultInterfaces.values.toList(),
contexts: contextCollection.values
.where((c) => c.enumValues != null)
.map((c) => PopulatedContextType(
enumName: c.enumName,
enumValues: c.enumValues!,
generateEnum: c.generateEnum,
))
.toList(),
);
}