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; }
}

/// <summary>
/// 提取元数据
/// </summary>
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<InterpolationFormatClauseSyntax>().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; } } /// <summary> /// 替换表达式 /// </summary> public override void Replace() { var ies = Expression.GetParent<InvocationExpressionSyntax>();//当前表达式是不是方法调用 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<InterpolatedStringContentSyntax>();//字符串插值,要转成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<InterpolatedStringContentSyntax>().AddRange(list) , Expression.StringEndToken) .WithTriviaFrom(Expression); } } }
  • 用CSharpSyntaxRewriter替换
   public class ReplaceRewriter : CSharpSyntaxRewriter
    {
        private readonly Translater translater; public readonly HashSet<Lang> Langs = new HashSet<Lang>(); public ReplaceRewriter(Translater translater) { this.translater = translater; LiteralCodeNodes = ToDictionaryEx<LiteralCodeNode>(); InterpolatedCodeNodes = ToDictionaryEx<InterpolatedCodeNode>(); } private Dictionary<string, T> ToDictionaryEx<T>() where T : CsCodeNode { return translater.CodeNodes.Items.OfType<T>().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<string, InterpolatedCodeNode> InterpolatedCodeNodes { get; set; } public Dictionary<string, LiteralCodeNode> 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翻译):

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM