翼度科技»论坛 编程开发 .net 查看内容

.NET应用系统的国际化-基于Roslyn抽取词条、更新代码

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
上篇文章我们介绍了
VUE+.NET应用系统的国际化-多语言词条服务
系统国际化改造整体设计思路如下:

  • 提供一个工具,识别前后端代码中的中文,形成多语言词条,按语言、界面、模块统一管理多有的多语言词条
  • 提供一个翻译服务,批量翻译多语言词条
  • 提供一个词条服务,支持后端代码在运行时根据用户登录的语言,动态获取对应的多语言文本
  • 提供前端多语言JS生成服务,按界面动态生成对应的多语言JS文件,方便前端VUE文件使用。
  • 提供代码替换工具,将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","默认文本"); <br><br>throw new CustomException(PackageExceptionConst.FileMd5CheckFailed, <strong>text</strong> + 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         <strong>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. </strong>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())<br>                {
  11.                     backgroundWorker.ReportProgress(10, $"扫描Project: {project.Name}");
  12.                     var documents = project.Documents.Where(x => x.Name.Contains(".cs"));<br>
  13.                     if (project.Name.ToLower().Contains("test"))
  14.                     {
  15.                         continue;
  16.                     }
  17.                     var codeReplace = new CodeReplace();
  18.                     foreach (var document in documents)
  19.                     {
  20.                         var tree = await document.GetSyntaxTreeAsync();
  21.                         var root = tree.GetCompilationUnitRoot();
  22.                         if (root.Members == null || root.Members.Count == 0) continue;
  23.                         //member
  24.                         var classDeclartions = root.DescendantNodes().Where(i => i is ClassDeclarationSyntax);
  25.                         foreach (var classDeclare in classDeclartions)
  26.                         {
  27.                             var programDeclaration = classDeclare as ClassDeclarationSyntax;
  28.                             if (programDeclaration == null) continue;
  29.                             foreach (var memberDeclarationSyntax in programDeclaration.Members)
  30.                             {
  31.                                 foreach (var item in GetLiteralStringExpression(memberDeclarationSyntax))
  32.                                 {
  33.                                     var statementCode = item.Item1;
  34.                                     foreach (var syntaxNode in item.Item3)
  35.                                     {
  36.                                         ExpressionSyntaxParser expressionSyntaxParser = new ExpressionSyntaxParser();
  37.                                         var text = "";
  38.                                         var expressionSyntax = expressionSyntaxParser
  39.                                             .GetExpressionSyntaxVerifyRule(syntaxNode as ExpressionSyntax, statementCode);
  40.                                         if (expressionSyntax != null)
  41.                                         {
  42.                                             // 排除
  43.                                             if (expressionSyntaxParser.IsExcludeCaller(expressionSyntax, statementCode))
  44.                                             {
  45.                                                 continue;
  46.                                             }
  47.                                             text = expressionSyntaxParser.GetExpressionSyntaxOriginalText(expressionSyntax, statementCode);
  48.                                             if (expressionSyntax is Microsoft.CodeAnalysis.CSharp.Syntax.InterpolatedStringExpressionSyntax)
  49.                                             {
  50.                                                 text = expressionSyntaxParser.GetExpressionSyntaxOriginalText(expressionSyntax, statementCode);
  51.                                                 if (expressionSyntax is Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax)
  52.                                                 {
  53.                                                     if (!expressionSyntax.IsKind(SyntaxKind.StringLiteralExpression))
  54.                                                     {
  55.                                                         continue;
  56.                                                     }
  57.                                                     text = expressionSyntax.NormalizeWhitespace().ToString();
  58.                                                 }
  59.                                             }
  60.                                         }
  61.                                         if (CheckChinese(text) == false) continue;
  62.                                         if (string.IsNullOrWhiteSpace(text)) continue;
  63.                                         if (string.IsNullOrWhiteSpace(text.Replace(""", "").Trim())) continue;
  64.                                         results.Add(new TermScanResult()
  65.                                         {
  66.                                             Id = Guid.NewGuid(),
  67.                                             ClassPath = programDeclaration.SyntaxTree.FilePath,
  68.                                             SlnPath = slnPath,
  69.                                             OriginalText = text.Replace(""", "").Trim(),
  70.                                             ChineseText = text,
  71.                                             SlnName = slnFile.Name,
  72.                                             ProjectName = project.Name,
  73.                                             ClassFile = programDeclaration.Identifier.Text,
  74.                                             MethodName = item.Item2,
  75.                                             Code = statementCode,
  76.                                             SubSystemCode = subSystem
  77.                                         });
  78.                                     }
  79.                                 }
  80.                             }
  81.                         }
  82.                     }
  83.                 }
  84.             }
  85.      return results;
  86. }
