【WinRT】使用 T4 模板简化字符串的本地化
在 WinRT 中,对控件、甚至图片资源的本地化都是极其方便的,之前我在博客中也介绍过如何本地化应用名称:http://www.cnblogs.com/h82258652/p/4292157.html
关于 WinRT 中的本地化,CodeProject 上有一篇十分值得大家一看的文章:http://www.codeproject.com/Articles/862152/Localization-in-Windows-Universal-Apps
大家可以去仔细学习一下。
以前的本地化:
说回重点,如果有 Winform、WPF、Windows Phone Silverlight 本地化经验的都知道,打开 Resources.resx 文件,然后填写键值对就可以了(其它语言则在添加相应的 resx 文件,例如美国的就添加 Resources.en-US.resx)。这种方法最大的优点就是,填写的键会生成相应的属性,例如我在 resx 中填写了一个 China 的键,则 resx 文件会生成一个相应的 cs 文件,并且包含这么一段:
然后我们就可以使用:resx的名字.China 来使用相应的本地化的字符串了。
现在 Windows Runtime 的本地化:
在 WinRT 中,本地化很多东西都变得方便了,唯独就是字符串的本地化变麻烦了,资源文件不再给我们生成强类型的属性来访问字符串了,要我们自己来手动写代码来获取每一个键对应的值。
例如我们也在 WinRT 的资源文件——resw 里填写一个 China 的键:
那么在 cs 代码中访问这个 China 就是:
一两个键还好,手动写一下,问题我们的程序不可能就只有那么点需要本地化的字符串,少则几十个,多则上百上千。而且手动编写的话,还会存在拼写错误的情况,最主要的是,根本没法重构!!
解决方案:
那么必然就需要使用一种自动化的,生成代码的技术,这里我们选择 T4 模板来解决这个问题。
首先,我们先来研究下,究竟 resw 是如何存放我们编写好的数据呢?用记事本或者其它文本编辑器打开 resw。
可以看出,本质是一个 XML 文件,并且我们可以轻易看到,我们填写的键值存放在 data 节点中。
假设我们知道这个 resw 文件的路径的话,我们可以编写出如下代码:
1 XmlDocument document = new XmlDocument(); 2 document.Load(reswPath); 3 4 // 获取 resw 文件中的 data 节点。 5 XmlNodeList dataNodes = document.GetElementsByTagName("data"); 6 foreach(var temp in dataNodes) 7 { 8 XmlElement dataNode = temp as XmlElement; 9 if(dataNode != null) 10 { 11 string value = dataNode.GetAttribute("name"); 12 // key 中包含 ‘.’ 的作为控件的多语言化,不处理。 13 if(value.Contains(".") == false) 14 { 15 names.Add(value); 16 } 17 } 18 }
其中 names 变量用于存放 resw 中的键。
需要注意的是,键中包含 ‘.’ 的键是用于控件的本地化的,所以这些键我们跳过。(因为 ‘.’ 也不能包含在属性名中)
接下来就是如何获得这些 resw 的路径了。
这里我们往上想一步,获得当前工程的路径,然后搜索 resw 文件不就行了吗?
在获取当前工程的路径时,我们需要用到 T4 的语法,这里我编写成帮助函数。(注意:T4 的帮助函数必须放到 T4 文件的最后)
1 <#+ 2 // 获取当前 T4 模板所在的工程的目录。 3 public string GetProjectPath() 4 { 5 return Host.ResolveAssemblyReference("$(ProjectDir)"); 6 } 7 #>
这里有一点要注意,由于使用到 Host 属性,所以需要把 T4 文件前面的 hostspecific 修改为 true。
然后搜索 resw 就很简单了:
1 string projectPath = GetProjectPath(); 2 3 string stringsPath = Path.Combine(projectPath, "Strings"); 4 string[] reswPaths; 5 6 // 当前项目存在 Strings 文件夹。 7 if(Directory.Exists(stringsPath)) 8 { 9 // 获取 Strings 文件夹下所有的 resw 文件的路径。 10 reswPaths = Directory.GetFiles(stringsPath, "*.resw", SearchOption.AllDirectories); 11 } 12 else 13 { 14 reswPaths = new string[0]; 15 }
这里进行了路径拼接是因为生成程序的目录下,也会有由于 VS 调试生成的 resw 文件,这里我们是不需要的,而且这样搜索的效率会高一点。我们仅仅需要 Strings 文件夹下的 resw。
需要的基本都准备好了,最后一个大难题就是,我们想生成的 cs 的命名空间跟当前项目相同。关于这一点,博客园好像找不到,最后在 stackoverflow 上找到了一个差不多的答案。修改符合我们需求后,我们将获取命名空间的也封装成帮助函数:
1 // 获取当前 T4 模板所在的工程的默认命名空间。 2 public string GetProjectDefaultNamespace() 3 { 4 IServiceProvider serviceProvider = (IServiceProvider)this.Host; 5 EnvDTE.DTE dte = (EnvDTE.DTE)serviceProvider.GetService(typeof(EnvDTE.DTE)); 6 EnvDTE.Project project = (EnvDTE.Project)dte.Solution.FindProjectItem(this.Host.TemplateFile).ContainingProject; 7 return project.Properties.Item("DefaultNamespace").Value.ToString(); 8 }
最后附上完整代码:
1 <#@ template debug="false" hostspecific="true" language="C#" #> 2 <#@ assembly name="System.Core" #> 3 <#@ assembly name="System.Xml" #> 4 <#@ assembly name="EnvDTE" #> 5 <#@ import namespace="System.Linq" #> 6 <#@ import namespace="System.Text" #> 7 <#@ import namespace="System.Collections.Generic" #> 8 <#@ import namespace="System.IO" #> 9 <#@ import namespace="System.Xml" #> 10 <#@ output extension=".cs" #> 11 12 <# 13 // 用于存放所有 resw 的 key。 14 HashSet<string> names = new HashSet<string>(); 15 string projectPath = GetProjectPath(); 16 17 string stringsPath = Path.Combine(projectPath, "Strings"); 18 string[] reswPaths; 19 20 // 当前项目存在 Strings 文件夹。 21 if(Directory.Exists(stringsPath)) 22 { 23 // 获取 Strings 文件夹下所有的 resw 文件的路径。 24 reswPaths = Directory.GetFiles(stringsPath, "*.resw", SearchOption.AllDirectories); 25 } 26 else 27 { 28 reswPaths = new string[0]; 29 } 30 31 foreach(string reswPath in reswPaths) 32 { 33 XmlDocument document = new XmlDocument(); 34 document.Load(reswPath); 35 36 // 获取 resw 文件中的 data 节点。 37 XmlNodeList dataNodes = document.GetElementsByTagName("data"); 38 foreach(var temp in dataNodes) 39 { 40 XmlElement dataNode = temp as XmlElement; 41 if(dataNode != null) 42 { 43 string value = dataNode.GetAttribute("name"); 44 // key 中包含 ‘.’ 的作为控件的多语言化,不处理。 45 if(value.Contains(".") == false) 46 { 47 names.Add(value); 48 } 49 } 50 } 51 } 52 #> 53 <# 54 if(names.Count > 0) 55 { 56 #> 57 using Windows.ApplicationModel.Resources; 58 59 namespace <# Write(GetProjectDefaultNamespace());#> 60 { 61 public class LocalizedStrings 62 { 63 private readonly static ResourceLoader Loader = new ResourceLoader(); 64 65 <# 66 foreach(string name in names) 67 { 68 if(string.IsNullOrWhiteSpace(name)) 69 { 70 continue; 71 } 72 73 // 将 key 的第一个字母大写,作为属性名。 74 string propertyName = name[0].ToString().ToUpper() + name.Substring(1); 75 #> 76 public static string <#=propertyName#> 77 { 78 get 79 { 80 return Loader.GetString("<#=name#>"); 81 } 82 } 83 <# 84 } 85 #> 86 } 87 } 88 <# 89 } 90 #> 91 <#+ 92 // 获取当前 T4 模板所在的工程的目录。 93 public string GetProjectPath() 94 { 95 return Host.ResolveAssemblyReference("$(ProjectDir)"); 96 } 97 98 // 获取当前 T4 模板所在的工程的默认命名空间。 99 public string GetProjectDefaultNamespace() 100 { 101 IServiceProvider serviceProvider = (IServiceProvider)this.Host; 102 EnvDTE.DTE dte = (EnvDTE.DTE)serviceProvider.GetService(typeof(EnvDTE.DTE)); 103 EnvDTE.Project project = (EnvDTE.Project)dte.Solution.FindProjectItem(this.Host.TemplateFile).ContainingProject; 104 return project.Properties.Item("DefaultNamespace").Value.ToString(); 105 } 106 #>
其中 <#=variable#> 为输出变量的值,学习过 Asp.net 的园友应该会很熟悉的了。
最后效果:
除了 T4 生成出来的没格式化这点比较蛋疼之外,其它一切问题都被 T4 解决好了,使用 LocalizedStrings.China 就可以访问到 China 这个键对应的值了。以后修改完 resw 之后,重新生成一下项目就可以更新 LocalizedStrings 了。
后记:
今天有空,再调整了下 T4 的格式,生成的代码的格式终于没问题了。
1 <#@ template debug="false" hostspecific="true" language="C#" #> 2 <#@ assembly name="System.Core" #> 3 <#@ assembly name="System.Xml" #> 4 <#@ assembly name="EnvDTE" #> 5 <#@ import namespace="System.Linq" #> 6 <#@ import namespace="System.Text" #> 7 <#@ import namespace="System.Collections.Generic" #> 8 <#@ import namespace="System.IO" #> 9 <#@ import namespace="System.Xml" #> 10 <#@ output extension=".cs" #> 11 <# 12 // 用于存放所有 resw 的 key。 13 HashSet<string> names = new HashSet<string>(); 14 string projectPath = GetProjectPath(); 15 16 string stringsPath = Path.Combine(projectPath, "Strings"); 17 string[] reswPaths; 18 19 // 当前项目存在 Strings 文件夹。 20 if(Directory.Exists(stringsPath)) 21 { 22 // 获取 Strings 文件夹下所有的 resw 文件的路径。 23 reswPaths = Directory.GetFiles(stringsPath, "*.resw", SearchOption.AllDirectories); 24 } 25 else 26 { 27 reswPaths = new string[0]; 28 } 29 30 foreach(string reswPath in reswPaths) 31 { 32 XmlDocument document = new XmlDocument(); 33 document.Load(reswPath); 34 35 // 获取 resw 文件中的 data 节点。 36 XmlNodeList dataNodes = document.GetElementsByTagName("data"); 37 foreach(var temp in dataNodes) 38 { 39 XmlElement dataNode = temp as XmlElement; 40 if(dataNode != null) 41 { 42 string value = dataNode.GetAttribute("name"); 43 // key 中包含 ‘.’ 的作为控件的多语言化,不处理。 44 if(value.Contains(".") == false) 45 { 46 names.Add(value); 47 } 48 } 49 } 50 } 51 #> 52 <# 53 if(names.Count > 0) 54 { 55 #> 56 using Windows.ApplicationModel.Resources; 57 58 namespace <# WriteLine(GetProjectDefaultNamespace());#> 59 { 60 public class LocalizedStrings 61 { 62 private readonly static ResourceLoader Loader = new ResourceLoader(); 63 <# 64 foreach(string name in names) 65 { 66 if(string.IsNullOrWhiteSpace(name)) 67 { 68 continue; 69 } 70 71 // 将 key 的第一个字母大写,作为属性名。 72 string propertyName = name[0].ToString().ToUpper() + name.Substring(1); 73 #> 74 75 public static string <#=propertyName#> 76 { 77 get 78 { 79 return Loader.GetString("<#=name#>"); 80 } 81 } 82 <# 83 } 84 #> 85 } 86 } 87 <# 88 } 89 #> 90 <#+ 91 // 获取当前 T4 模板所在的工程的目录。 92 public string GetProjectPath() 93 { 94 return Host.ResolveAssemblyReference("$(ProjectDir)"); 95 } 96 97 // 获取当前 T4 模板所在的工程的默认命名空间。 98 public string GetProjectDefaultNamespace() 99 { 100 IServiceProvider serviceProvider = (IServiceProvider)this.Host; 101 EnvDTE.DTE dte = (EnvDTE.DTE)serviceProvider.GetService(typeof(EnvDTE.DTE)); 102 EnvDTE.Project project = (EnvDTE.Project)dte.Solution.FindProjectItem(this.Host.TemplateFile).ContainingProject; 103 return project.Properties.Item("DefaultNamespace").Value.ToString(); 104 } 105 #>
关键在于要将<##>这些控制块顶端对齐,也就是<#前面的空格在之前的版本被输出了,所以不好控制输出的格式。
2015年5月29日补充:
之前的方案没法支持多个 resw,只能支持 Resources.resw。resw 改成别的名字就不可以用了。
顺便获取默认命名空间有直接的 API,之前没装插件,没有智能感知所以不知道。。。
1 <#@ template debug="false" hostspecific="true" language="C#" #> 2 <#@ assembly name="System.Core" #> 3 <#@ assembly name="System.Xml" #> 4 <#@ import namespace="System.Linq" #> 5 <#@ import namespace="System.Text" #> 6 <#@ import namespace="System.IO" #> 7 <#@ import namespace="System.Collections.Generic" #> 8 <#@ import namespace="System.Xml" #> 9 <#@ output extension=".cs" #> 10 <# 11 bool hadOutput = false; 12 13 HashSet<KeyName> resourceKeys = new HashSet<KeyName>();// 存放 key 和对应的 resw 的名字。 14 15 foreach (var reswPath in GetAllReswPath()) 16 { 17 string name = GetReswName(reswPath); 18 var keys = GetReswKeys(reswPath).ToList(); 19 20 for (int i = 0; i < keys.Count; i++) 21 { 22 var key = keys[i]; 23 24 if (string.IsNullOrWhiteSpace(key)) 25 { 26 continue; 27 } 28 29 KeyName keyName = KeyName.Create(key, name); 30 resourceKeys.Add(keyName); 31 } 32 } 33 #> 34 <# 35 if (resourceKeys.Any()) 36 { 37 #> 38 using Windows.ApplicationModel.Resources; 39 40 namespace <#= GetNamespace() #> 41 { 42 public static partial class LocalizedStrings 43 { 44 <# 45 } 46 #> 47 <# 48 foreach (var keyName in resourceKeys) 49 { 50 string key = keyName.Key; 51 string name = keyName.Name; 52 53 // 将 key 的第一个字母大写,作为属性名。 54 string propertyName = key[0].ToString().ToUpper() + key.Substring(1); 55 56 // ResourceLoader 的 key,如果为 "Resources",则可以省略。 57 string resourceName = string.Equals(name, "Resources", StringComparison.OrdinalIgnoreCase) ? string.Empty : ("\"" + name + "\""); 58 59 // 不是第一个属性,添加换行。 60 if (hadOutput == true) 61 { 62 WriteLine(string.Empty); 63 } 64 #> 65 public static string <#= propertyName #> 66 { 67 get 68 { 69 return ResourceLoader.GetForCurrentView(<#= resourceName #>).GetString("<#= key #>"); 70 } 71 } 72 <# 73 hadOutput = true; 74 } 75 #> 76 <# 77 if (resourceKeys.Any()) 78 { 79 #> 80 } 81 } 82 <# 83 } 84 #> 85 <#+ 86 /// <summary> 87 /// 获取当前项目的默认命名空间。 88 /// </summary> 89 /// <returns>当前项目的默认命名空间。</returns> 90 private string GetNamespace() 91 { 92 return this.Host.ResolveParameterValue("directiveId", "namespaceDirectiveProcessor", "namespaceHint"); 93 } 94 95 /// <summary> 96 /// 获取当前项目的绝对路径。 97 /// </summary> 98 /// <returns>当前项目的绝对路径。</returns> 99 private string GetProjectPath() 100 { 101 return this.Host.ResolveAssemblyReference("$(ProjectDir)"); 102 } 103 104 /// <summary> 105 /// 获取 Strings 文件夹内的所有 resw 的绝对路径。 106 /// </summary> 107 /// <returns>Strings 文件夹内的所有 resw 的绝对路径。如果没有,则返回空集合。</returns> 108 private IEnumerable<string> GetAllReswPath() 109 { 110 string projectPath = GetProjectPath(); 111 string stringsPath = Path.Combine(projectPath, "Strings"); 112 113 // 当前项目存在 Strings 文件夹。 114 if (Directory.Exists(stringsPath)) 115 { 116 // 获取 Strings 文件夹下所有的 resw 文件的路径。 117 return Directory.GetFiles(stringsPath, "*.resw", SearchOption.AllDirectories); 118 } 119 else 120 { 121 return Enumerable.Empty<string>(); 122 } 123 } 124 125 /// <summary> 126 /// 获取 resw 的文件名。 127 /// </summary> 128 /// <returns>resw 的文件名。</returns> 129 private string GetReswName(string reswPath) 130 { 131 return Path.GetFileNameWithoutExtension(reswPath); 132 } 133 134 /// <summary> 135 /// 获取 resw 内的所有键的名称。 136 /// </summary> 137 /// <returns>resw 内所有键的名称,不包含用于本地化控件属性的键。</returns> 138 private IEnumerable<string> GetReswKeys(string reswPath) 139 { 140 XmlDocument document = new XmlDocument(); 141 document.Load(reswPath); 142 143 // 获取 resw 文件中的 data 节点。 144 XmlNodeList dataNodes = document.GetElementsByTagName("data"); 145 foreach(var temp in dataNodes) 146 { 147 XmlElement dataNode = temp as XmlElement; 148 if (dataNode != null) 149 { 150 string key = dataNode.GetAttribute("name"); 151 // key 中包含 ‘.’ 的作为控件的多语言化,不处理。 152 if (key.Contains(".") == false) 153 { 154 yield return key; 155 } 156 } 157 } 158 } 159 160 // 辅助结构体 161 private struct KeyName 162 { 163 internal string Key 164 { 165 get; 166 set; 167 } 168 169 internal string Name 170 { 171 get; 172 set; 173 } 174 175 internal static KeyName Create(string key, string name) 176 { 177 return new KeyName() 178 { 179 Key = key, 180 Name = name 181 }; 182 } 183 } 184 #>