// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
namespace WixToolset.Core
{
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using WixToolset.Core.Native;
using WixToolset.Data;
using WixToolset.Data.Bind;
using WixToolset.Extensibility;
using WixToolset.Extensibility.Services;
///
/// Parses localization files and localizes database values.
///
public sealed class Localizer : ILocalizer
{
public static readonly XNamespace WxlNamespace = "http://wixtoolset.org/schemas/v4/wxl";
private static string XmlElementName = "WixLocalization";
private Dictionary variables;
private Dictionary localizedControls;
///
/// Instantiate a new Localizer.
///
public Localizer(IMessaging messaging, IEnumerable localizations)
{
this.Codepage = -1;
this.variables = new Dictionary();
this.localizedControls = new Dictionary();
foreach (var localization in localizations)
{
if (-1 == this.Codepage)
{
this.Codepage = localization.Codepage;
}
foreach (var variable in localization.Variables)
{
Localizer.AddWixVariable(messaging, this.variables, variable);
}
foreach (KeyValuePair localizedControl in localization.LocalizedControls)
{
if (!this.localizedControls.ContainsKey(localizedControl.Key))
{
this.localizedControls.Add(localizedControl.Key, localizedControl.Value);
}
}
}
}
///
/// Gets the codepage.
///
/// The codepage.
public int Codepage { get; }
///
/// Get a localized data value.
///
/// The name of the localization variable.
/// The localized data value or null if it wasn't found.
public string GetLocalizedValue(string id)
{
return this.variables.TryGetValue(id, out var wixVariableRow) ? wixVariableRow.Value : null;
}
///
/// Get a localized control.
///
/// The optional id of the control's dialog.
/// The id of the control.
/// The localized control or null if it wasn't found.
public LocalizedControl GetLocalizedControl(string dialog, string control)
{
return this.localizedControls.TryGetValue(LocalizedControl.GetKey(dialog, control), out var localizedControl) ? localizedControl : null;
}
///
/// Loads a localization file from a path on disk.
///
/// Path to library file saved on disk.
/// Collection containing TableDefinitions to use when loading the localization file.
/// Suppress xml schema validation while loading.
/// Returns the loaded localization file.
public static Localization ParseLocalizationFile(IMessaging messaging, string path)
{
XElement root = XDocument.Load(path).Root;
Localization localization = null;
SourceLineNumber sourceLineNumbers = SourceLineNumber.CreateFromXObject(root);
if (Localizer.XmlElementName == root.Name.LocalName)
{
if (Localizer.WxlNamespace == root.Name.Namespace)
{
localization = ParseWixLocalizationElement(messaging, root);
}
else // invalid or missing namespace
{
if (null == root.Name.Namespace)
{
messaging.Write(ErrorMessages.InvalidWixXmlNamespace(sourceLineNumbers, Localizer.XmlElementName, Localizer.WxlNamespace.NamespaceName));
}
else
{
messaging.Write(ErrorMessages.InvalidWixXmlNamespace(sourceLineNumbers, Localizer.XmlElementName, root.Name.LocalName, Localizer.WxlNamespace.NamespaceName));
}
}
}
else
{
messaging.Write(ErrorMessages.InvalidDocumentElement(sourceLineNumbers, root.Name.LocalName, "localization", Localizer.XmlElementName));
}
return localization;
}
///
/// Adds a WixVariableRow to a dictionary while performing the expected override checks.
///
/// Dictionary of variable rows.
/// Row to add to the variables dictionary.
private static void AddWixVariable(IMessaging messaging, IDictionary variables, BindVariable wixVariableRow)
{
if (!variables.TryGetValue(wixVariableRow.Id, out var existingWixVariableRow) || (existingWixVariableRow.Overridable && !wixVariableRow.Overridable))
{
variables[wixVariableRow.Id] = wixVariableRow;
}
else if (!wixVariableRow.Overridable)
{
messaging.Write(ErrorMessages.DuplicateLocalizationIdentifier(wixVariableRow.SourceLineNumbers, wixVariableRow.Id));
}
}
///
/// Parses the WixLocalization element.
///
/// Element to parse.
private static Localization ParseWixLocalizationElement(IMessaging messaging, XElement node)
{
int codepage = -1;
string culture = null;
SourceLineNumber sourceLineNumbers = SourceLineNumber.CreateFromXObject(node);
foreach (XAttribute attrib in node.Attributes())
{
if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || Localizer.WxlNamespace == attrib.Name.Namespace)
{
switch (attrib.Name.LocalName)
{
case "Codepage":
codepage = Common.GetValidCodePage(attrib.Value, true, false, sourceLineNumbers);
break;
case "Culture":
culture = attrib.Value;
break;
case "Language":
// do nothing; @Language is used for locutil which can't convert Culture to lcid
break;
default:
Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib);
break;
}
}
else
{
Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib);
}
}
Dictionary variables = new Dictionary();
Dictionary localizedControls = new Dictionary();
foreach (XElement child in node.Elements())
{
if (Localizer.WxlNamespace == child.Name.Namespace)
{
switch (child.Name.LocalName)
{
case "String":
Localizer.ParseString(messaging, child, variables);
break;
case "UI":
Localizer.ParseUI(messaging, child, localizedControls);
break;
default:
messaging.Write(ErrorMessages.UnexpectedElement(sourceLineNumbers, node.Name.ToString(), child.Name.ToString()));
break;
}
}
else
{
messaging.Write(ErrorMessages.UnsupportedExtensionElement(sourceLineNumbers, node.Name.ToString(), child.Name.ToString()));
}
}
return messaging.EncounteredError ? null : new Localization(codepage, culture, variables, localizedControls);
}
///
/// Parse a localization string into a WixVariableRow.
///
/// Element to parse.
private static void ParseString(IMessaging messaging, XElement node, IDictionary variables)
{
string id = null;
bool overridable = false;
SourceLineNumber sourceLineNumbers = SourceLineNumber.CreateFromXObject(node);
foreach (XAttribute attrib in node.Attributes())
{
if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || Localizer.WxlNamespace == attrib.Name.Namespace)
{
switch (attrib.Name.LocalName)
{
case "Id":
id = Common.GetAttributeIdentifierValue(messaging, sourceLineNumbers, attrib);
break;
case "Overridable":
overridable = YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib);
break;
case "Localizable":
; // do nothing
break;
default:
messaging.Write(ErrorMessages.UnexpectedAttribute(sourceLineNumbers, attrib.Parent.Name.ToString(), attrib.Name.ToString()));
break;
}
}
else
{
messaging.Write(ErrorMessages.UnsupportedExtensionAttribute(sourceLineNumbers, attrib.Parent.Name.ToString(), attrib.Name.ToString()));
}
}
string value = Common.GetInnerText(node);
if (null == id)
{
messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, "String", "Id"));
}
else if (0 == id.Length)
{
messaging.Write(ErrorMessages.IllegalIdentifier(sourceLineNumbers, "String", "Id", 0));
}
if (!messaging.EncounteredError)
{
var variable = new BindVariable
{
SourceLineNumbers = sourceLineNumbers,
Id = id,
Overridable = overridable,
Value = value,
};
Localizer.AddWixVariable(messaging, variables, variable);
}
}
///
/// Parse a localized control.
///
/// Element to parse.
/// Dictionary of localized controls.
private static void ParseUI(IMessaging messaging, XElement node, IDictionary localizedControls)
{
string dialog = null;
string control = null;
int x = CompilerConstants.IntegerNotSet;
int y = CompilerConstants.IntegerNotSet;
int width = CompilerConstants.IntegerNotSet;
int height = CompilerConstants.IntegerNotSet;
int attribs = 0;
string text = null;
SourceLineNumber sourceLineNumbers = SourceLineNumber.CreateFromXObject(node);
foreach (XAttribute attrib in node.Attributes())
{
if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || Localizer.WxlNamespace == attrib.Name.Namespace)
{
switch (attrib.Name.LocalName)
{
case "Dialog":
dialog = Common.GetAttributeIdentifierValue(messaging, sourceLineNumbers, attrib);
break;
case "Control":
control = Common.GetAttributeIdentifierValue(messaging, sourceLineNumbers, attrib);
break;
case "X":
x = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, short.MaxValue);
break;
case "Y":
y = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, short.MaxValue);
break;
case "Width":
width = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, short.MaxValue);
break;
case "Height":
height = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, short.MaxValue);
break;
case "RightToLeft":
if (YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib))
{
attribs |= MsiInterop.MsidbControlAttributesRTLRO;
}
break;
case "RightAligned":
if (YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib))
{
attribs |= MsiInterop.MsidbControlAttributesRightAligned;
}
break;
case "LeftScroll":
if (YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib))
{
attribs |= MsiInterop.MsidbControlAttributesLeftScroll;
}
break;
default:
Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib);
break;
}
}
else
{
Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib);
}
}
text = Common.GetInnerText(node);
if (String.IsNullOrEmpty(control) && 0 < attribs)
{
if (MsiInterop.MsidbControlAttributesRTLRO == (attribs & MsiInterop.MsidbControlAttributesRTLRO))
{
messaging.Write(ErrorMessages.IllegalAttributeWithoutOtherAttributes(sourceLineNumbers, node.Name.ToString(), "RightToLeft", "Control"));
}
else if (MsiInterop.MsidbControlAttributesRightAligned == (attribs & MsiInterop.MsidbControlAttributesRightAligned))
{
messaging.Write(ErrorMessages.IllegalAttributeWithoutOtherAttributes(sourceLineNumbers, node.Name.ToString(), "RightAligned", "Control"));
}
else if (MsiInterop.MsidbControlAttributesLeftScroll == (attribs & MsiInterop.MsidbControlAttributesLeftScroll))
{
messaging.Write(ErrorMessages.IllegalAttributeWithoutOtherAttributes(sourceLineNumbers, node.Name.ToString(), "LeftScroll", "Control"));
}
}
if (String.IsNullOrEmpty(control) && String.IsNullOrEmpty(dialog))
{
messaging.Write(ErrorMessages.ExpectedAttributesWithOtherAttribute(sourceLineNumbers, node.Name.ToString(), "Dialog", "Control"));
}
if (!messaging.EncounteredError)
{
LocalizedControl localizedControl = new LocalizedControl(dialog, control, x, y, width, height, attribs, text);
string key = localizedControl.GetKey();
if (localizedControls.ContainsKey(key))
{
if (String.IsNullOrEmpty(localizedControl.Control))
{
messaging.Write(ErrorMessages.DuplicatedUiLocalization(sourceLineNumbers, localizedControl.Dialog));
}
else
{
messaging.Write(ErrorMessages.DuplicatedUiLocalization(sourceLineNumbers, localizedControl.Dialog, localizedControl.Control));
}
}
else
{
localizedControls.Add(key, localizedControl);
}
}
}
}
}