如何通过源生成器自动为Resx文件添加资源键值对?
实现开发模式下自动填充本地化键的源生成器
完全可以实现这样的源生成器,核心是利用.NET的源生成器在编译时扫描代码中的本地化调用,提取键并自动补充到主语言的.resx文件中,且仅在开发模式运行。以下是具体实现思路和关键步骤:
核心实现步骤
1. 创建源生成器项目
创建类库项目,引用必要的NuGet包:
Microsoft.CodeAnalysis.CSharp:用于语法和语义分析Microsoft.CodeAnalysis.Analyzers:提供源生成器基础框架System.Xml:用于解析和修改.resx文件(本质为XML格式)
在项目文件中标记为源生成器:
<PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" /> </ItemGroup>
2. 扫描代码提取本地化键
实现ISourceGenerator接口,在Execute方法中完成以下操作:
- 先判断当前编译模式是否为Debug,非Debug模式直接跳过执行
- 遍历所有语法树,定位
IStringLocalizer/IHtmlLocalizer的索引器调用(即_localizer["key"]格式的代码) - 通过语义分析确认调用对象的类型确实是本地化接口,避免误判
- 提取字符串字面量形式的键值(动态生成的键无法在编译时确定,直接跳过)
3. 修改主.resx文件
找到项目中的主语言.resx文件(比如Resources.resx):
- 解析.resx的XML结构,检查目标键是否已存在
- 若不存在,则新增
<data>节点,设置name属性为提取的键,<value>节点内容与键相同 - 保存修改后的XML文件
4. 限制仅开发模式运行
通过编译配置判断是否执行逻辑:
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_configuration", out var config) || config != "Debug") { return; }
关键代码示例
以下是源生成器的核心逻辑片段:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Xml; [Generator] public class LocalizationKeyGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { // 无需额外初始化操作 } public void Execute(GeneratorExecutionContext context) { // 仅在Debug模式运行 if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_configuration", out var config) || config != "Debug") { return; } // 遍历所有语法树 foreach (var tree in context.Compilation.SyntaxTrees) { var root = tree.GetRoot(context.CancellationToken); var elementAccesses = root.DescendantNodes().OfType<ElementAccessExpressionSyntax>(); foreach (var elementAccess in elementAccesses) { // 定位_localizer的成员访问 if (elementAccess.Expression is not MemberAccessExpressionSyntax memberAccess) continue; // 语义分析确认类型 var semanticModel = context.Compilation.GetSemanticModel(tree); var symbol = semanticModel.GetSymbolInfo(memberAccess.Expression).Symbol; if (symbol == null || (!symbol.ToString().Contains("IStringLocalizer") && !symbol.ToString().Contains("IHtmlLocalizer"))) continue; // 提取字符串字面量键 if (elementAccess.ArgumentList.Arguments.First().Expression is not LiteralExpressionSyntax literal) continue; var key = literal.Token.Value.ToString(); AddKeyToMainResx(context, key); } } } private void AddKeyToMainResx(GeneratorExecutionContext context, string key) { // 找到主resx文件(示例为Resources.resx) var resxFile = context.AdditionalFiles.FirstOrDefault(f => f.Path.EndsWith("Resources.resx")); if (resxFile == null) return; var xmlDoc = new XmlDocument(); xmlDoc.Load(resxFile.Path); // 检查键是否已存在 var existingKey = xmlDoc.SelectSingleNode($"//data[@name='{key}']"); if (existingKey != null) return; // 创建新的本地化节点 var dataNode = xmlDoc.CreateElement("data"); dataNode.SetAttribute("name", key); dataNode.SetAttribute("xml:space", "preserve"); var valueNode = xmlDoc.CreateElement("value"); valueNode.InnerText = key; dataNode.AppendChild(valueNode); xmlDoc.DocumentElement.AppendChild(dataNode); xmlDoc.Save(resxFile.Path); } }
注意事项
- 避免重复操作:每次编译仅添加不存在的键,防止重复写入
- 文件权限:确保开发环境下源生成器有.resx文件的写入权限
- 动态键处理:对于非字面量的动态键(如变量、表达式),编译时无法提取,需手动补充
- 多项目适配:若解决方案包含多个项目,需确保源生成器仅处理当前项目的.resx文件
- XML格式兼容:修改.resx时保持原有XML格式(如编码、缩进),避免破坏文件结构
内容的提问来源于stack exchange,提问作者Enricosoft
相关产品推荐
相关产品推荐