复制代码
上述代码中,我们先使用MSBuilder编译,构建 sln解决方案
  1. MSBuildHelper.RegisterMSBuilder();
  2. var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(slnPath);<br><br>
复制代码
然后遍历solution下的各个Project中的class类
  1. foreach (var project in solution.Projects.ToList())<br>
复制代码
  1. var documents = project.Documents.Where(x => x.Name.Contains(".cs"));<br>
复制代码
然后遍历类中声明、成员、方法中的每行代码,通过正则表达式识别是否有中文字符
  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. <strong>CodeScanReplace这个方法中完成了代码的二次扫描和替换</strong>
复制代码
  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.                                     foreach (var method in programDeclaration.Members)
  50.                                     {
  51.                                         if (method is ConstructorDeclarationSyntax)
  52.                                         {
  53.                                             syntaxNodes.Add((ConstructorDeclarationSyntax)method);
  54.                                         }
  55.                                         else if (method is MethodDeclarationSyntax)
  56.                                         {
  57.                                             syntaxNodes.Add((MethodDeclarationSyntax)method);
  58.                                         }
  59.                                         else if (method is PropertyDeclarationSyntax)
  60.                                         {
  61.                                             syntaxNodes.Add(method);
  62.                                         }
  63.                                         else if (method is FieldDeclarationSyntax)
  64.                                         {
  65.                                             // 注:常量不支持
  66.                                             syntaxNodes.Add(method);
  67.                                         }
  68.                                     }
  69.                                 }
  70.                                 var terms = termScanResult.Where(
  71.                                     f => f.ProjectName == document.Project.Name && f.ClassPath == document.FilePath).ToList();
  72.                                 backgroundWorker.ReportProgress(10, $"正在检查{document.FilePath}文件.");
  73.                                 ReplaceNodesAndSave(root, syntaxNodes, terms, result, backgroundWorker, document.Name);
  74.                             }
  75.                         }
  76.                     }
  77.                 }
  78.             }
  79.             catch (Exception ex)
  80.             {
  81.                 LogUtils.LogError(string.Format("异常类型:{0}\r\n异常消息:{1}\r\n异常信息:{2}\r\n",
  82.                     ex.GetType().Name, ex.Message, ex.StackTrace));
  83.                 backgroundWorker.ReportProgress(0, ex.Message);
  84.             }
  85.         }
  86.         public async void ReplaceNodesAndSave(SyntaxNode classSyntaxNode, List<MemberDeclarationSyntax> syntaxNodes, IEnumerable<TermScanResult> terms, Tuple<List<I18NTerm>, List<TermScanResult>> result,
  87.             System.ComponentModel.BackgroundWorker backgroundWorker, string className)
  88.         {
  89.             {//check pro是否存在词条
  90.                 if (AppConfig.Instance.IsCheckTermPro)
  91.                 {
  92.                     backgroundWorker.ReportProgress(15, $"词条验证中.");
  93.                     var termsCodes = terms.Select(f => f.I18NTerm.Code).ToList();
  94.                     var size = 100;
  95.                     var p = (result.Item2.Count() + size - 1) / size;
  96.                     using DBHelper dBHelper = new DBHelper();
  97.                     List<I18NTerm> items = new List<I18NTerm>();
  98.                     for (int i = 0; i < p; i++)
  99.                     {
  100.                         var list = termsCodes
  101.                             .Skip(i * size).Take(size);
  102.                         Thread.Sleep(10);
  103.                         var segmentItems = await dBHelper.GetTermsAsync(termsCodes).ConfigureAwait(false);
  104.                         items.AddRange(segmentItems);
  105.                     }
  106.                     List<TermScanResult> termScans = new List<TermScanResult>();
  107.                     foreach (var term in terms)
  108.                     {
  109.                         if (items.Any(f => f.Code == term.I18NTerm.Code))
  110.                         {
  111.                             termScans.Add(term);
  112.                         }
  113.                         else
  114.                         {
  115.                             backgroundWorker.ReportProgress(20, $"词条{term.OriginalText}未导入到词条库,该词条将忽略替换.");
  116.                         }
  117.                     }
  118.                     terms = termScans;
  119.                 }
  120.             }
  121.             var newclassDeclare = classSyntaxNode;
  122.             newclassDeclare = classSyntaxNode.ReplaceNodes(syntaxNodes,
  123.                     (methodDeclaration, _) =>
  124.                     {                     
  125.                         MemberDeclarationSyntax newMemberDeclarationSyntax = methodDeclaration;
  126.                         var className = ((ClassDeclarationSyntax)newMemberDeclarationSyntax.Parent).Identifier.Text;
  127.                         List<StatementSyntax> statementSyntaxes = new List<StatementSyntax>();
  128.                         switch (newMemberDeclarationSyntax)
  129.                         {
  130.                             case ConstructorDeclarationSyntax:
  131.                                 {
  132.                                     var blockSyntax = (newMemberDeclarationSyntax as ConstructorDeclarationSyntax).NormalizeWhitespace().Body;
  133.                                     if (blockSyntax == null)
  134.                                     {
  135.                                         break;
  136.                                     }
  137.                                     foreach (var statement in blockSyntax.Statements)
  138.                                     {
  139.                                         var nodeStatement = statement.DescendantNodes();
  140.                                         statementSyntaxes.Add(new CodeReplace().ReplaceStatementNodes(statement,
  141.                                             new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms));
  142.                                     }
  143.                                     break;
  144.                                 }
  145.                             case MethodDeclarationSyntax:
  146.                                 {
  147.                                     var blockSyntax = (methodDeclaration as MethodDeclarationSyntax).NormalizeWhitespace().Body;
  148.                                     if (blockSyntax == null)
  149.                                     {
  150.                                         break;
  151.                                     }
  152.                                     foreach (var statement in blockSyntax.Statements)
  153.                                     {
  154.                                         var nodeStatement = statement.DescendantNodes();
  155.                                         statementSyntaxes.Add(new CodeReplace().ReplaceStatementNodes(statement,
  156.                                                new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms));
  157.                                     }
  158.                                     break;
  159.                                 }
  160.                             case PropertyDeclarationSyntax:
  161.                                 {
  162.                                     var propertyDeclarationSyntax = newMemberDeclarationSyntax as PropertyDeclarationSyntax;
  163.                                     var nodeStatement = propertyDeclarationSyntax.DescendantNodes();
  164.                                     return new CodeReplace().ReplacePropertyNodes(newMemberDeclarationSyntax as PropertyDeclarationSyntax,
  165.                                         new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms);
  166.                                 }
  167.                             case FieldDeclarationSyntax:
  168.                                 {
  169.                                     var fieldDeclarationSyntax = newMemberDeclarationSyntax as FieldDeclarationSyntax;
  170.                                     var nodeStatement = fieldDeclarationSyntax.DescendantNodes();
  171.                                     return new CodeReplace().ReplaceFiledNodes(fieldDeclarationSyntax,
  172.                                            new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms);
  173.                                 }
  174.                         }
  175.                         backgroundWorker.ReportProgress(50, $"解析并对类文件{className}中的方法做语句替换.");
  176.                         // 替换方法内部
  177.                         if (newMemberDeclarationSyntax is MethodDeclarationSyntax)
  178.                         {
  179.                             return new CodeReplace().ReplaceMethodDeclaration(newMemberDeclarationSyntax as MethodDeclarationSyntax, statementSyntaxes);
  180.                         }
  181.                         else if (newMemberDeclarationSyntax is ConstructorDeclarationSyntax)
  182.                         {
  183.                             return new CodeReplace().ReplaceConstructorDeclaration(newMemberDeclarationSyntax as ConstructorDeclarationSyntax, statementSyntaxes);
  184.                         }
  185.                         return newMemberDeclarationSyntax;
  186.                     });
  187.             var sourceStr = newclassDeclare.NormalizeWhitespace().GetText().ToString();
  188.             File.WriteAllText(newclassDeclare.SyntaxTree.FilePath, sourceStr);
  189.             backgroundWorker.ReportProgress(100, $"完成{className}的替换.");
  190.         }
  191.     }
