File: MS\Internal\Globalization\LocalizableResourceBuilder.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
 
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Markup.Localizer;
using System.Diagnostics;
using System.Text;
 
namespace MS.Internal.Globalization
{
    ///<summary>
    /// this class is builds the BamlLocalizableResources
    /// it handles all the localizability attribute reading, and inheritance resolution in the tree.
    ///</summary>
    internal sealed class LocalizableResourceBuilder
    {
        internal LocalizableResourceBuilder(InternalBamlLocalizabilityResolver resolver)
        {
            _resolver = resolver;
        }
 
        /// <summary>
        /// build a localizable resource from a baml tree node
        /// </summary>
        internal BamlLocalizableResource BuildFromNode(BamlLocalizableResourceKey key, BamlTreeNode node)
        {
            if (node.Formatted)
            {
                // the content of the node has been formatted to be part of
                // parents' content, so no need to create a seperate entry for the
                // element
                return null;
            }
 
            BamlLocalizableResource resource = null;
            LocalizabilityAttribute localizability = null;
            string formattingTag;
 
            //
            // variable controling what comments gets applied
            //
            BamlStartElementNode commentNode = null;  // node containing comment
            string commentTargetName = null;  // the target of the comment, e.g. $Content, FontSize, etc.
 
            //
            // step 1: Get the localizability attribute from the source files
            //
            switch (node.NodeType)
            {
                case BamlNodeType.StartElement:
                    {
                        // For element
                        commentNode = (BamlStartElementNode)node;
                        GetLocalizabilityForElementNode(commentNode, out localizability, out formattingTag);
                        commentTargetName = BamlConst.ContentSuffix;
                        break;
                    }
                case BamlNodeType.LiteralContent:
                    {
                        // For literal content, get the attribute from parent element
                        GetLocalizabilityForElementNode((BamlStartElementNode)node.Parent, out localizability, out formattingTag);
 
                        commentNode = (BamlStartElementNode)node.Parent;
                        commentTargetName = BamlConst.ContentSuffix;
                        break;
                    }
 
                case BamlNodeType.Property:
                    {
                        BamlStartComplexPropertyNode propertyNode = (BamlStartComplexPropertyNode)node;
                        if (LocComments.IsLocCommentsProperty(propertyNode.OwnerTypeFullName, propertyNode.PropertyName)
                           || LocComments.IsLocLocalizabilityProperty(propertyNode.OwnerTypeFullName, propertyNode.PropertyName)
                           )
                        {
                            // skip Localization.Comments and Localization.Attributes properties. They aren't localizable
                            return null;
                        }
 
                        // For property
                        GetLocalizabilityForPropertyNode(propertyNode, out localizability);
 
                        commentTargetName = propertyNode.PropertyName;
                        commentNode = (BamlStartElementNode)node.Parent;
                        break;
                    }
                default:
                    {
                        Invariant.Assert(false); // no localizable resource for such node
                        break;
                    }
            }
 
            //
            // Step 2: Find out the inheritance value
            //
 
            // The node participates in localizability inheritance
            // let's fill things in
            localizability = CombineAndPropagateInheritanceValues(
                node as ILocalizabilityInheritable,
                localizability
                );
 
            //
            // Step 3: We finalized the localizability values. We now apply.
            //
            string content = null;
 
            if (localizability.Category != LocalizationCategory.NeverLocalize
             && localizability.Category != LocalizationCategory.Ignore
             && TryGetContent(key, node, out content))
            {
                // we only create one if it is localizable
                resource = new BamlLocalizableResource();
                resource.Readable = (localizability.Readability == Readability.Readable);
                resource.Modifiable = (localizability.Modifiability == Modifiability.Modifiable);
                resource.Category = localizability.Category;
                // continue to fill in content.
                resource.Content = content;
                resource.Comments = _resolver.GetStringComment(commentNode, commentTargetName);
            }
 
            // return the resource
            return resource;
        }
 
