经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
.NET应用系统的国际化-基于Roslyn抽取词条、更新代码
来源:cnblogs  作者:Eric zhou  时间:2023/3/20 8:43:49  对本文有异议

上篇文章我们介绍了

VUE+.NET应用系统的国际化-多语言词条服务

系统国际化改造整体设计思路如下:

  1. 提供一个工具,识别前后端代码中的中文,形成多语言词条,按语言、界面、模块统一管理多有的多语言词条
  2. 提供一个翻译服务,批量翻译多语言词条
  3. 提供一个词条服务,支持后端代码在运行时根据用户登录的语言,动态获取对应的多语言文本
  4. 提供前端多语言JS生成服务,按界面动态生成对应的多语言JS文件,方便前端VUE文件使用。
  5. 提供代码替换工具,将VUE前端代码中的中文替换为$t("词条ID"),后端代码中的中文替换为TermService.Current.GetText("词条ID")

今天,我们在上篇文章的基础上,继续介绍基于Roslyn抽取词条、更新代码。

一、业务背景

先说一下业务背景,后端.NET代码中存在大量的中文提示和异常消息,甚至一些中文返回值文本。

这些中文文字都需要识别出来,抽取为多语言词条,同时将代码替换为调用多语言词条服务获取翻译后的文本。

例如:

  1. private static void CheckMd5(string fileName, string md5Data)
  2. {
  3. string md5Str = MD5Service.GetMD5(fileName);
  4. if (!string.Equals(md5Str, md5Data, StringComparison.OrdinalIgnoreCase))
  5. {
  6. throw new CustomException(PackageExceptionConst.FileMd5CheckFailed, "服务包文件MD5校验失败:" + fileName);
  7. }
  8. }

代码中需要将“服务包文件MD5校验失败”这个文本做多语言改造。

这里通过调用多语言词条服务I18NTermService,根据线程上下文中设置的语言,获取对应的翻译文本。例如以下代码:

  1. var text=T.Core.I18N.Service.TermService.Current.GetTextFormatted("词条ID""默认文本");

    throw new CustomException(PackageExceptionConst.FileMd5CheckFailed, text + fileName);

以上背景下,我们准备使用Roslyn技术对代码进行中文扫描,对扫描出来的文本,做词条抽取、代码替换。

二、使用Roslyn技术对代码进行中文扫描

首先,我们先定义好代码中多语言词条的扫描结果类TermScanResult

  1. 1 [Serializable]
  2. 2 public class TermScanResult
  3. 3 {
  4. 4 public Guid Id { get; set; }
  5. 5 public string OriginalText { get; set; }
  6. 6
  7. 7 public string ChineseText { get; set; }
  8. 8
  9. 9 public string SlnName { get; set; }
  10. 10
  11. 11 public string ProjectName { get; set; }
  12. 12
  13. 13 public string ClassFile { get; set; }
  14. 14
  15. 15 public string MethodName { get; set; }
  16. 16
  17. 17 public string Code { get; set; }
  18. 18
  19. 19 public I18NTerm I18NTerm { get; set; }
  20. 20
  21. 21 public string SlnPath { get; set; }
  22. 22
  23. 23 public string ClassPath { get; set; }
  24. 24 28 public string SubSystemCode { get; set; }
  25. 29
  26. 30 public override string ToString()
  27. 31 {
  28. 32 return Code;
  29. 33 }
  30. 34 }

上述代码中SubSystemCode是一个业务管理维度。大家忽略即可。

我们会以sln解决方案为单位,扫描代码中的中文文字。

