<#@ 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(); 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 #> { /// /// Class that contains all our little helper functions /// public static class Utilities { /// /// 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 /// public class Localized : Attribute {} /// /// 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 /// public static Func GetReplacementString = key => key; #region Methods /// /// Look up ressources from a specific namespace /// /// Namspace to get resources from /// Dictionary<namespace, Dictionary<key, value>> public static Dictionary> GetResourcesByNameSpace(string ns) { var result = new Dictionary>(); 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(); 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); } #> /// /// Return this class as a Dictionary<class, Dictionary<key, value>> /// public static Dictionary> GetAsDictionary() { return Utilities.GetResourcesByNameSpace("<#=path#>"); } private static System.Resources.ResourceManager _resourceManager; /// /// Get the ResourceManager /// 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)); } } /// /// Get localized entry for a given key /// 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().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().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(); 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)) { #> /// /// /// /// <#= item.Value.Replace("\r", "").Replace("\n", " ")#> /// /// /// <#= item.Comment.Replace("\r", "").Replace("\n", " ")#> /// /// /// 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) #> /// /// /// /// <#= item.Value.Replace("\r", "").Replace("\n", " ")#> /// /// /// <#= item.Comment.Replace("\r", "").Replace("\n", " ")#> /// /// /// [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) { #> /// /// /// /// <#= item.Value.Replace("\r", "").Replace("\n", " ")#> /// /// /// <#= item.Comment.Replace("\r", "").Replace("\n", " ")#> /// /// /// /// 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 ! /// /// /// /// 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 /// /// /// INFO: to be included at the bottom of the T4 file /// public class T4ResXHelpers { #region Singleton // http://www.yoda.arachsys.com/csharp/singleton.html (Fourth: Simplified) /// /// Public instance to Helpers /// 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 /// /// Determine the processing level. Not used yet. /// public enum ProcessLevel { /// /// Process all files from Folder recursive, in which T4ResX.tt resides /// Folder, /// /// Process all files from Project recursive, in which T4ResX.tt resides /// Project } /// /// Declared type of entry. Not used yet. /// [Flags] public enum ResxType { None = 0, Constant = 1 } /// /// Template used for each RESX item discovered /// 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; } } /// /// Match files without culture extension /// public const string CultureInvariantRegex = @".*\.[a-z]{2}(-[a-z]{2})?\.resx$"; /// /// Finds tokens in the form of {NAME} /// public const string NamedTokenRegex = @"{\b\p{Lu}{3,}\b}"; /// /// Finds tokens in the form of {0} /// public const string ParamTokenRegex = @"{[0-9]{1}}"; /// /// Finds tokens in the form of {NAME} & {0} /// public const string AnyTokenRegex = @"{[0-9]{1}}|{\b\p{Lu}{3,}\b}"; /// /// Get the declared type of an item /// INFO: Currently only constants works. This is open for future ideas. /// public ResxType GetType(string value) { var result = ResxType.None; if (Regex.IsMatch(value, @"\[type:constant\]")) { result |= ResxType.Constant; } return result; } /// /// See if the content contains any tokens /// public bool HasTokens(string content) { return Regex.IsMatch(content, AnyTokenRegex); } /// /// Reformat a string to various forms /// 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)); } /// /// Same as above but single item /// 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 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; /// /// Collection of Visual Studio Automation-Helper methods. /// /// 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 /// /// Gets the solution name of the project. /// public string GetSolutionName() { return Path.GetFileNameWithoutExtension(Dte.Solution.FullName); } public EnvDTE.Project GetProject(string projectName) { return GetAllProjects().First(p => p.Name == projectName); } /// /// Get all projects of the solution. /// Works not with nested solutions folders. /// /// public IEnumerable GetAllProjects() { var projectList = new List(); var folders = Dte.Solution.Projects.Cast().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().Where(p => p.Kind != EnvDTE80.ProjectKinds.vsProjectKindSolutionFolder); if (projects.Any()) { projectList.AddRange(projects); } return projectList; } public IEnumerable GetAllSolutionItems() { var itemList = new List(); foreach (EnvDTE.Project item in GetAllProjects()) { if (item == null || item.ProjectItems == null) { continue; } itemList.AddRange(GetAllProjectItemsRecursive(item.ProjectItems)); } return itemList; } public IEnumerable GetAllProjectItems() { return GetAllProjectItemsRecursive(GetTemplateAsProjectItem(Dte).ContainingProject.ProjectItems); } public IEnumerable 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(); } /// /// Gets the project name of the active template file. /// public string GetCurrentProjectName() { return Dte.ActiveDocument.ProjectItem.ContainingProject.Name; } /// /// Sets the custom tool for generated item. /// Filename of the generated item /// The name of the custom tool. /// /// SetCustomToolForGeneratedItem("Resource.resx", "ResXFileCodeGenerator"); /// /// public void SetCustomToolForGeneratedItem(string generatedFilename, string customToolName) { EnvDTE.ProjectItem subItem = GetTemplateAsProjectItem(Dte).ProjectItems.Cast().First(p => p.Name == generatedFilename); SetPropertyValue(subItem, "CustomTool", customToolName); } /// /// Sets the custom tool for the first generated item. /// The name of the custom tool. /// /// SetCustomToolForFirstGeneratedItem("ResXFileCodeGenerator"); /// /// public void SetCustomToolForFirstGeneratedItem(string customToolName) { EnvDTE.ProjectItem firstSubItem = GetTemplateAsProjectItem(Dte).ProjectItems.Cast().First(); SetPropertyValue(firstSubItem, "CustomTool", customToolName); } /// /// Sets a property value for the project item. /// 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; } /// /// Gets the T4 template as vs projectitem. /// public EnvDTE.ProjectItem GetTemplateAsProjectItem(EnvDTE.DTE dte) { return dte.Solution.FindProjectItem(Host.TemplateFile); } /// /// Adds a missing file to the t4 vs projectitem. /// public void AddMissingFileToProject(EnvDTE.ProjectItem pItem, string fileName) { var isMissing = !(from itm in pItem.ProjectItems.Cast() where itm.Name == fileName select itm).Any(); if (isMissing) { pItem.ProjectItems.AddFromFile(GetPath(fileName)); } } /// /// Gets the vs automation object EnvDTE.DTE. /// 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; } /// /// Gets the full path of the file. /// public string GetPath(string fileName) { string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); if (templateDirectory != null) { return Path.Combine(templateDirectory, fileName); } return null; } } #endregion #>