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翻译):