        /// <summary>
        /// This builds the localizable string from the baml tree node
        /// </summary>
        /// <return>
        /// return true when the node has valid localizable content, false otherwise.
        /// </return>
        internal bool TryGetContent(BamlLocalizableResourceKey key, BamlTreeNode currentNode, out string content)
        {
            content = string.Empty;
 
            switch (currentNode.NodeType)
            {
                case BamlNodeType.Property:
                    {
                        bool isValidContent = true;
                        BamlPropertyNode propertyNode = (BamlPropertyNode)currentNode;
                        content = BamlResourceContentUtil.EscapeString(propertyNode.Value);
 
                        //
                        // Markup extensions are not localizable values, e.g. {x:Type SolidColorBrush}.
                        // So if the string can be parsed as Markup extensions, we will exclude it unless
                        // the user sets localization comments explicitly to localize this value.
                        //
                        string typeName, args;
                        string tempContent = content;
                        if (MarkupExtensionParser.GetMarkupExtensionTypeAndArgs(ref tempContent, out typeName, out args))
                        {
                            // See if this value has been marked as localizable explicitly in comments
                            LocalizabilityGroup localizability = _resolver.GetLocalizabilityComment(
                                propertyNode.Parent as BamlStartElementNode,
                                propertyNode.PropertyName
                                );
 
                            isValidContent = (localizability != null && localizability.Readability == Readability.Readable);
                        }
                        return isValidContent;
                    }
                case BamlNodeType.LiteralContent:
                    {
                        content = BamlResourceContentUtil.EscapeString(
                            ((BamlLiteralContentNode)currentNode).Content
                            );
                        return true; // succeed
                    }
                case BamlNodeType.StartElement:
                    {
                        BamlStartElementNode elementNode = (BamlStartElementNode)currentNode;
                        if (elementNode.Content == null)
                        {
                            StringBuilder contentBuilder = new StringBuilder();
                            foreach (BamlTreeNode child in elementNode.Children)
                            {
                                // we only format element and text inline
                                // other nodes like property node we don't put them into the content of the element
                                switch (child.NodeType)
                                {
                                    case BamlNodeType.StartElement:
                                        {
                                            string childContent;
                                            if (TryFormatElementContent(key, (BamlStartElementNode)child, out childContent))
                                            {
                                                contentBuilder.Append(childContent);
                                            }
                                            else
                                            {
                                                return false; // failed to get content for children element
                                            }
 
                                            break;
                                        }
                                    case BamlNodeType.Text:
                                        {
                                            contentBuilder.Append(BamlResourceContentUtil.EscapeString(
                                                ((BamlTextNode)child).Content)
                                                );
                                            break;
                                        }
                                }
                            }
 
                            elementNode.Content = contentBuilder.ToString();
                        }
 
                        content = elementNode.Content;
                        return true;
                    }
                default:
                    return true;
            }
        }
 
