private void ProcessObjectElement(JsonElement element, string elementName, TraversalArgs args)
        {
            HashSet <string> childKeys = new();
            int childrenCount          = element.EnumerateObject().Count();

            foreach (TraversalRule rule in args.Rules)
            {
                int childIndex = 0;
                childKeys.Clear();
                foreach (JsonProperty child in element.EnumerateObject())
                {
                    string childElementName = child.Name;
                    string childKey         = (rule.KeyCreator ?? _defaultObjectKeyExtractor).CreateKey(child.Value, childElementName, elementName, childIndex, childrenCount);

                    if (!childKeys.Add(childKey))
                    {
                        // Child key was already used before. Keys should be unique.
                        throw new LocalizationKeyIsNotUniqueException(childKey, args.IdentifierPrefix);
                    }

                    TraversalArgs nextArgs = args;
                    nextArgs.Rules = rule.ChildRules;

                    using IDisposable? loggerScope = _logger.BeginScope(childElementName);
                    TraverseJsonElements(child.Value, childElementName, childKey, nextArgs);
                    childIndex++;
                }
            }
        }
        private void TraverseJsonElements(
            JsonElement element,
            string elementName,
            string localizationKey,
            TraversalArgs args)
        {
            using IDisposable? loggerScope = _logger.BeginScope(elementName);
            List <TraversalRule> complyingRules = args.Rules.Where(r => r.AllowsTraversalOfIdentifier(elementName)).ToList();

            if (complyingRules.Count == 0)
            {
                // This identifier was filtered out.
                _logger.LogDebug(LocalizableStrings.stringExtractor_log_jsonElementExcluded, args.IdentifierPrefix + _keySeparator + elementName);
                return;
            }

            JsonValueKind valueKind = element.ValueKind;

            if (valueKind == JsonValueKind.String)
            {
                ProcessStringElement(element, elementName, localizationKey, args);
                return;
            }

            string newIdentifierPrefix = args.IdentifierPrefix + _keySeparator + elementName;

            if (valueKind == JsonValueKind.Array)
            {
                TraversalArgs newData = args;
                newData.IdentifierPrefix = newIdentifierPrefix;
                newData.Rules            = complyingRules;
                ProcessArrayElement(element, elementName, newData);
                return;
            }

            if (valueKind == JsonValueKind.Object)
            {
                TraversalArgs newData = args;
                newData.IdentifierPrefix = newIdentifierPrefix;
                newData.Rules            = complyingRules;
                newData.KeyPrefix        = args.KeyPrefix + _keySeparator + localizationKey;
                ProcessObjectElement(element, elementName, newData);
            }
        }
        private void ProcessObjectElement(JsonElement element, string elementName, TraversalArgs args)
        {
            foreach (TraversalRule rule in args.Rules)
            {
                int childIndex = 0;
                foreach (JsonProperty child in element.EnumerateObject())
                {
                    string childElementName = child.Name;
                    string childKey         = (rule.KeyCreator ?? _defaultObjectKeyExtractor).CreateKey(child.Value, childElementName, elementName, childIndex);

                    TraversalArgs nextArgs = args;
                    nextArgs.Rules = rule.ChildRules;

                    using IDisposable? loggerScope = _logger.BeginScope(childElementName);
                    TraverseJsonElements(child.Value, childElementName, childKey, nextArgs);
                    childIndex++;
                }
            }
        }
        private void ProcessArrayElement(JsonElement element, string elementName, TraversalArgs args)
        {
            foreach (TraversalRule rule in args.Rules)
            {
                int childIndex = 0;
                foreach (var child in element.EnumerateArray())
                {
                    string childElementName = childIndex.ToString();
                    string?childKey         = (rule.KeyCreator ?? _defaultArrayKeyExtractor).CreateKey(child, childElementName, elementName, childIndex);

                    TraversalArgs nextArgs = args;
                    nextArgs.Rules = rule.ChildRules;

                    using IDisposable? loggerScope = _logger.BeginScope(childElementName);
                    TraverseJsonElements(child, childElementName, childKey, nextArgs);
                    childIndex++;
                }
            }
        }
        private void ProcessStringElement(JsonElement element, string elementName, string key, TraversalArgs data)
        {
            string identifier = (data.IdentifierPrefix + _keySeparator + elementName).ToLowerInvariant();

            if (data.ExtractedStringIds.Contains(identifier))
            {
                // This string was already included by an earlier rule, possibly with a different key. Skip.
                _logger.LogDebug(LocalizableStrings.stringExtractor_log_skippingAlreadyAddedElement, identifier);
                return;
            }

            string finalKey = data.KeyPrefix == null ? key : (data.KeyPrefix + _keySeparator + key);

            if (finalKey.StartsWith(_keySeparator + string.Empty + _keySeparator))
            {
                // Omit the dots generated by the root element and the initial empty prefix.
                finalKey = finalKey.Substring(2);
            }

            data.ExtractedStringIds.Add(identifier);
            data.ExtractedStrings.Add(new TemplateString(identifier, finalKey, element.GetString() ?? string.Empty));
            _logger.LogTrace(LocalizableStrings.stringExtractor_log_jsonElementAdded, identifier);
        }