<#@ template language="C#" debug="true" hostspecific="true" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Xml" #> <#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #> <#@ assembly name="EnvDTE" #> <#@ assembly name="EnvDTE80" #> <#@ assembly name="VSLangProj" #> <#@ import namespace="EnvDTE" #> <#@ import namespace="EnvDTE80" #> <#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #> <#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> <#@ import namespace="System" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Xml" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Diagnostics" #> <#@ import namespace="System.Globalization" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Text.RegularExpressions" #> <# /* * T4ResX * Author Robert Hoffmann (itechnology) * License MIT / http://bit.ly/mit-license * * Version 1.00 * https://github.com/itechnology/T4ResX */ #> <# /* CONFIGURATION SETTINGS ************************/ // If you are using this in an assembly where adding a RESX adds a code behind designer.cs file, then be sure to set this to true // Otherwise the designer.cs and the T4ResX.cs will contain the same namespaces and compilation will fail // Classic ASP.NET websites do not generate designer files in the codebehind var removeDesignerFiles = false; /* CONFIGURATION SETTINGS ************************/ /* T4ResX: Find all RESX files * Not alot to see here .. :) * All the important stuff is in T4Helper & T4ResXHelpers * Make this alot easier to maintain and elaborate outside of the .tt environement **********************************************************************************/ T4Helper = new DteHelper(this.Host); var projectItems = T4Helper.GetAllProjectItems(); var project = T4Helper.GetProject(T4Helper.GetCurrentProjectName()); var projectPath = System.IO.Path.GetDirectoryName(project.FileName); var rootNameSpace = project.Properties.Item("RootNamespace").Value.ToString(); var items = new List<T4ResXHelpers.ResXItem>(); projectItems .ToList() .ForEach(projectItem => { var itemPath = T4Helper.GetProjectItemFullPath(projectItem); if (itemPath.EndsWith(".resx")) { if (removeDesignerFiles && !Regex.IsMatch(itemPath, T4ResXHelpers.CultureInvariantRegex, RegexOptions.IgnoreCase)) { T4Helper.SetPropertyValue(projectItem, "CustomTool", ""); } T4ResXHelpers.AddResX(projectPath, rootNameSpace, itemPath, items); } }); #> /* * T4ResX * Author Robert Hoffmann (itechnology) * License MIT / http://bit.ly/mit-license * * Version 1.00 * https://github.com/itechnology/T4ResX */ using System; using System.Linq; using System.Threading; using System.Reflection; using System.Collections.Generic; using System.Text.RegularExpressions; namespace <#= rootNameSpace #> { /// <summary> /// Class that contains all our little helper functions /// </summary> public static class Utilities { /// <summary> /// A fake attribute that allows us to filter classes by Attribute /// It's helpfull when using GetResourcesByNameSpace(), and when T4ResX is tossed into a project containing other classes/properties /// Like this we only return stuff generated by T4ResX itself /// </summary> public class Localized : Attribute {} ///<summary> /// We bind this function to our replacement function when needed /// Like this the replacement function can reside in any assembly you like /// Bind it once on ApplicationStart, or rebind it to a different replacement function before calling it /// /// Poor Man's IOC /// http://www.i-technology.net ///</summary> public static Func<string, string> GetReplacementString = key => key; #region Methods /// <summary> /// Look up ressources from a specific namespace /// </summary> /// <param name="ns">Namspace to get resources from</param> /// <returns>Dictionary<namespace, Dictionary<key, value>></returns> public static Dictionary<string, Dictionary<string, string>> GetResourcesByNameSpace(string ns) { var result = new Dictionary<string, Dictionary<string, string>>(); var qs = ns.Split('^'); ns = qs[0]; var path = string.IsNullOrEmpty(ns) ? "<#= rootNameSpace #>" : string.Format("{0}.{1}", "<#= rootNameSpace #>", ns); var wCard = path; if (ns.EndsWith(".*")) { wCard = path.Replace(".*", ""); } var current = Assembly.GetExecutingAssembly(); current .GetTypes() .Where(type => type.GetCustomAttributes(typeof(Localized), false).Length != 0) .Where(type => type.Namespace != null && (ns == "" || (ns.EndsWith(".*") ? type.Namespace.StartsWith(wCard, StringComparison.InvariantCultureIgnoreCase) : string.Equals(type.Namespace, path, StringComparison.InvariantCultureIgnoreCase))) ) .Where(type => qs.Length != 2 || Regex.IsMatch(type.Name, qs[1], RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline)) .ToList() .ForEach(typeFound => { var instance = current.CreateInstance(typeFound.FullName); if (instance != null) { var instanceType = instance.GetType(); var instanceClass = instanceType.FullName.Replace(wCard, ""); var propertyList = new Dictionary<string, string>(); instanceType .GetProperties() .Where(t => t.GetCustomAttributes(typeof(Localized), false).Length != 0) .ToList() .ForEach(property => propertyList.Add(property.Name, property.GetValue(null, null).ToString())); if (propertyList.Count > 0) { result.Add(instanceClass.StartsWith(".") ? instanceClass.Substring(1) : instanceClass, propertyList); } } }); return result; } #endregion } } <# var currentClassName = string.Empty; var writeClassHeader = false; // We only want culture invariant entries for now var resxItems = items.Where(c => string.IsNullOrEmpty(c.Culture)); foreach (T4ResXHelpers.ResXItem item in resxItems) { // Since we are lazy writing everything below with partial classes, we need to know when we can or cannot add the ResourceManager if (string.Equals(currentClassName, item.ClassName)) { writeClassHeader = false; } else { writeClassHeader = true; currentClassName = item.ClassName; } #> namespace <#= T4ResXHelpers.Current.NormalizeString(item.NameSpace)#> { <# if (writeClassHeader){ Write("[Utilities.Localized]"); } #> public partial class <#= item.ClassName #> { <# // If we are entering a new class, we must add some functions to the top of it if (writeClassHeader) { var path = T4ResXHelpers.Current.NormalizeString(item.NameSpace.Replace(rootNameSpace, "")) + "^" + item.ClassName; if (path.StartsWith(".")) { path = path.Substring(1); } #> ///<summary> /// Return this class as a Dictionary<class, Dictionary<key, value>> ///</summary> public static Dictionary<string, Dictionary<string, string>> GetAsDictionary() { return Utilities.GetResourcesByNameSpace("<#=path#>"); } private static System.Resources.ResourceManager _resourceManager; ///<summary> /// Get the ResourceManager ///</summary> private static System.Resources.ResourceManager ResourceManager { get { return _resourceManager ?? (_resourceManager = new System.Resources.ResourceManager("<#=T4ResXHelpers.Current.NormalizeString(item.NameSpace)#>.<#=item.ClassName#>", typeof(<#=item.ClassName#>).Assembly)); } } ///<summary> /// Get localized entry for a given key ///</summary> public static string GetResourceString(string key, params object[] args) { var value = ResourceManager.GetString(key, Thread.CurrentThread.CurrentUICulture); if (!string.IsNullOrEmpty(value)) { var regex = @"{\b\p{Lu}{3,}\b}"; var tokens = Regex.Matches(value, regex).Cast<Match>().Select(m => m.Value).ToList(); tokens .ForEach(t => { value = value.Replace(t, Utilities.GetReplacementString(t.Replace("{", "").Replace("}", ""))); }); if (args.Any()) { regex = @"{[0-9]{1}}"; tokens = Regex.Matches(value, regex).Cast<Match>().Select(m => m.Value).ToList(); if (tokens.Any()) { // If argument length is less than token length, add an error message // This can happen if arguments are accidentally forgottent in a translation if (args.Count() < tokens.Count()) { var newArgs = new List<object>(); for (var i = 0; i < tokens.Count(); i++) { newArgs.Add(args.Length > i ? args[i] : "argument {" + i + "} is undefined"); } args = newArgs.ToArray(); } value = string.Format(value, args); } } } return value; } <# } // END writeClassHeader if (T4ResXHelpers.Current.HasTokens(item.Value)) { #> ///<summary> /// <list type='bullet'> /// <item> /// <description><#= item.Value.Replace("\r", "").Replace("\n", " ")#></description> /// </item> /// <item> /// <description><#= item.Comment.Replace("\r", "").Replace("\n", " ")#></description> /// </item> /// </list> ///</summary> public static string <#= T4ResXHelpers.Current.NormalizeItem(item.Key, false)#>Formatted(params object[] args) { return GetResourceString("<#=T4ResXHelpers.Current.NormalizeItem(item.Key, false)#>", args); } <# } // END HasTokens(item.Value) #> ///<summary> /// <list type='bullet'> /// <item> /// <description><#= item.Value.Replace("\r", "").Replace("\n", " ")#></description> /// </item> /// <item> /// <description><#= item.Comment.Replace("\r", "").Replace("\n", " ")#></description> /// </item> /// </list> ///</summary> [Utilities.Localized] public static string <#= T4ResXHelpers.Current.NormalizeItem(item.Key, false)#> { get { return GetResourceString("<#=item.Key#>"); } } <# if ((T4ResXHelpers.Current.GetType(item.Comment) & T4ResXHelpers.ResxType.Constant) == T4ResXHelpers.ResxType.Constant) { #> ///<summary> /// <list type='bullet'> /// <item> /// <description><#= item.Value.Replace("\r", "").Replace("\n", " ")#></description> /// </item> /// <item> /// <description><#= item.Comment.Replace("\r", "").Replace("\n", " ")#></description> /// </item> /// <item> /// <description> /// There are places where we cannot use strings as they are considered dynamic /// /// [RegularExpressionAttribute(User.PseudoRegexConstant, ErrorMessageResourceName = "PseudoError", ErrorMessageResourceType = typeof(User))] /// /// However: /// constant = no dynamic content /// If you have an idea of how to make constants dynamically localizable, let me know ! /// </description> /// </item> /// </list> ///</summary> public const string <#=T4ResXHelpers.Current.NormalizeItem(item.Key, false)#>Constant = "<#=item.Value.Replace("\r", "").Replace("\n", " ")#>"; <# } // END T4ResXHelpers.Current.GetType(item.Comment) #> } } <# } // END foreach #> <#+ #region I-Technology.NET T4 Helpers /// <summary> /// /// INFO: to be included at the bottom of the T4 file /// </summary> public class T4ResXHelpers { #region Singleton // http://www.yoda.arachsys.com/csharp/singleton.html (Fourth: Simplified) /// <summary> /// Public instance to Helpers /// </summary> public static readonly T4ResXHelpers Current = new T4ResXHelpers(); // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static T4ResXHelpers() { } T4ResXHelpers() { // Eventual init code } #endregion /// <summary> /// Determine the processing level. Not used yet. /// </summary> public enum ProcessLevel { /// <summary> /// Process all files from Folder recursive, in which T4ResX.tt resides /// </summary> Folder, /// <summary> /// Process all files from Project recursive, in which T4ResX.tt resides /// </summary> Project } /// <summary> /// Declared type of entry. Not used yet. /// </summary> [Flags] public enum ResxType { None = 0, Constant = 1 } /// <summary> /// Template used for each RESX item discovered /// </summary> public class ResXItem { public string NameSpace { get; set; } public string ClassName { get; set; } public string Key { get; set; } public string Value { get; set; } public string Comment { get; set; } public string Culture { get; set; } } /// <summary> /// Match files without culture extension /// </summary> public const string CultureInvariantRegex = @".*\.[a-z]{2}(-[a-z]{2})?\.resx$"; /// <summary> /// Finds tokens in the form of {NAME} /// </summary> public const string NamedTokenRegex = @"{\b\p{Lu}{3,}\b}"; /// <summary> /// Finds tokens in the form of {0} /// </summary> public const string ParamTokenRegex = @"{[0-9]{1}}"; /// <summary> /// Finds tokens in the form of {NAME} & {0} /// </summary> public const string AnyTokenRegex = @"{[0-9]{1}}|{\b\p{Lu}{3,}\b}"; /// <summary> /// Get the declared type of an item /// INFO: Currently only constants works. This is open for future ideas. /// </summary> public ResxType GetType(string value) { var result = ResxType.None; if (Regex.IsMatch(value, @"\[type:constant\]")) { result |= ResxType.Constant; } return result; } /// <summary> /// See if the content contains any tokens /// </summary> public bool HasTokens(string content) { return Regex.IsMatch(content, AnyTokenRegex); } ///<summary> /// Reformat a string to various forms ///</summary> public string NormalizeString(string s, bool isClass = true, bool camelCase = true) { return s.Split('.') .Aggregate((c, n) => NormalizeItem(c, isClass, camelCase) + (isClass ? "." : "_") + NormalizeItem(n, isClass, camelCase)); } /// <summary> /// Same as above but single item /// </summary> public string NormalizeItem(string s, bool isClass = true, bool camelCase = true) { s = s.Replace(isClass ? "." : "_", "#"); var r = @"[^\p{L}0-9#]"; var m = Regex.Matches(s, r); foreach (Match match in m) { if (camelCase) { var chars = s.ToCharArray(); chars[match.Index + 1] = char.ToUpper(s[match.Index + 1]); s = new string(chars); } s = s.Remove(match.Index, 1); s = s.Insert(match.Index, "_"); } if (Regex.IsMatch(s, @"^[0-9]")) { s = s.Insert(0, "_"); } return s.Replace("#", isClass ? "." : "_"); } #region ResXLoading public static void AddResX(string projectPath, string rootNameSpace, string itemPath, List<ResXItem> items) { var culture = CultureInfo.InvariantCulture; var file = System.IO.Path.GetFileNameWithoutExtension(itemPath); try { if (file != null) { culture = CultureInfo.CreateSpecificCulture(file.Split('.').Last()); } } catch { culture = CultureInfo.InvariantCulture; } var xml = new XmlDocument(); xml.Load(itemPath); if (xml.DocumentElement != null) { var nodes = xml.DocumentElement.SelectNodes("//data"); if (nodes != null) { foreach (XmlElement element in nodes) { var className = System.IO.Path.GetFileNameWithoutExtension(itemPath); var nameSpace = rootNameSpace + itemPath.Replace(projectPath, "").Replace("\\", ".").Replace("." + className + ".resx", ""); var entry = new ResXItem { ClassName = className, NameSpace = nameSpace, Key = string.Empty, Value = string.Empty, Comment = string.Empty, Culture = culture.Name }; var elementKey = element.Attributes.GetNamedItem("name"); if (elementKey != null) { entry.Key = elementKey.Value ?? string.Empty; } var elementValue = element.SelectSingleNode("value"); if (elementValue != null) { entry.Value = elementValue.InnerText; } var elementComment = element.SelectSingleNode("comment"); if (elementComment != null) { entry.Comment = elementComment.InnerText; } items.Add(entry); } } } } #endregion } #endregion #region TangibleT4 Helpers public DteHelper T4Helper; /// <summary> /// Collection of Visual Studio Automation-Helper methods. /// </summary> /// <returns></returns> public class DteHelper { public DteHelper(object host) { Host = host as ITextTemplatingEngineHost; } public EnvDTE.DTE Dte { get { return GetHostServiceProvider(); } } private readonly ITextTemplatingEngineHost Host; /// Functions requires hostspecific true /// <summary> /// Gets the solution name of the project. /// </summary> public string GetSolutionName() { return Path.GetFileNameWithoutExtension(Dte.Solution.FullName); } public EnvDTE.Project GetProject(string projectName) { return GetAllProjects().First(p => p.Name == projectName); } /// <summary> /// Get all projects of the solution. /// Works not with nested solutions folders. /// </summary> /// <returns></returns> public IEnumerable<EnvDTE.Project> GetAllProjects() { var projectList = new List<EnvDTE.Project>(); var folders = Dte.Solution.Projects.Cast<EnvDTE.Project>().Where(p => p.Kind == EnvDTE80.ProjectKinds.vsProjectKindSolutionFolder); foreach (EnvDTE.Project folder in folders) { if (folder.ProjectItems == null) continue; foreach (EnvDTE.ProjectItem item in folder.ProjectItems) { if (item.Object is EnvDTE.Project) { projectList.Add(item.Object as EnvDTE.Project); } } } var projects = Dte.Solution.Projects.Cast<EnvDTE.Project>().Where(p => p.Kind != EnvDTE80.ProjectKinds.vsProjectKindSolutionFolder); if (projects.Any()) { projectList.AddRange(projects); } return projectList; } public IEnumerable<EnvDTE.ProjectItem> GetAllSolutionItems() { var itemList = new List<EnvDTE.ProjectItem>(); foreach (EnvDTE.Project item in GetAllProjects()) { if (item == null || item.ProjectItems == null) { continue; } itemList.AddRange(GetAllProjectItemsRecursive(item.ProjectItems)); } return itemList; } public IEnumerable<EnvDTE.ProjectItem> GetAllProjectItems() { return GetAllProjectItemsRecursive(GetTemplateAsProjectItem(Dte).ContainingProject.ProjectItems); } public IEnumerable<EnvDTE.ProjectItem> GetAllProjectItemsRecursive(EnvDTE.ProjectItems projectItems) { foreach (EnvDTE.ProjectItem projectItem in projectItems) { if (projectItem.ProjectItems == null) { continue; } foreach (EnvDTE.ProjectItem subItem in GetAllProjectItemsRecursive(projectItem.ProjectItems)) { yield return subItem; } yield return projectItem; } } public string GetProjectItemFullPath(EnvDTE.ProjectItem item) { return item.Properties.Item("FullPath").Value.ToString(); } /// <summary> /// Gets the project name of the active template file. /// </summary> public string GetCurrentProjectName() { return Dte.ActiveDocument.ProjectItem.ContainingProject.Name; } /// <summary> /// Sets the custom tool for generated item. /// <param name="generatedFilename">Filename of the generated item</param> /// <param name="customToolName">The name of the custom tool.</param> /// <example> /// SetCustomToolForGeneratedItem("Resource.resx", "ResXFileCodeGenerator"); /// </example> /// </summary> public void SetCustomToolForGeneratedItem(string generatedFilename, string customToolName) { EnvDTE.ProjectItem subItem = GetTemplateAsProjectItem(Dte).ProjectItems.Cast<EnvDTE.ProjectItem>().First(p => p.Name == generatedFilename); SetPropertyValue(subItem, "CustomTool", customToolName); } /// <summary> /// Sets the custom tool for the first generated item. /// <param name="customToolName">The name of the custom tool.</param> /// <example> /// SetCustomToolForFirstGeneratedItem("ResXFileCodeGenerator"); /// </example> /// </summary> public void SetCustomToolForFirstGeneratedItem(string customToolName) { EnvDTE.ProjectItem firstSubItem = GetTemplateAsProjectItem(Dte).ProjectItems.Cast<EnvDTE.ProjectItem>().First(); SetPropertyValue(firstSubItem, "CustomTool", customToolName); } /// <summary> /// Sets a property value for the project item. /// </summary> public void SetPropertyValue(EnvDTE.ProjectItem item, string propertyName, object value) { EnvDTE.Property property = item.Properties.Item(propertyName); if (property == null) { throw new ArgumentException(String.Format("The property {0} was not found.", propertyName)); } property.Value = value; } /// <summary> /// Gets the T4 template as vs projectitem. /// </summary> public EnvDTE.ProjectItem GetTemplateAsProjectItem(EnvDTE.DTE dte) { return dte.Solution.FindProjectItem(Host.TemplateFile); } /// <summary> /// Adds a missing file to the t4 vs projectitem. /// </summary> public void AddMissingFileToProject(EnvDTE.ProjectItem pItem, string fileName) { var isMissing = !(from itm in pItem.ProjectItems.Cast<EnvDTE.ProjectItem>() where itm.Name == fileName select itm).Any(); if (isMissing) { pItem.ProjectItems.AddFromFile(GetPath(fileName)); } } /// <summary> /// Gets the vs automation object EnvDTE.DTE. /// </summary> public EnvDTE.DTE GetHostServiceProvider() { var hostServiceProvider = Host as IServiceProvider; EnvDTE.DTE dte = null; if (hostServiceProvider != null) { dte = hostServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE; } return dte; } /// <summary> /// Gets the full path of the file. /// </summary> public string GetPath(string fileName) { string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); if (templateDirectory != null) { return Path.Combine(templateDirectory, fileName); } return null; } } #endregion #>