        private bool TryFormatElementContent(
            BamlLocalizableResourceKey key,
            BamlStartElementNode node,
            out string content
            )
        {
            content = string.Empty;
 
            string formattingTag;
            LocalizabilityAttribute attribute;
            GetLocalizabilityForElementNode(node, out attribute, out formattingTag);
            attribute = CombineAndPropagateInheritanceValues(node, attribute);
 
            if (formattingTag != null
              && attribute.Category != LocalizationCategory.NeverLocalize
              && attribute.Category != LocalizationCategory.Ignore
              && attribute.Modifiability == Modifiability.Modifiable
              && attribute.Readability == Readability.Readable
              )
            {
                // this node should be formatted inline
                StringBuilder contentBuilder = new StringBuilder();
 
                // write opening tag
                if (node.Uid != null)
                {
                    contentBuilder.Append(
                        TypeConverterHelper.InvariantEnglishUS,
                        $"<{formattingTag} {XamlReaderHelper.DefinitionUid}=\"{BamlResourceContentUtil.EscapeString(node.Uid)}\">"
                    );
                }
                else
                {
                    contentBuilder.Append(TypeConverterHelper.InvariantEnglishUS, $"<{formattingTag}>");
                }
 
                // recurisively call down to format the content
                string childContent;
                bool succeed = TryGetContent(key, node, out childContent);
 
                if (succeed)
                {
                    contentBuilder.Append(childContent);
 
                    // write closing tag
                    contentBuilder.Append(TypeConverterHelper.InvariantEnglishUS, $"</{formattingTag}>");
 
                    // remeber that we format this element so that we don't format the value again.
                    // e.g. <Text x:Uid="t"> <Bold x:Uid="b"> ... </Bold> </Text>
                    // if <Bold> is already inlined in Text element's contennt, we don't need to
                    // have a seperate entry for <Bold> anymore
                    node.Formatted = true;
                    content = contentBuilder.ToString();
                }
 
                return succeed;
            }
            else
            {
                // this node should be represented by place holder.
                bool succeed = true;
 
                if (node.Uid != null)
                {
                    content = string.Create(
                        TypeConverterHelper.InvariantEnglishUS,
                        $"{BamlConst.ChildStart}{BamlResourceContentUtil.EscapeString(node.Uid)}{BamlConst.ChildEnd}");
                }
                else
                {
                    // we want to enforce the rule that all children element
                    // must have UID defined.
                    _resolver.RaiseErrorNotifyEvent(
                        new BamlLocalizerErrorNotifyEventArgs(
                            key,
                            BamlLocalizerError.UidMissingOnChildElement
                            )
                    );
                    succeed = false; // failed
                }
 
                return succeed;
            }
        }
 
 
        private void GetLocalizabilityForElementNode(
            BamlStartElementNode node,
            out LocalizabilityAttribute localizability,
            out string formattingTag
            )
        {
            localizability = null;
            formattingTag = null;
 
            // get the names we need
            string assemblyName = node.AssemblyName;
            string className = node.TypeFullName;
 
            // query the resolver
            ElementLocalizability result = _resolver.GetElementLocalizability(
                 assemblyName,
                 className
                 );
 
            LocalizabilityGroup comment = null;
            comment = _resolver.GetLocalizabilityComment(node, BamlConst.ContentSuffix);
 
            if (comment != null)
            {
                localizability = comment.Override(result.Attribute);
            }
            else
            {
                localizability = result.Attribute;
            }
 
            formattingTag = result.FormattingTag;
        }
 
 
        private void GetLocalizabilityForPropertyNode(
            BamlStartComplexPropertyNode node,
            out LocalizabilityAttribute localizability
            )
        {
            localizability = null;
 
            string assemblyName = node.AssemblyName;
            string className = node.OwnerTypeFullName;
            string propertyLocalName = node.PropertyName;
 
            if (className == null || className.Length == 0)
            {
                // class name can be empty or null. For example, <Set PropertyPath="...">
                // We will use the parent node's value.
                string formattingTag;
                GetLocalizabilityForElementNode((BamlStartElementNode)node.Parent, out localizability, out formattingTag);
                return;
            }
 
            LocalizabilityGroup comment = _resolver.GetLocalizabilityComment(
                    (BamlStartElementNode)node.Parent,
                    node.PropertyName
                    );
 
            localizability = _resolver.GetPropertyLocalizability(
                    assemblyName,
                    className,
                    propertyLocalName
                    );
 
            if (comment != null)
            {
                localizability = comment.Override(localizability);
            }
        }
 