以下是具体的实现代码

  1. public async Task<List<TermScanResult>> CheckSln(string slnPath, System.ComponentModel.BackgroundWorker backgroundWorker, SubSystemFile subSystemFiles, string subSystem)
  2. {
  3. var slnFile = new FileInfo(slnPath);
  4. var results = new List<TermScanResult>();
  5. MSBuildHelper.RegisterMSBuilder();
  6. var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(slnPath);
  7. var subSystemInfo = subSystemFiles?.SubSystemSlnMappings.FirstOrDefault(w => w.SlnName.Select(s => s += ".sln").Contains(slnFile.Name.ToLower()));
  8. if (solution.Projects != null && solution.Projects.Count() > 0)
  9. {
  10. foreach (var project in solution.Projects.ToList())
  11. {
  12. backgroundWorker.ReportProgress(10, $"扫描Project: {project.Name}");
  13. var documents = project.Documents.Where(x => x.Name.Contains(".cs"));
  14. if (project.Name.ToLower().Contains("test"))
  15. {
  16. continue;
  17. }
  18. var codeReplace = new CodeReplace();
  19. foreach (var document in documents)
  20. {
  21. var tree = await document.GetSyntaxTreeAsync();
  22. var root = tree.GetCompilationUnitRoot();
  23. if (root.Members == null || root.Members.Count == 0) continue;
  24. //member
  25. var classDeclartions = root.DescendantNodes().Where(i => i is ClassDeclarationSyntax);
  26. foreach (var classDeclare in classDeclartions)
  27. {
  28. var programDeclaration = classDeclare as ClassDeclarationSyntax;
  29. if (programDeclaration == null) continue;
  30. foreach (var memberDeclarationSyntax in programDeclaration.Members)
  31. {
  32. foreach (var item in GetLiteralStringExpression(memberDeclarationSyntax))
  33. {
  34. var statementCode = item.Item1;
  35. foreach (var syntaxNode in item.Item3)
  36. {
  37. ExpressionSyntaxParser expressionSyntaxParser = new ExpressionSyntaxParser();
  38. var text = "";
  39. var expressionSyntax = expressionSyntaxParser
  40. .GetExpressionSyntaxVerifyRule(syntaxNode as ExpressionSyntax, statementCode);
  41. if (expressionSyntax != null)
  42. {
  43. // 排除
  44. if (expressionSyntaxParser.IsExcludeCaller(expressionSyntax, statementCode))
  45. {
  46. continue;
  47. }
  48. text = expressionSyntaxParser.GetExpressionSyntaxOriginalText(expressionSyntax, statementCode);
  49. if (expressionSyntax is Microsoft.CodeAnalysis.CSharp.Syntax.InterpolatedStringExpressionSyntax)
  50. {
  51. text = expressionSyntaxParser.GetExpressionSyntaxOriginalText(expressionSyntax, statementCode);
  52. if (expressionSyntax is Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax)
  53. {
  54. if (!expressionSyntax.IsKind(SyntaxKind.StringLiteralExpression))
  55. {
  56. continue;
  57. }
  58. text = expressionSyntax.NormalizeWhitespace().ToString();
  59. }
  60. }
  61. }
  62. if (CheckChinese(text) == false) continue;
  63. if (string.IsNullOrWhiteSpace(text)) continue;
  64. if (string.IsNullOrWhiteSpace(text.Replace("\"", "").Trim())) continue;
  65. results.Add(new TermScanResult()
  66. {
  67. Id = Guid.NewGuid(),
  68. ClassPath = programDeclaration.SyntaxTree.FilePath,
  69. SlnPath = slnPath,
  70. OriginalText = text.Replace("\"", "").Trim(),
  71. ChineseText = text,
  72. SlnName = slnFile.Name,
  73. ProjectName = project.Name,
  74. ClassFile = programDeclaration.Identifier.Text,
  75. MethodName = item.Item2,
  76. Code = statementCode,
  77. SubSystemCode = subSystem
  78. });
  79. }
  80. }
  81. }
  82. }
  83. }
  84. }
  85. }
  86. return results;
  87. }

上述代码中,我们先使用MSBuilder编译,构建 sln解决方案

  1. MSBuildHelper.RegisterMSBuilder();
  2. var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(slnPath);

然后遍历solution下的各个Project中的class类

  1. foreach (var project in solution.Projects.ToList())
  1. var documents = project.Documents.Where(x => x.Name.Contains(".cs"));

然后遍历类中声明、成员、方法中的每行代码,通过正则表达式识别是否有中文字符

  1. public static bool CheckChinese(string strZh)
  2. {
  3. Regex re = new Regex(@"[\u4e00-\u9fa5]+");
  4. if (re.IsMatch(strZh))
  5. {
  6. return true;
  7. }
  8. return false;
  9. }

如果存在中文字符,作为扫描后的结果,识别为多语言词条

  1. results.Add(new TermScanResult()
  2. {
  3. Id = Guid.NewGuid(),
  4. ClassPath = programDeclaration.SyntaxTree.FilePath,
  5. SlnPath = slnPath,
  6. OriginalText = text.Replace("\"", "").Trim(),
  7. ChineseText = text,
  8. SlnName = slnFile.Name,
  9. ProjectName = project.Name,
  10. ClassFile = programDeclaration.Identifier.Text,
  11. MethodName = item.Item2,
  12. Code = statementCode, //管理维度
  13. SubSystemCode = subSystem //管理维度
  1. });

TermScanResult中没有对词条属性赋值。

public I18NTerm I18NTerm { get; set; }

