main
cs 342 lines 15.3 KB
Raw
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 }