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