下一篇文章的代码中,我们会通过多语言翻译服务,将翻译后的文本放到I18NTerm 属性中,作为多语言词条。

三、代码替换

代码替换这块逻辑中,我们设计了一个类SourceWeaver,对上一步的代码扫描结果,进行代码替换

  1. CodeScanReplace这个方法中完成了代码的二次扫描和替换
  1. /// <summary>
  2. /// 源代码替换服务
  3. /// </summary>
  4. public class SourceWeaver
  5. {
  6. List<CommonTermDto> commonTerms = new List<CommonTermDto>();
  7. List<CommonTermDto> commSubTerms = new List<CommonTermDto>();
  8. public SourceWeaver()
  9. {
  10. commonTerms = JsonConvert.DeserializeObject<List<CommonTermDto>>(File.ReadAllText("comm_data.json"));
  11. commSubTerms = JsonConvert.DeserializeObject<List<CommonTermDto>>(File.ReadAllText("comm_sub_data.json"));
  12. }
  13. public async Task CodeScanReplace(Tuple<List<I18NTerm>, List<TermScanResult>> result, System.ComponentModel.BackgroundWorker backgroundWorker)
  14. {
  15. try
  16. {
  17. backgroundWorker.ReportProgress(0, "正在对代码进行替换.");
  18. var termScanResultGroupBy = result.Item2.GroupBy(g => g.SlnName);
  19. foreach (var termScanResult in termScanResultGroupBy)
  20. {
  21. var termScan = termScanResult.FirstOrDefault();
  22. MSBuildHelper.RegisterMSBuilder();
  23. var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(termScan.SlnPath).ConfigureAwait(false);
  24. if (solution.Projects.Any())
  25. {
  26. foreach (var project in solution.Projects.ToList())
  27. {
  28. if (project.Name.ToLower().Contains("test"))
  29. {
  30. continue;
  31. }
  32. var projectTermScanResults = result.Item2.Where(f => f.ProjectName == project.Name);
  33. var documents = project.Documents.Where(x =>
  34. {
  35. return x.Name.Contains(".cs") && projectTermScanResults.Any(f => $"{f.ClassPath}" == x.FilePath);
  36. });
  37. foreach (var document in documents)
  38. {
  39. var tree = await document.GetSyntaxTreeAsync().ConfigureAwait(false);
  40. var root = tree.GetCompilationUnitRoot();
  41. if (root.Members.Count == 0) continue;
  42. var classDeclartions = root.DescendantNodes()
  43. .Where(i => i is ClassDeclarationSyntax);
  44. List<MemberDeclarationSyntax> syntaxNodes = new List<MemberDeclarationSyntax>();
  45. foreach (var classDeclare in classDeclartions)
  46. {
  47. if (!(classDeclare is ClassDeclarationSyntax programDeclaration)) continue;
  48. var className = programDeclaration.Identifier.Text;
  49.  
  50. foreach (var method in programDeclaration.Members)
  51. {
  52. if (method is ConstructorDeclarationSyntax)
  53. {
  54. syntaxNodes.Add((ConstructorDeclarationSyntax)method);
  55. }
  56. else if (method is MethodDeclarationSyntax)
  57. {
  58. syntaxNodes.Add((MethodDeclarationSyntax)method);
  59. }
  60. else if (method is PropertyDeclarationSyntax)
  61. {
  62. syntaxNodes.Add(method);
  63. }
  64. else if (method is FieldDeclarationSyntax)
  65. {
  66. // 注:常量不支持
  67. syntaxNodes.Add(method);
  68. }
  69. }
  70. }
  71. var terms = termScanResult.Where(
  72. f => f.ProjectName == document.Project.Name && f.ClassPath == document.FilePath).ToList();
  73. backgroundWorker.ReportProgress(10, $"正在检查{document.FilePath}文件.");
  74. ReplaceNodesAndSave(root, syntaxNodes, terms, result, backgroundWorker, document.Name);
  75. }
  76. }
  77. }
  78. }
  79. }
  80. catch (Exception ex)
  81. {
  82. LogUtils.LogError(string.Format("异常类型:{0}\r\n异常消息:{1}\r\n异常信息:{2}\r\n",
  83. ex.GetType().Name, ex.Message, ex.StackTrace));
  84. backgroundWorker.ReportProgress(0, ex.Message);
  85. }
  86. }
  87. public async void ReplaceNodesAndSave(SyntaxNode classSyntaxNode, List<MemberDeclarationSyntax> syntaxNodes, IEnumerable<TermScanResult> terms, Tuple<List<I18NTerm>, List<TermScanResult>> result,
  88. System.ComponentModel.BackgroundWorker backgroundWorker, string className)
  89. {
  90. {//check pro是否存在词条
  91. if (AppConfig.Instance.IsCheckTermPro)
  92. {
  93. backgroundWorker.ReportProgress(15, $"词条验证中.");
  94. var termsCodes = terms.Select(f => f.I18NTerm.Code).ToList();
  95. var size = 100;
  96. var p = (result.Item2.Count() + size - 1) / size;
  97. using DBHelper dBHelper = new DBHelper();
  98. List<I18NTerm> items = new List<I18NTerm>();
  99. for (int i = 0; i < p; i++)
  100. {
  101. var list = termsCodes
  102. .Skip(i * size).Take(size);
  103. Thread.Sleep(10);
  104. var segmentItems = await dBHelper.GetTermsAsync(termsCodes).ConfigureAwait(false);
  105. items.AddRange(segmentItems);
  106. }
  107. List<TermScanResult> termScans = new List<TermScanResult>();
  108. foreach (var term in terms)
  109. {
  110. if (items.Any(f => f.Code == term.I18NTerm.Code))
  111. {
  112. termScans.Add(term);
  113. }
  114. else
  115. {
  116. backgroundWorker.ReportProgress(20, $"词条{term.OriginalText}未导入到词条库,该词条将忽略替换.");
  117. }
  118. }
  119. terms = termScans;
  120. }
  121. }
  122. var newclassDeclare = classSyntaxNode;
  123. newclassDeclare = classSyntaxNode.ReplaceNodes(syntaxNodes,
  124. (methodDeclaration, _) =>
  125. {
  126. MemberDeclarationSyntax newMemberDeclarationSyntax = methodDeclaration;
  127. var className = ((ClassDeclarationSyntax)newMemberDeclarationSyntax.Parent).Identifier.Text;
  128. List<StatementSyntax> statementSyntaxes = new List<StatementSyntax>();
  129. switch (newMemberDeclarationSyntax)
  130. {
  131. case ConstructorDeclarationSyntax:
  132. {
  133. var blockSyntax = (newMemberDeclarationSyntax as ConstructorDeclarationSyntax).NormalizeWhitespace().Body;
  134. if (blockSyntax == null)
  135. {
  136. break;
  137. }
  138. foreach (var statement in blockSyntax.Statements)
  139. {
  140. var nodeStatement = statement.DescendantNodes();
  141. statementSyntaxes.Add(new CodeReplace().ReplaceStatementNodes(statement,
  142. new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms));
  143. }
  144. break;
  145. }
  146. case MethodDeclarationSyntax:
  147. {
  148. var blockSyntax = (methodDeclaration as MethodDeclarationSyntax).NormalizeWhitespace().Body;
  149. if (blockSyntax == null)
  150. {
  151. break;
  152. }
  153. foreach (var statement in blockSyntax.Statements)
  154. {
  155. var nodeStatement = statement.DescendantNodes();
  156. statementSyntaxes.Add(new CodeReplace().ReplaceStatementNodes(statement,
  157. new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms));
  158. }
  159. break;
  160. }
  161. case PropertyDeclarationSyntax:
  162. {
  163. var propertyDeclarationSyntax = newMemberDeclarationSyntax as PropertyDeclarationSyntax;
  164. var nodeStatement = propertyDeclarationSyntax.DescendantNodes();
  165. return new CodeReplace().ReplacePropertyNodes(newMemberDeclarationSyntax as PropertyDeclarationSyntax,
  166. new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms);
  167. }
  168. case FieldDeclarationSyntax:
  169. {
  170. var fieldDeclarationSyntax = newMemberDeclarationSyntax as FieldDeclarationSyntax;
  171. var nodeStatement = fieldDeclarationSyntax.DescendantNodes();
  172. return new CodeReplace().ReplaceFiledNodes(fieldDeclarationSyntax,
  173. new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms);
  174. }
  175. }
  176. backgroundWorker.ReportProgress(50, $"解析并对类文件{className}中的方法做语句替换.");
  177. // 替换方法内部
  178. if (newMemberDeclarationSyntax is MethodDeclarationSyntax)
  179. {
  180. return new CodeReplace().ReplaceMethodDeclaration(newMemberDeclarationSyntax as MethodDeclarationSyntax, statementSyntaxes);
  181. }
  182. else if (newMemberDeclarationSyntax is ConstructorDeclarationSyntax)
  183. {
  184. return new CodeReplace().ReplaceConstructorDeclaration(newMemberDeclarationSyntax as ConstructorDeclarationSyntax, statementSyntaxes);
  185. }
  186. return newMemberDeclarationSyntax;
  187. });
  188. var sourceStr = newclassDeclare.NormalizeWhitespace().GetText().ToString();
  189. File.WriteAllText(newclassDeclare.SyntaxTree.FilePath, sourceStr);
  190. backgroundWorker.ReportProgress(100, $"完成{className}的替换.");
  191. }
  192. }

