Roslyn+T4+EnvDTE项目完全自动化(2) ——本地化代码
前言
以前做一个金融软件项目,软件要英文、繁体版本,开始甲方弄了好几个月,手动一条一条替换,发现很容易出错,因为有金融专业术语,字符串在不同语义要特殊处理,第三方工具没法使用。最后我用Roslyn写了一个工具,只需10分钟就能翻译整个软件
有很多工具可本地化代码,但这些插件只能机械的把字符串提取到资源文件,自动翻译也不准确,完全无法使用。因为项目有6w+个中文字符串,也不能一条条手动提取
-
resharper Localization Manager 的弊端:对于项目中存量6w+中文字符串,没法批量自动命名字符串、自动提取到资源文件、自动翻译对应的英文、无法识别代码语义
- 字符串插值,要转成string.Format,不然语义不完整
- 日志输出,不用本地化字符串
- 中文字符串,作为条件分支,不能本地化,实时切换语言时,代码逻辑会错乱,只能用资源文件key(替换成key值,不用再单元测试)
替换前
public class Stock { protected static readonly ILog logger = LogManager.GetLogger(typeof(Stock)); public void Test(string errorMsg, string type, Window window) { if (string.IsNullOrWhiteSpace(Account)) { //需要本地化 throw new Exception("账户不能为空!"); } //调用资源文件字符串 window.Title = "导出统计信息"; //调用资源文件字符串 MessageBox.Show("系统出现异常,请重新登录。", "提示"); //字符串插值,要转成string.Format,不然语义不完整 var msg = $"账号{Account}-{AccountId}触发了交易风控:{errorMsg}"; //日志输出,不用本地化字符串 logger.Error($"账号{Account}-{AccountId}需要修改密码:{errorMsg}"); //中文字符串,作为条件分支,不能本地化,只能用资源文件key switch (type) { case Stock.FixedPrice: break; case Stock.MarketPrice: break; case Stock.FollowPrice: break; case Stock.KnockOffPrice: break; case Stock.AmountOfMoney: break; } //中文字符串,作为条件分支,不能本地化,只能用资源文件key if (type == Stock.FixedPrice) { } //中文字符串,作为条件分支,不能本地化,只能用资源文件key if (type == "市价") { } } private const string FixedPrice = "限价"; private const string MarketPrice = "市价"; private const string FollowPrice = "跟价"; private const string KnockOffPrice = "拆价"; private const string AmountOfMoney = "金额"; public string AccountId { get; set; } public string Account { get; set; } }
替换后
public class Stock { protected static readonly ILog logger = LogManager.GetLogger(typeof(Stock)); public void Test(string errorMsg, string type, Window window) { if (string.IsNullOrWhiteSpace(Account)) { //需要本地化 throw new Exception(UiKey.Res_AccountCannotBeEmpty.GetResource()); } //调用资源文件字符串 window.Title = UiKey.Res_ExportStatistics.GetResource(); //调用资源文件字符串 MessageBox.Show(UiKey.Res_TheSystemIsAbnormalPleaseLogInAgain.GetResource(), UiKey.Res_Tips.GetResource()); //字符串插值,要转成string.Format,不然语义不完整 var msg = string.Format(UiKey.Res_AccountNumberTriggeredTransactionRiskControl_Format.GetResource(), Account, AccountId, errorMsg); //日志输出,不用本地化字符串 logger.Error($"账号{Account}-{AccountId}需要修改密码:{errorMsg}"); //中文字符串,作为条件分支,不能本地化,只能用资源文件key switch (type) { case Stock.FixedPrice: break; case Stock.MarketPrice: break; case Stock.FollowPrice: break; case Stock.KnockOffPrice: break; case Stock.AmountOfMoney: break; } //中文字符串,作为条件分支,不能本地化,只能用资源文件key if (type == Stock.FixedPrice) { } //中文字符串,作为条件分支,不能本地化,只能用资源文件key if (type == UiKey.Res_MarketPrice) { } } private const string FixedPrice = UiKey.Res_FixedPrice; private const string MarketPrice = UiKey.Res_MarketPrice; private const string FollowPrice = UiKey.Res_FollowPrice; private const string KnockOffPrice = UiKey.Res_KnockOffPrice; private const string AmountOfMoney = UiKey.Res_AmountOfMoney; public string AccountId { get; set; } public string Account { get; set; } }
////// 提取元数据 /// public override void Update() { try { _Langs.Clear(); session = new Session(); var i = 0; var buf = Expression.ChildNodes().ToList(); foreach (var item in buf)//字符串插值拆分 { { if (item is InterpolatedStringTextSyntax obj) { if (Reg.Singleton.FormatReg.IsMatch(obj.TextToken.ValueText)) { throw new Exception($"String.Format不能嵌套Interpolated:{obj}"); } session.Parts.Add(new Part { InterpolatedStringTextSyntax = obj, }); } } { if (item is InterpolationSyntax obj) { int index = i++; Part part = new Part { InterpolationSyntax = obj, Index = index, }; session.Parts.Add(part); var f = obj.DescendantNodes().OfType().FirstOrDefault(); part.Format = f?.ToFullString(); } } } if (buf.Count == 1 && i == 0) { NeedToLiteral = true; } else if (buf.Count == 2 && i == 1) { if (isFirst) { _IsStringFormat = false; _IsTrimLineBreak = true; var part = session.Parts.FirstOrDefault(p => p.InterpolatedStringTextSyntax != null); if (part != null) { var text = part.InterpolatedStringTextSyntax.TextToken.ValueText; if (!string.IsNullOrEmpty(text) && text.Length <= Helpers.SystemConfig.MaxTrimPunctuationLength) { _IsTrimPunctuation = true; } } } } if (buf.Count == 2 && i == buf.Count) { return; } if (Helpers.SystemConfig.IsDirectReferenceResources) { _IsTrimPunctuation = false; _IsTrimLineBreak = false; } RaiseChanged(); if (IsStringFormat) { var lang = currentNode.Dic.GetLang(session.Parts.GetFormat()); lang.Suffix = "Format"; _Langs.Add(lang); } else { foreach (var part in session.Parts.ToList()) { if (part.Index == null) { var text = part.InterpolatedStringTextSyntax.TextToken.ValueText; if (Reg.Singleton.CnRegex.IsMatch(text)) { if (IsTrimPunctuation || IsTrimLineBreak)//一新行翻译一条 { var lineBreakReg = Reg.Punctuation.LineBreakReg; var m = lineBreakReg.Match(text); var punctuation = Reg.Punctuation.GetPunctuationStr(lineBreakReg, m); string left = m.Groups[1].Value; string right = Reg.Punctuation.GetLineBreak(lineBreakReg, m); PunctuationHit.Result pun = null; if (!IsTrimPunctuation) { left += punctuation; } else { pun = Reg.Punctuation.GetPunctuation(left + punctuation); } if (!string.IsNullOrEmpty(left)) { var index = session.Parts.IndexOf(part); if (!string.IsNullOrEmpty(right)) { session.Parts.Insert(index, new Part { InterpolatedStringTextSyntax = right.EscapeAtString(Expression.StringStartToken.ValueText).InterpolatedText(), }); } var item = new Part { InterpolatedStringTextSyntax = left.InterpolatedText(), }; session.Parts.Insert(index, item); session.Parts.Remove(part); var lang = currentNode.Dic.GetLang(left); if (pun != null) { lang.WordsType = pun.WordsType; } item.Lang = lang; Langs.Add(lang); continue; } } { var lang = currentNode.Dic.GetLang(text); part.Lang = lang; Langs.Add(lang); } } } } } } catch (Exception ex) { ex.ShowUiError(); } finally { isFirst = false; } } /// /// 替换表达式 /// public override void Replace() { var ies = Expression.GetParent();//当前表达式是不是方法调用 if (ies != null) { var methodName = ies.ToRawString(); { var method = Helpers.SystemConfig.ExcludeMethod.GetLinesEx();//排除方法,直接跳过 if (method.Any(p => methodName.Contains(p))) { return ; } } } if (NeedToLiteral) { var lang = Langs.First(); ReplaceNode = Expression.TranslateNode(lang);//上下文判断是否调用资源文件,还是直接引用Key } else { var isOnlyEn = Expression.IsOnlyEn();//有些只能使用英文,翻译后不添加资源文件 if (IsStringFormat) { var lang = Langs.First(); ExpressionSyntax format; if (isOnlyEn) { format = lang.En.GetEnSyntax(); } else { if (Helpers.SystemConfig.IsDirectReferenceResources) { format = lang.Name.GetUiSyntax(); } else { format = lang.Name.GetResourceExpression(); } } var args = session.Parts .Where(p => p.Index != null) .Select(p => SyntaxFactory.Argument(p.InterpolationSyntax.Expression.WithoutTrivia()).LeadingTriviaSpace()) .ToList(); args.Insert(0, SyntaxFactory.Argument(format)); ReplaceNode = args.StringFormatSyntax(); } else { var list = new List ();//字符串插值,要转成string.Format foreach (var item in session.Parts) { if (item.Index == null) { if (item.Lang == null) { list.Add(item.InterpolatedStringTextSyntax); } else { InterpolationSyntax obj; if (isOnlyEn) { obj = item.Lang.En.GetEnSyntax().Interpolation(); } else { if (Helpers.SystemConfig.IsDirectReferenceResources) { obj = item.Lang.Name.GetUiSyntax().Interpolation(); } else { if (item.Lang.WordsType == null) { obj = item.Lang.Name.GetResourceExpression().Interpolation(); } else { obj = item.Lang.Name.GetResourceExpression(item.Lang.WordsType.Value).Interpolation(); } } } list.Add(obj); } } else { list.Add(item.InterpolationSyntax); } } ReplaceNode = SyntaxFactory.InterpolatedStringExpression( Expression.StringStartToken , new SyntaxList ().AddRange(list) , Expression.StringEndToken) .WithTriviaFrom(Expression); } } }
- 用CSharpSyntaxRewriter替换
public class ReplaceRewriter : CSharpSyntaxRewriter
{
private readonly Translater translater;
public readonly HashSet Langs = new HashSet();
public ReplaceRewriter(Translater translater)
{
this.translater = translater;
LiteralCodeNodes = ToDictionaryEx();
InterpolatedCodeNodes = ToDictionaryEx();
}
private Dictionary ToDictionaryEx() where T : CsCodeNode
{
return translater.CodeNodes.Items.OfType().Where(p => p.Langs.All(p1 => !String.IsNullOrEmpty(p1.En)))
.ToDictionaryEx(p => SyntaxHelper.ToRawString(p.SyntaxNode),
delegate (string key, T value, T existsValue, ref bool isThrow)
{
value.Langs.CheckEqualsLangs(existsValue.Langs);
isThrow = false;
});
}
public Dictionary InterpolatedCodeNodes { get; set; }
public Dictionary LiteralCodeNodes { get; set; }
public override SyntaxNode VisitLiteralExpression(LiteralExpressionSyntax node)
{
if (!node.IsKind(SyntaxKind.StringLiteralExpression))
{
goto Return;
}
LiteralCodeNode obj;
if (!LiteralCodeNodes.TryGetValue(node.ToRawString(), out obj) || obj.ReplaceNode == null)
{
goto Return;
}
//if (!obj.Expression.IsOnlyEn())
{
foreach (var lang in obj.Langs)
{
Langs.Add(lang);
}
}
return obj.ReplaceNode.WithTriviaFrom(node);
Return:
return base.VisitLiteralExpression(node);
}
public override SyntaxNode VisitInterpolatedStringExpression(InterpolatedStringExpressionSyntax node)
{
if (InterpolatedCodeNodes.Count == 0)
{
goto Return;
}
InterpolatedCodeNode obj;
if (!InterpolatedCodeNodes.TryGetValue(node.ToRawString(), out obj) || obj.ReplaceNode == null)
{
goto Return;
}
//if (!obj.Expression.IsOnlyEn())
{
foreach (var lang in obj.Langs)
{
Langs.Add(lang);
}
}
return obj.ReplaceNode.WithTriviaFrom(node);
Return:
return base.VisitInterpolatedStringExpression(node);
}
}
工具类
public static class Helpers
{
public static string GetResource(this string key)
{
return Test.Strings.Ui.ResourceManager.GetString(key, Test.Strings.Ui.Culture);
}
}
资源文件(专业术语,可以先导入资源文件。工具会先去资源文件找,没找到再调用百度api翻译):