复制代码
关键的代码语义替换的实现代码:
  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.             var I18Expr = "";
  7.             var interpolationSyntaxes = syntaxNode.DescendantNodes().OfType<InterpolationSyntax>();         
  8.             var term = terms.FirstOrDefault(i => i.ChineseText == originalText);
  9.             if (term == null)
  10.                 return syntaxNode;
  11.             string termcode = term.I18NTerm.Code;
  12. if (syntaxNode is InterpolatedStringExpressionSyntax)
  13.             {
  14.                 if (interpolationSyntaxes.Count() > 0)
  15.                 {
  16.                     var parms = "";
  17.                     foreach (var item in interpolationSyntaxes)
  18.                     {
  19.                         parms += $",{item.ToString().TrimStart('{').TrimEnd('}')}";
  20.                     }
  21.                     I18Expr = "$"{T.Core.I18N.Service.TermService.Current.GetTextFormatted("" + termcode + "", " + originalText + parms + ")}"";
  22.                     var token1 = SyntaxFactory.Token(default, SyntaxKind.StringLiteralToken, I18Expr, "", default);
  23.                     return SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, token1);
  24.                 }
  25.                 else
  26.                 {
  27.                     var startToken = SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken);
  28.                     if ((syntaxNode as InterpolatedStringExpressionSyntax).StringStartToken.Value == startToken.Value)
  29.                     {
  30.                         // 如果本身有"$"
  31.                         I18Expr = "$"{T.Core.I18N.Service.TermService.Current.GetText("" + termcode + ""," + originalText + ")}";
  32.                     }
  33.                     else
  34.                     {
  35.                         // 如果没有"$"
  36.                         I18Expr = "$"{T.Core.I18N.Service.TermService.Current.GetText("" + termcode + "",\\teld"" + originalText + "")}";
  37.                         I18Expr = I18Expr.Replace("\\teld", "$");
  38.                     }
  39.                 }
  40.             }
  41.             else
  42.             {
  43.                 I18Expr = "$"{T.Core.I18N.Service.TermService.Current.GetText("" + termcode + ""," + originalText + ")}";
  44.             }
  45.             var token = SyntaxFactory.Token(default(SyntaxTriviaList), SyntaxKind.InterpolatedVerbatimStringStartToken, I18Expr, "$"", default(SyntaxTriviaList));
  46.             var literalExpressionSyntax = SyntaxFactory.InterpolatedStringExpression(token);
  47.             return literalExpressionSyntax;
  48.         }
复制代码
  1. T.Core.I18N.Service.TermService这个就是多语言词条服务类,这个类中提供了一个GetText的方法,通过词条编号,获取多语言文本。<br><br>代码完成替换后,打开VS,对工程引用多语言词条服务的Nuget包/dll,重新编译代码,手工校对替换后的代码即可。<br>以上是.NET应用系统的国际化-基于Roslyn抽取词条、更新代码的分享。<br><br><br>周国庆<br>2023/3/19<br><br><br><br>
复制代码
[code][/code]
来源:https://www.cnblogs.com/tianqing/archive/2023/03/19/17232474.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具