关键的代码语义替换的实现代码:

  1. public StatementSyntax ReplaceStatementNodes(StatementSyntax statement, List<ExpressionSyntax> expressionSyntaxes, IEnumerable<TermScanResult> terms
  2. , List<CommonTermDto> commonTerms, List<CommonTermDto> commSubTerms)
  3. {
  4. var statementSyntax = statement.ReplaceNodes(expressionSyntaxes, (syntaxNode, _) =>
  5. {
  6. var statementStr = statement.NormalizeWhitespace().ToString();
  7. var argumentLists = statement.DescendantNodes().
  8. OfType<InvocationExpressionSyntax>();
  9. ExpressionSyntaxParser expressionSyntaxParser = new ExpressionSyntaxParser();
  10. return expressionSyntaxParser.ExpressionSyntaxTermReplace(syntaxNode, statementStr, terms, commonTerms, commSubTerms);
  11. });
  12. return statementSyntax;
  13. }

这里,我们抽象了一个ExpressionSyntaxParser 类,负责替换代码:

  1. T.Core.I18N.Service.TermService.Current.GetTextFormatted
  1. public ExpressionSyntax ExpressionSyntaxTermReplace(ExpressionSyntax syntaxNode, string statementStr, IEnumerable<TermScanResult> terms
  2. , List<CommonTermDto> commonTerms, List<CommonTermDto> commSubTerms)
  3. {
  4. var expressionSyntax = GetExpressionSyntaxVerifyRule(syntaxNode, statementStr);
  5. var originalText = GetExpressionSyntaxOriginalText(expressionSyntax, statementStr);
  6.  
  7. var I18Expr = "";
  8. var interpolationSyntaxes = syntaxNode.DescendantNodes().OfType<InterpolationSyntax>();
  9. var term = terms.FirstOrDefault(i => i.ChineseText == originalText);
  10. if (term == null)
  11. return syntaxNode;
  12. string termcode = term.I18NTerm.Code;
  13. if (syntaxNode is InterpolatedStringExpressionSyntax)
  14. {
  15. if (interpolationSyntaxes.Count() > 0)
  16. {
  17. var parms = "";
  18. foreach (var item in interpolationSyntaxes)
  19. {
  20. parms += $",{item.ToString().TrimStart('{').TrimEnd('}')}";
  21. }
  22. I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetTextFormatted(\"" + termcode + "\", " + originalText + parms + ")}\"";
  23. var token1 = SyntaxFactory.Token(default, SyntaxKind.StringLiteralToken, I18Expr, "", default);
  24. return SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, token1);
  25. }
  26. else
  27. {
  28. var startToken = SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken);
  29. if ((syntaxNode as InterpolatedStringExpressionSyntax).StringStartToken.Value == startToken.Value)
  30. {
  31. // 如果本身有"$"
  32. I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetText(\"" + termcode + "\"," + originalText + ")}";
  33. }
  34. else
  35. {
  36. // 如果没有"$"
  37. I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetText(\"" + termcode + "\",\\teld\"" + originalText + "\")}";
  38. I18Expr = I18Expr.Replace("\\teld", "$");
  39. }
  40. }
  41. }
  42. else
  43. {
  44. I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetText(\"" + termcode + "\"," + originalText + ")}";
  45. }
  46. var token = SyntaxFactory.Token(default(SyntaxTriviaList), SyntaxKind.InterpolatedVerbatimStringStartToken, I18Expr, "$\"", default(SyntaxTriviaList));
  47. var literalExpressionSyntax = SyntaxFactory.InterpolatedStringExpression(token);
  48. return literalExpressionSyntax;
  49. }
  1. T.Core.I18N.Service.TermService这个就是多语言词条服务类,这个类中提供了一个GetText的方法,通过词条编号,获取多语言文本。

    代码完成替换后,打开VS,对工程引用多语言词条服务的Nuget包/dll,重新编译代码,手工校对替换后的代码即可。
    以上是.NET应用系统的国际化-基于Roslyn抽取词条、更新代码的分享。



    周国庆
    2023/3/19







原文链接:https://www.cnblogs.com/tianqing/p/17232474.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号