PageRenderTime 58ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 1ms

/extras/ResxCheck/ResxCheck/Program.cs

https://bitbucket.org/tuldok89/openpdn
C# | 386 lines | 306 code | 67 blank | 13 comment | 12 complexity | 79f834c247427596698fab2070eb7aae MD5 | raw file
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Resources;
  9. using System.Text;
  10. using System.Threading;
  11. using System.Xml.Linq;
  12. using System.Xml.XPath;
  13. [assembly: AssemblyTitle("ResxCheck")]
  14. [assembly: AssemblyProduct("ResxCheck")]
  15. [assembly: AssemblyCopyright("Copyright © 2008 dotPDN LLC")]
  16. [assembly: AssemblyVersion("3.30.*")]
  17. [assembly: AssemblyFileVersion("3.30.0.0")]
  18. namespace ResxCheck
  19. {
  20. public enum Warning
  21. {
  22. Duplicate,
  23. Missing,
  24. ExtraFormatTags,
  25. MalformedFormat,
  26. Extra,
  27. }
  28. public static class Extensions
  29. {
  30. public static IEnumerable<T> DoForEach<T>(this IEnumerable<T> source, Action<T> f)
  31. {
  32. foreach (T item in source)
  33. {
  34. f(item);
  35. yield return item;
  36. }
  37. }
  38. public static void Execute<T>(this IEnumerable<T> list)
  39. {
  40. foreach (var item in list)
  41. {
  42. // Nothing. Hopefully you have a Do() clause in there.
  43. }
  44. }
  45. }
  46. class Program
  47. {
  48. static void PrintHeader(TextWriter output)
  49. {
  50. output.WriteLine("ResxCheck v{0}", Assembly.GetExecutingAssembly().GetName().Version);
  51. output.WriteLine("Copyright (C) 2008 dotPDN LLC, http://www.dotpdn.com/");
  52. output.WriteLine();
  53. }
  54. static void PrintUsage(TextWriter output)
  55. {
  56. string ourName = Path.GetFileName(Assembly.GetExecutingAssembly().CodeBase);
  57. output.WriteLine("Usage:");
  58. output.WriteLine(" {0} <base.resx> [[mui1.resx] [mui2.resx] [mui3.resx] ... [muiN.resx]]", ourName);
  59. output.WriteLine();
  60. output.WriteLine("base.resx should be the original resx that is supplied by the developer or design team.");
  61. output.WriteLine();
  62. output.WriteLine("mui1.resx through muiN.resx should be the translated resx files based off of base.resx.");
  63. output.WriteLine("You may specify as many mui resx files as you would like to check.");
  64. output.WriteLine("(You can also not specify any, and then only base.resx will be checked)");
  65. output.WriteLine("TIP: You can specify a wildcard, such as *.resx");
  66. output.WriteLine();
  67. output.WriteLine("This program will check for:");
  68. output.WriteLine(" * base.resx must not have any string defined more than once");
  69. output.WriteLine(" * base.resx must not have any strings with incorrect formatting tags, e.g. having a { but no closing }, or vice versa");
  70. output.WriteLine();
  71. output.WriteLine("If any mui.resx files are specified, then these rules will also be checked:");
  72. output.WriteLine(" * mui.resx must not have any string defined more than once");
  73. output.WriteLine(" * mui.resx must have all the strings that base.resx defines");
  74. output.WriteLine(" * mui.resx must not have any strings defined that are not defined in base.resx");
  75. output.WriteLine(" * mui.resx must not have any strings with incorrect formatting tags, e.g. having a { but no closing }, or vice versa");
  76. output.WriteLine(" * mui.resx must not have any additional formatting tags, e.g. {2}");
  77. output.WriteLine();
  78. output.WriteLine("Examples:");
  79. output.WriteLine();
  80. output.WriteLine(" {0} strings.resx Strings.DE.resx String.IT.resx String.JP.resx", ourName);
  81. output.WriteLine(" This will use strings.resx as the 'base', and then check the DE, IT, and JP translations to ensure they pass the constraints and rules described above.");
  82. output.WriteLine();
  83. output.WriteLine(" {0} strings.resx translations\\*.resx", ourName);
  84. output.WriteLine(" This will use strings.resx as the 'base', and then all of the RESX files found in the translations directory will be validated against it.");
  85. }
  86. delegate void WarnFn(Warning reason, string extraFormat, params object[] extraArgs);
  87. static void Warn(TextWriter output, string name, Warning reason, string extraFormat, params object[] extraArgs)
  88. {
  89. string reasonText;
  90. switch (reason)
  91. {
  92. case Warning.Duplicate:
  93. reasonText = "duplicate string name";
  94. break;
  95. case Warning.Extra:
  96. reasonText = "extra string";
  97. break;
  98. case Warning.ExtraFormatTags:
  99. reasonText = "extra format tags";
  100. break;
  101. case Warning.MalformedFormat:
  102. reasonText = "invalid format";
  103. break;
  104. case Warning.Missing:
  105. reasonText = "missing string";
  106. break;
  107. default:
  108. throw new InvalidEnumArgumentException();
  109. }
  110. output.WriteLine(
  111. "{0}: {1}{2}",
  112. name,
  113. reasonText,
  114. string.IsNullOrEmpty(extraFormat) ?
  115. "" :
  116. string.Format(": {0}", string.Format(extraFormat, extraArgs)));
  117. }
  118. static void AnalyzeSingle(WarnFn warnFn, IEnumerable<IGrouping<string, string>> grouping)
  119. {
  120. // Verify that all strings can have their formatting requirements satisified
  121. grouping.SelectMany(item => item.Select(val => new KeyValuePair<string, string>(item.Key, val)))
  122. .Where(kvPair => CountFormatArgs(kvPair.Value, 100) == -1)
  123. .DoForEach(kvPair => warnFn(Warning.MalformedFormat, "'{0}' = '{1}'", kvPair.Key, kvPair.Value))
  124. .Execute();
  125. // Verify there are no strings defined more than once.
  126. grouping.Where(item => item.Take(2).Count() > 1)
  127. .SelectMany(item => item.Select(val => new KeyValuePair<string, string>(item.Key, val)))
  128. .DoForEach(val => warnFn(Warning.Duplicate, "'{0}' = '{1}'", val.Key, val.Value))
  129. .Execute();
  130. }
  131. static int CountFormatArgs(string formatString)
  132. {
  133. return CountFormatArgs(formatString, 1000);
  134. }
  135. static int CountFormatArgs(string formatString, int max)
  136. {
  137. bool isError;
  138. List<string> argsList = new List<string>();
  139. do
  140. {
  141. string[] args = argsList.ToArray();
  142. try
  143. {
  144. string.Format(formatString, args);
  145. isError = false;
  146. }
  147. catch (Exception)
  148. {
  149. isError = true;
  150. }
  151. argsList.Add("x");
  152. } while (isError && (max == -1 || argsList.Count <= max));
  153. int count = argsList.Count - 1;
  154. if (count == max)
  155. {
  156. return -1;
  157. }
  158. else
  159. {
  160. return argsList.Count - 1;
  161. }
  162. }
  163. static void AnalyzeMuiWithBase(
  164. WarnFn baseWarnFn,
  165. IEnumerable<IGrouping<string, string>> baseList,
  166. WarnFn muiWarnFn,
  167. IEnumerable<IGrouping<string, string>> muiList)
  168. {
  169. AnalyzeSingle(muiWarnFn, muiList);
  170. var baseDict = baseList.ToDictionary(v => v.Key, v => v.First());
  171. var muiDict = muiList.ToDictionary(v => v.Key, v => v.First());
  172. // Verify that mui has everything that base defines
  173. baseDict.Keys.Except(muiDict.Keys)
  174. .DoForEach(key => muiWarnFn(Warning.Missing, key))
  175. .Execute();
  176. // Verify that mui doesn't have any extra entries
  177. muiDict.Keys.Except(baseDict.Keys)
  178. .DoForEach(key => muiWarnFn(Warning.Extra, "'{0}' = '{1}'", key, muiDict[key]))
  179. .Execute();
  180. // Verify that the formatting of the strings works. So if base defines a string
  181. // with {0}, {1} then that must be in the mui string as well.
  182. // To do this, we convert baseList and muiList to a lookup from key -> val1,val2
  183. // Then, we filter to only the items where calling string.Format(val1) raises an exception
  184. // Then, for each of these we determine how many formatting parameters val1 requires,
  185. // and then make sure that val2 does not throw an exception when formatted with that many.
  186. baseDict.Keys
  187. .Where(key => muiDict.ContainsKey(key))
  188. .ToDictionary(key => key, key => new { Base = baseDict[key], Mui = muiDict[key] })
  189. .Where(item => CountFormatArgs(item.Value.Base) != -1)
  190. .Where(item => CountFormatArgs(item.Value.Mui) > CountFormatArgs(item.Value.Base))
  191. .Select(item => item.Key)
  192. .DoForEach(key => muiWarnFn(Warning.ExtraFormatTags, "'{0}' = '{1}'", key, muiDict[key]))
  193. .Execute();
  194. }
  195. static IEnumerable<KeyValuePair<string, string>> FromResX(string resxFileName)
  196. {
  197. XDocument xDoc = XDocument.Load(resxFileName);
  198. var query = from xe in xDoc.XPathSelectElements("/root/data")
  199. let attributes = xe.Attributes()
  200. let name = (from attribute in attributes
  201. where attribute.Name.LocalName == "name"
  202. select attribute.Value)
  203. let elements = xe.Elements()
  204. let value = (from element in elements
  205. where element.Name.LocalName == "value"
  206. select element.Value)
  207. select new KeyValuePair<string, string>(name.First(), value.First());
  208. return query;
  209. }
  210. static T Eval<T>(Func<T> f, T valueIfError)
  211. {
  212. T value;
  213. try
  214. {
  215. value = f();
  216. return value;
  217. }
  218. catch (Exception)
  219. {
  220. return valueIfError;
  221. }
  222. }
  223. static int Main(string[] args)
  224. {
  225. PrintHeader(Console.Out);
  226. if (args.Length < 1)
  227. {
  228. PrintUsage(Console.Out);
  229. return 1;
  230. }
  231. DateTime startTime = DateTime.Now;
  232. Console.WriteLine("--- Start @ {0}", startTime.ToLongTimeString());
  233. string dir = Environment.CurrentDirectory;
  234. string baseName = args[0];
  235. string basePathName = Path.GetFullPath(baseName);
  236. string baseDir = Path.GetDirectoryName(basePathName);
  237. string baseFileName = Path.GetFileName(basePathName);
  238. string baseFileNameNoExt = Path.GetFileNameWithoutExtension(baseFileName);
  239. var baseEnum = FromResX(basePathName);
  240. var baseGrouping = baseEnum.GroupBy(item => item.Key, item => item.Value);
  241. bool anyErrors = false;
  242. WarnFn baseWarnFn = new WarnFn(
  243. (reason, format, formatArgs) =>
  244. {
  245. anyErrors = true;
  246. Warn(Console.Out, baseFileName, reason, format, formatArgs);
  247. });
  248. List<Action> waitActions = new List<Action>();
  249. Action<Action> addWaitAction = a => { waitActions.Add(a); };
  250. ManualResetEvent e0 = new ManualResetEvent(false);
  251. ThreadPool.QueueUserWorkItem(ignored =>
  252. {
  253. Console.WriteLine("Analyzing base {0} ...", baseFileName);
  254. try
  255. {
  256. AnalyzeSingle(baseWarnFn, baseGrouping);
  257. }
  258. catch (Exception ex)
  259. {
  260. Console.WriteLine("{0} : {1}", baseFileName, ex);
  261. }
  262. finally
  263. {
  264. e0.Set();
  265. }
  266. });
  267. addWaitAction(() => e0.WaitOne());
  268. var muiNames = args.Skip(1)
  269. .SelectMany(spec => Eval(() => Directory.GetFiles(dir, spec), new string[0]));
  270. foreach (string muiName in muiNames)
  271. {
  272. string muiPathName = Path.GetFullPath(muiName);
  273. string muiDir = Path.GetDirectoryName(muiPathName);
  274. string muiFileName = Path.GetFileName(muiPathName);
  275. string muiFileNameNoExt = Path.GetFileNameWithoutExtension(muiFileName);
  276. ManualResetEvent eN = new ManualResetEvent(false);
  277. ThreadPool.QueueUserWorkItem(ignored =>
  278. {
  279. try
  280. {
  281. WarnFn muiNWarnFn = new WarnFn(
  282. (reason, format, formatArgs) =>
  283. {
  284. anyErrors = true;
  285. Warn(Console.Out, muiFileName, reason, format, formatArgs);
  286. });
  287. Console.WriteLine("Analyzing mui {0} ...", muiFileName);
  288. var muiEnum = FromResX(muiPathName);
  289. var muiGrouping = muiEnum.GroupBy(item => item.Key, item => item.Value);
  290. AnalyzeMuiWithBase(baseWarnFn, baseGrouping, muiNWarnFn, muiGrouping);
  291. }
  292. catch (Exception ex)
  293. {
  294. Console.WriteLine("{0} : {1}", muiFileName, ex);
  295. }
  296. finally
  297. {
  298. eN.Set();
  299. }
  300. });
  301. addWaitAction(() => eN.WaitOne());
  302. }
  303. foreach (Action waitAction in waitActions)
  304. {
  305. waitAction();
  306. }
  307. DateTime endTime = DateTime.Now;
  308. Console.WriteLine(
  309. "--- End @ {0} ({1} ms), processed {2} resx files",
  310. endTime.ToLongTimeString(),
  311. (endTime - startTime).TotalMilliseconds,
  312. 1 + muiNames.Count());
  313. Console.WriteLine("There were{0} errors", anyErrors ? "" : " no");
  314. return anyErrors ? 1 : 0;
  315. // pause
  316. //Console.Read();
  317. }
  318. }
  319. }