| 1 | // 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. |
| 2 | |
| 3 | namespace WixToolset.Core |
| 4 | { |
| 5 | using System; |
| 6 | using System.Collections.Generic; |
| 7 | using System.Xml.Linq; |
| 8 | using WixToolset.Data; |
| 9 | using WixToolset.Data.Bind; |
| 10 | using WixToolset.Extensibility; |
| 11 | using WixToolset.Extensibility.Services; |
| 12 | |
| 13 | internal class LocalizationParser : ILocalizationParser |
| 14 | { |
| 15 | public static readonly XNamespace WxlNamespace = "http://wixtoolset.org/schemas/v4/wxl"; |
| 16 | private const string XmlElementName = "WixLocalization"; |
| 17 | |
| 18 | internal LocalizationParser(IServiceProvider serviceProvider) |
| 19 | { |
| 20 | this.Messaging = serviceProvider.GetService<IMessaging>(); |
| 21 | } |
| 22 | |
| 23 | private IMessaging Messaging { get; } |
| 24 | |
| 25 | public Localization ParseLocalization(string path) |
| 26 | { |
| 27 | var document = XDocument.Load(path); |
| 28 | return this.ParseLocalization(document); |
| 29 | } |
| 30 | |
| 31 | public Localization ParseLocalization(XDocument document) |
| 32 | { |
| 33 | var root = document.Root; |
| 34 | Localization localization = null; |
| 35 | |
| 36 | var sourceLineNumbers = SourceLineNumber.CreateFromXObject(root); |
| 37 | if (LocalizationParser.XmlElementName == root.Name.LocalName) |
| 38 | { |
| 39 | if (LocalizationParser.WxlNamespace == root.Name.Namespace) |
| 40 | { |
| 41 | localization = ParseWixLocalizationElement(this.Messaging, root); |
| 42 | } |
| 43 | else // invalid or missing namespace |
| 44 | { |
| 45 | if (null == root.Name.Namespace) |
| 46 | { |
| 47 | this.Messaging.Write(ErrorMessages.InvalidWixXmlNamespace(sourceLineNumbers, LocalizationParser.XmlElementName, LocalizationParser.WxlNamespace.NamespaceName)); |
| 48 | } |
| 49 | else |
| 50 | { |
| 51 | this.Messaging.Write(ErrorMessages.InvalidWixXmlNamespace(sourceLineNumbers, LocalizationParser.XmlElementName, root.Name.LocalName, LocalizationParser.WxlNamespace.NamespaceName)); |
| 52 | } |
| 53 | } |
| 54 | } |
| 55 | else |
| 56 | { |
| 57 | this.Messaging.Write(ErrorMessages.InvalidDocumentElement(sourceLineNumbers, root.Name.LocalName, "localization", LocalizationParser.XmlElementName)); |
| 58 | } |
| 59 | |
| 60 | return localization; |
| 61 | } |
| 62 | |
| 63 | /// <summary> |
| 64 | /// Adds a WixVariableRow to a dictionary while performing the expected override checks. |
| 65 | /// </summary> |
| 66 | /// <param name="messaging"></param> |
| 67 | /// <param name="variables">Dictionary of variable rows.</param> |
| 68 | /// <param name="wixVariableRow">Row to add to the variables dictionary.</param> |
| 69 | private static void AddWixVariable(IMessaging messaging, IDictionary<string, BindVariable> variables, BindVariable wixVariableRow) |
| 70 | { |
| 71 | if (!variables.TryGetValue(wixVariableRow.Id, out var existingWixVariableRow) || (existingWixVariableRow.Overridable && !wixVariableRow.Overridable)) |
| 72 | { |
| 73 | variables[wixVariableRow.Id] = wixVariableRow; |
| 74 | } |
| 75 | else if (!wixVariableRow.Overridable) |
| 76 | { |
| 77 | messaging.Write(ErrorMessages.DuplicateLocalizationIdentifier(wixVariableRow.SourceLineNumbers, wixVariableRow.Id)); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | /// <summary> |
| 82 | /// Parses the WixLocalization element. |
| 83 | /// </summary> |
| 84 | /// <param name="messaging"></param> |
| 85 | /// <param name="node">Element to parse.</param> |
| 86 | private static Localization ParseWixLocalizationElement(IMessaging messaging, XElement node) |
| 87 | { |
| 88 | var sourceLineNumbers = SourceLineNumber.CreateFromXObject(node); |
| 89 | var location = LocalizationLocation.Source; |
| 90 | int? codepage = null; |
| 91 | int? summaryInformationCodepage = null; |
| 92 | string culture = null; |
| 93 | |
| 94 | foreach (var attrib in node.Attributes()) |
| 95 | { |
| 96 | if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || LocalizationParser.WxlNamespace == attrib.Name.Namespace) |
| 97 | { |
| 98 | switch (attrib.Name.LocalName) |
| 99 | { |
| 100 | case "ExtensionDefaultCulture": |
| 101 | if (Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib) == YesNoType.Yes) |
| 102 | { |
| 103 | location = LocalizationLocation.ExtensionDefaultCulture; |
| 104 | } |
| 105 | break; |
| 106 | |
| 107 | case "Codepage": |
| 108 | codepage = Common.GetValidCodePage(attrib.Value, allowNoChange: true, onlyAnsi: false, sourceLineNumbers); |
| 109 | break; |
| 110 | case "SummaryInformationCodepage": |
| 111 | summaryInformationCodepage = Common.GetValidCodePage(attrib.Value, allowNoChange: true, onlyAnsi: false, sourceLineNumbers); |
| 112 | break; |
| 113 | case "Culture": |
| 114 | culture = attrib.Value; |
| 115 | break; |
| 116 | case "Language": |
| 117 | // do nothing; @Language is used for locutil which can't convert Culture to lcid |
| 118 | break; |
| 119 | default: |
| 120 | Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib); |
| 121 | break; |
| 122 | } |
| 123 | } |
| 124 | else |
| 125 | { |
| 126 | Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib); |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | var variables = new Dictionary<string, BindVariable>(); |
| 131 | var localizedControls = new Dictionary<string, LocalizedControl>(); |
| 132 | |
| 133 | foreach (var child in node.Elements()) |
| 134 | { |
| 135 | if (LocalizationParser.WxlNamespace == child.Name.Namespace) |
| 136 | { |
| 137 | switch (child.Name.LocalName) |
| 138 | { |
| 139 | case "String": |
| 140 | LocalizationParser.ParseString(messaging, child, variables); |
| 141 | break; |
| 142 | |
| 143 | case "UI": |
| 144 | LocalizationParser.ParseUI(messaging, child, localizedControls); |
| 145 | break; |
| 146 | |
| 147 | default: |
| 148 | messaging.Write(ErrorMessages.UnexpectedElement(sourceLineNumbers, node.Name.ToString(), child.Name.ToString())); |
| 149 | break; |
| 150 | } |
| 151 | } |
| 152 | else |
| 153 | { |
| 154 | messaging.Write(ErrorMessages.UnsupportedExtensionElement(sourceLineNumbers, node.Name.ToString(), child.Name.ToString())); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | return messaging.EncounteredError ? null : new Localization(location, codepage, summaryInformationCodepage, culture, variables, localizedControls); |
| 159 | } |
| 160 | |
| 161 | /// <summary> |
| 162 | /// Parse a localization string into a WixVariableRow. |
| 163 | /// </summary> |
| 164 | /// <param name="messaging"></param> |
| 165 | /// <param name="node">Element to parse.</param> |
| 166 | /// <param name="variables"></param> |
| 167 | private static void ParseString(IMessaging messaging, XElement node, IDictionary<string, BindVariable> variables) |
| 168 | { |
| 169 | string id = null; |
| 170 | var overridable = false; |
| 171 | string value = null; |
| 172 | var sourceLineNumbers = SourceLineNumber.CreateFromXObject(node); |
| 173 | |
| 174 | foreach (var attrib in node.Attributes()) |
| 175 | { |
| 176 | if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || LocalizationParser.WxlNamespace == attrib.Name.Namespace) |
| 177 | { |
| 178 | switch (attrib.Name.LocalName) |
| 179 | { |
| 180 | case "Id": |
| 181 | id = Common.GetAttributeIdentifierValue(messaging, sourceLineNumbers, attrib); |
| 182 | break; |
| 183 | case "Overridable": |
| 184 | overridable = YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib); |
| 185 | break; |
| 186 | case "Localizable": |
| 187 | ; // do nothing |
| 188 | break; |
| 189 | case "Value": |
| 190 | value = Common.GetAttributeValue(messaging, sourceLineNumbers, attrib, EmptyRule.CanBeEmpty); |
| 191 | break; |
| 192 | default: |
| 193 | messaging.Write(ErrorMessages.UnexpectedAttribute(sourceLineNumbers, attrib.Parent.Name.ToString(), attrib.Name.ToString())); |
| 194 | break; |
| 195 | } |
| 196 | } |
| 197 | else |
| 198 | { |
| 199 | messaging.Write(ErrorMessages.UnsupportedExtensionAttribute(sourceLineNumbers, attrib.Parent.Name.ToString(), attrib.Name.ToString())); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | if (null == id) |
| 204 | { |
| 205 | messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, "String", "Id")); |
| 206 | } |
| 207 | else if (0 == id.Length) |
| 208 | { |
| 209 | messaging.Write(ErrorMessages.IllegalIdentifier(sourceLineNumbers, "String", "Id", 0)); |
| 210 | } |
| 211 | |
| 212 | Common.InnerTextDisallowed(messaging, node, "Value"); |
| 213 | |
| 214 | if (!messaging.EncounteredError) |
| 215 | { |
| 216 | var variable = new BindVariable |
| 217 | { |
| 218 | SourceLineNumbers = sourceLineNumbers, |
| 219 | Id = id, |
| 220 | Overridable = overridable, |
| 221 | Value = value, |
| 222 | }; |
| 223 | |
| 224 | LocalizationParser.AddWixVariable(messaging, variables, variable); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | /// <summary> |
| 229 | /// Parse a localized control. |
| 230 | /// </summary> |
| 231 | /// <param name="messaging"></param> |
| 232 | /// <param name="node">Element to parse.</param> |
| 233 | /// <param name="localizedControls">Dictionary of localized controls.</param> |
| 234 | private static void ParseUI(IMessaging messaging, XElement node, IDictionary<string, LocalizedControl> localizedControls) |
| 235 | { |
| 236 | string dialog = null; |
| 237 | string control = null; |
| 238 | string text = null; |
| 239 | var x = CompilerConstants.IntegerNotSet; |
| 240 | var y = CompilerConstants.IntegerNotSet; |
| 241 | var width = CompilerConstants.IntegerNotSet; |
| 242 | var height = CompilerConstants.IntegerNotSet; |
| 243 | var sourceLineNumbers = SourceLineNumber.CreateFromXObject(node); |
| 244 | var rightToLeft = false; |
| 245 | var rightAligned = false; |
| 246 | var leftScroll = false; |
| 247 | |
| 248 | foreach (var attrib in node.Attributes()) |
| 249 | { |
| 250 | if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || LocalizationParser.WxlNamespace == attrib.Name.Namespace) |
| 251 | { |
| 252 | switch (attrib.Name.LocalName) |
| 253 | { |
| 254 | case "Dialog": |
| 255 | dialog = Common.GetAttributeIdentifierValue(messaging, sourceLineNumbers, attrib); |
| 256 | break; |
| 257 | case "Control": |
| 258 | control = Common.GetAttributeIdentifierValue(messaging, sourceLineNumbers, attrib); |
| 259 | break; |
| 260 | case "X": |
| 261 | x = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, Int16.MaxValue); |
| 262 | break; |
| 263 | case "Y": |
| 264 | y = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, Int16.MaxValue); |
| 265 | break; |
| 266 | case "Width": |
| 267 | width = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, Int16.MaxValue); |
| 268 | break; |
| 269 | case "Height": |
| 270 | height = Common.GetAttributeIntegerValue(messaging, sourceLineNumbers, attrib, 0, Int16.MaxValue); |
| 271 | break; |
| 272 | case "RightToLeft": |
| 273 | rightToLeft = YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib); |
| 274 | break; |
| 275 | case "RightAligned": |
| 276 | rightAligned = YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib); |
| 277 | break; |
| 278 | case "LeftScroll": |
| 279 | leftScroll = YesNoType.Yes == Common.GetAttributeYesNoValue(messaging, sourceLineNumbers, attrib); |
| 280 | break; |
| 281 | case "Text": |
| 282 | text = Common.GetAttributeValue(messaging, sourceLineNumbers, attrib, EmptyRule.CanBeEmpty); |
| 283 | break; |
| 284 | default: |
| 285 | Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib); |
| 286 | break; |
| 287 | } |
| 288 | } |
| 289 | else |
| 290 | { |
| 291 | Common.UnexpectedAttribute(messaging, sourceLineNumbers, attrib); |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | if (String.IsNullOrEmpty(control) && (rightToLeft || rightAligned || leftScroll)) |
| 296 | { |
| 297 | if (rightToLeft) |
| 298 | { |
| 299 | messaging.Write(ErrorMessages.IllegalAttributeWithoutOtherAttributes(sourceLineNumbers, node.Name.ToString(), "RightToLeft", "Control")); |
| 300 | } |
| 301 | |
| 302 | if (rightAligned) |
| 303 | { |
| 304 | messaging.Write(ErrorMessages.IllegalAttributeWithoutOtherAttributes(sourceLineNumbers, node.Name.ToString(), "RightAligned", "Control")); |
| 305 | } |
| 306 | |
| 307 | if (leftScroll) |
| 308 | { |
| 309 | messaging.Write(ErrorMessages.IllegalAttributeWithoutOtherAttributes(sourceLineNumbers, node.Name.ToString(), "LeftScroll", "Control")); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | if (String.IsNullOrEmpty(control) && String.IsNullOrEmpty(dialog)) |
| 314 | { |
| 315 | messaging.Write(ErrorMessages.ExpectedAttributesWithOtherAttribute(sourceLineNumbers, node.Name.ToString(), "Dialog", "Control")); |
| 316 | } |
| 317 | |
| 318 | Common.InnerTextDisallowed(messaging, node, "Text"); |
| 319 | |
| 320 | if (!messaging.EncounteredError) |
| 321 | { |
| 322 | var localizedControl = new LocalizedControl(dialog, control, x, y, width, height, rightToLeft, rightAligned, leftScroll, text); |
| 323 | var key = localizedControl.GetKey(); |
| 324 | if (localizedControls.ContainsKey(key)) |
| 325 | { |
| 326 | if (String.IsNullOrEmpty(localizedControl.Control)) |
| 327 | { |
| 328 | messaging.Write(ErrorMessages.DuplicatedUiLocalization(sourceLineNumbers, localizedControl.Dialog)); |
| 329 | } |
| 330 | else |
| 331 | { |
| 332 | messaging.Write(ErrorMessages.DuplicatedUiLocalization(sourceLineNumbers, localizedControl.Dialog, localizedControl.Control)); |
| 333 | } |
| 334 | } |
| 335 | else |
| 336 | { |
| 337 | localizedControls.Add(key, localizedControl); |
| 338 | } |
| 339 | } |
| 340 | } |
| 341 | } |
| 342 | } |