        /// <summary>
        /// Combine inheritable attributes, and propegate it down the tree.
        /// </summary>
        /// <param name="node">current node</param>
        /// <param name="localizabilityFromSource">localizability defined in source code</param>
        /// <returns>
        /// The LocalizabilityAttribute to used for this node. It is not the same as the
        /// inheritable attributes of the node when the node is set to Ignore.
        /// </returns>
        /// <remarks>We always walk the baml tree in depth-first order</remarks>
        private LocalizabilityAttribute CombineAndPropagateInheritanceValues(
            ILocalizabilityInheritable node,
            LocalizabilityAttribute localizabilityFromSource
            )
        {
            if (node == null)
            {
                return localizabilityFromSource;
            }
 
            // If this node's inheritable localizability has been constructed, we can skip it
            // This can happen when recursively format the content
            if (node.InheritableAttribute != null)
            {
                return (!node.IsIgnored) ? node.InheritableAttribute : LocalizabilityIgnore;
            }
 
            // To test wether the current node needs to inherit values from parents.
            // It inherits values if:
            // o This node is set to Ignore, in which case it propagates parent values.
            // o Some of its attributes set to Inherit.
            if (localizabilityFromSource.Category != LocalizationCategory.Ignore
              && localizabilityFromSource.Category != LocalizationCategory.Inherit
              && localizabilityFromSource.Readability != Readability.Inherit
              && localizabilityFromSource.Modifiability != Modifiability.Inherit)
            {
                // just return the same one because no value is inherited
                node.InheritableAttribute = localizabilityFromSource;
                return node.InheritableAttribute;
            }
 
            // find the ancestor to inherit values now.
            ILocalizabilityInheritable ancestor = node.LocalizabilityAncestor;
 
            // find out the attribute that is inheritable from above
            LocalizabilityAttribute inheritableAttribute = ancestor.InheritableAttribute;
 
            if (inheritableAttribute == null)
            {
                // if ancestor's inheritable value isn't resolved yet, we recursively
                // resolve it here.
                BamlStartElementNode elementNode = ancestor as BamlStartElementNode;
                if (elementNode != null)
                {
                    string formattingTag;
                    GetLocalizabilityForElementNode(elementNode, out inheritableAttribute, out formattingTag);
                }
                else
                {
                    BamlStartComplexPropertyNode propertyNode = ancestor as BamlStartComplexPropertyNode;
                    GetLocalizabilityForPropertyNode(propertyNode, out inheritableAttribute);
                }
 
                CombineAndPropagateInheritanceValues(ancestor, inheritableAttribute);
 
                inheritableAttribute = ancestor.InheritableAttribute;
                Debug.Assert(inheritableAttribute != null);
            }
 
            // if this item is set to ignore
            if (localizabilityFromSource.Category == LocalizationCategory.Ignore)
            {
                // It propagates ancestor's inheritable localizability, but it will use
                // its own value declared in source.
                // We also mark this node as being "Ignored" in the inheritance tree to signal that
                // this node is not using the inheritance value.
                node.InheritableAttribute = inheritableAttribute;
                node.IsIgnored = true;
                return LocalizabilityIgnore;
            }
 
            // the item is not set to ignore, so we process the inheritable values
            BamlTreeNode treeNode = (BamlTreeNode)node;
            switch (treeNode.NodeType)
            {
                case BamlNodeType.StartElement:
                case BamlNodeType.LiteralContent:
                    {
                        // if everything set to inherit, we just return the inheritable localizability
                        if (localizabilityFromSource.Category == LocalizationCategory.Inherit
                          && localizabilityFromSource.Readability == Readability.Inherit
                          && localizabilityFromSource.Modifiability == Modifiability.Inherit)
                        {
                            // just propagate the ancestor's localizability.
                            node.InheritableAttribute = inheritableAttribute;
                        }
                        else
                        {
                            // set new inherited values
                            node.InheritableAttribute = CreateInheritedLocalizability(
                                localizabilityFromSource,
                                inheritableAttribute
                                );
                        }
                        break;
                    }
                case BamlNodeType.Property:
                case BamlNodeType.StartComplexProperty:
                    {
                        ILocalizabilityInheritable parent = (ILocalizabilityInheritable)treeNode.Parent;
 
                        // Find the mininum localizability of the containing class and
                        // parent property. Parent property means the proeprty from parent node that
                        // has the same name.
                        LocalizabilityAttribute inheritedAttribute = CombineMinimumLocalizability(
                            inheritableAttribute,
                            parent.InheritableAttribute
                            );
 
                        node.InheritableAttribute = CreateInheritedLocalizability(
                            localizabilityFromSource,
                            inheritedAttribute
                            );
 
                        if (parent.IsIgnored && localizabilityFromSource.Category == LocalizationCategory.Inherit)
                        {
                            // If the parent node is Ignore and this property is set to inherit, then
                            // this property node is to be ignore as well. We set the the "Ignore" flag so that
                            // the node will always be ignored without looking at the source localizability again.
                            node.IsIgnored = true;
                            return LocalizabilityIgnore;
                        }
                        break;
                    }
                default:
                    {
                        Debug.Assert(false, "Can't process localizability attribute on nodes other than Element, Property and LiteralContent.");
                        break;
                    }
            }
 
            return node.InheritableAttribute;
        }
 
