zoukankan      html  css  js  c++  java
  • 【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         }
    获取 resw 中的所有键

    其中 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     }
    获取所有 resw 的路径

    这里进行了路径拼接是因为生成程序的目录下,也会有由于 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     }
    获取当前 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 <#
     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 #>
    格式化 fix

    关键在于要将<##>这些控制块顶端对齐,也就是<#前面的空格在之前的版本被输出了,所以不好控制输出的格式。

    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  #>
    2015/5/29 fix
  • 相关阅读:
    打印九九乘法表
    PAT (Basic Level) Practice (中文) 1091 N-自守数 (15分)
    PAT (Basic Level) Practice (中文)1090 危险品装箱 (25分) (单身狗进阶版 使用map+ vector+数组标记)
    PAT (Basic Level) Practice (中文) 1088 三人行 (20分)
    PAT (Basic Level) Practice (中文) 1087 有多少不同的值 (20分)
    PAT (Basic Level) Practice (中文)1086 就不告诉你 (15分)
    PAT (Basic Level) Practice (中文) 1085 PAT单位排行 (25分) (map搜索+set排序+并列进行排行)
    PAT (Basic Level) Practice (中文) 1083 是否存在相等的差 (20分)
    PAT (Basic Level) Practice (中文) 1082 射击比赛 (20分)
    PAT (Basic Level) Practice (中文) 1081 检查密码 (15分)
  • 原文地址:https://www.cnblogs.com/h82258652/p/4382622.html
Copyright © 2011-2022 走看看