build static method

BuildModelResult build({
  1. required BuildModelConfig buildConfig,
  2. required Map<String, dynamic> map,
  3. BuildModelResult? baseData,
  4. bool handleLinks = true,
  5. bool shouldEscapeText = true,
  6. 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(),
  );
}