        /// <summary>
        /// Create the inherited localizability attribute
        /// </summary>
        /// <param name="source">localizability attribute defined in source</param>
        /// <param name="inheritable">localizability attribute inheritable from above</param>
        /// <returns>LocalizabilityAttribute</returns>
        private LocalizabilityAttribute CreateInheritedLocalizability(
            LocalizabilityAttribute source,
            LocalizabilityAttribute inheritable
            )
        {
            LocalizationCategory category =
                (source.Category == LocalizationCategory.Inherit) ?
                inheritable.Category :
                source.Category;
 
            Readability readability =
                (source.Readability == Readability.Inherit) ?
                inheritable.Readability :
                source.Readability;
 
            Modifiability modifiability =
                (source.Modifiability == Modifiability.Inherit) ?
                inheritable.Modifiability :
                source.Modifiability;
 
            LocalizabilityAttribute attribute = new LocalizabilityAttribute(category);
            attribute.Readability = readability;
            attribute.Modifiability = modifiability;
            return attribute;
        }
 
 
        /// <summary>
        /// It combines the min values of two localizability attributes.
        /// </summary>
        /// <param name="first">first </param>
        /// <param name="second">second</param>
        /// <returns>LocalizabilityAttribute</returns>
        private LocalizabilityAttribute CombineMinimumLocalizability(
            LocalizabilityAttribute first,
            LocalizabilityAttribute second
            )
        {
            if (first == null || second == null)
            {
                return (first == null) ? second : first;
            }
 
            // min of two readability enum. The less the more restrictive.
            Readability readability = (Readability)Math.Min(
                (int)first.Readability,
                (int)second.Readability
                );
 
            // min of two Modifiability enum. The less the more restrictive.
            Modifiability modifiability = (Modifiability)Math.Min(
                (int)first.Modifiability,
                (int)second.Modifiability
                );
 
            // for category, NeverLocalize < Ignore < { all others } < None
            // If both categories belong to { all others }, first.Category wins
            LocalizationCategory category = LocalizationCategory.None;
 
            if (first.Category == LocalizationCategory.NeverLocalize
              || second.Category == LocalizationCategory.NeverLocalize)
            {
                category = LocalizationCategory.NeverLocalize;
            }
            else if (first.Category == LocalizationCategory.Ignore
                   || second.Category == LocalizationCategory.Ignore)
            {
                category = LocalizationCategory.Ignore;
            }
            else
            {
                category = (first.Category != LocalizationCategory.None) ?
                    first.Category :
                    second.Category;
            }
 
            LocalizabilityAttribute result = new LocalizabilityAttribute(category);
            result.Readability = readability;
            result.Modifiability = modifiability;
 
            return result;
        }
 
        //--------------------------------
        // private members
        //--------------------------------
        private InternalBamlLocalizabilityResolver _resolver;
        private readonly LocalizabilityAttribute LocalizabilityIgnore = new LocalizabilityAttribute(LocalizationCategory.Ignore);
    }
}