工具效果如图:

unity如何在image上制作表格 unity生成excel_接入百度翻译

 多语言是个非常简单且常用的功能。但是重复工作量大,程序手动把多语言Key配置到多语言表经常会出现错漏,或者几经改版,有些Key已经不用却没有剔除,久而久之造成冗余。这中简单且重复的工作必须让工具来完成。

功能设计:

多语言通过Key,Value的形式保存,通过多语言API GF.Localization.GetText(Key)获取当前语言对应的Value值。

1. 一键扫描多语言文本。扫描prefab资源、excel数据表以及代码里的多语言文本,这里扫描的就是多语言的Key。

2. 多语言列表(添加到此列表即为支持该语言)。点击"+"号弹出未添加的语言列表,点击对应语言添加到语言列表。多语言列表的第一项记为“母语”,其它语言以“母语”为基准翻译为对应语言。

3. 一键翻译。由于ChatGPT请求次数有限制,Google翻译需要魔法上网。最终为了体验选择了接入百度翻译。我们只需要把“母语”的Value填写好,其它语言直接通过百度翻译生成Value。

4. 由于机器翻译结果还需要人工审核修正。为了方便,工具先生成多语言Excel文件,方便交给其它部门翻译。项目真正使用的多语言文件是工具将多语言Excel导出的json文件。

5. 多语言工具以列表的形式显示“母语”,可以手动修改Key,Value值。

6. 细节体验优化。由于每次扫描结果会覆盖原多语言文件,可以通过勾选【锁定】强制保留该行。同时也在Excel的第一列生成了【锁定】勾选框方便策划操作。


unity如何在image上制作表格 unity生成excel_unity_02

多语言”母语“


unity如何在image上制作表格 unity生成excel_unity_03

基于”母语“自动生成/翻译的其它语言

 7. 由于百度翻译免费翻译字节数有上限,为了节省翻译字节。一键翻译默认只翻译Value值为空白的行,如果想强制翻译所有行可以通过一键翻译的下拉按钮强制翻译全部行。

unity如何在image上制作表格 unity生成excel_unity_04


unity如何在image上制作表格 unity生成excel_接入百度翻译_05

一键生成的多语言Excel


unity如何在image上制作表格 unity生成excel_百度翻译_06

自动导出多语言Excel为json文件

功能实现:

1. 一键扫描多语言文本:

①扫描Prefab资源上的多语言文本:

GameFramework框架提供了UIStringKey专门用来填写多语言文本Key, 所以只需要从所有Prefab上获取UIStringKey脚本上填写的Key即可。

unity如何在image上制作表格 unity生成excel_unity_07

 扫描prefab上的多语言Key:

/// <summary>
        /// 扫描Prefab中的国际化语言
        /// </summary>
        public static List<string> ScanLocalizationTextFromPrefab(Action<string, int, int> onProgressUpdate = null)
        {
            var assetGUIDs = AssetDatabase.FindAssets("t:Prefab", ConstEditor.PrefabsPath);
            List<string> keyList = new List<string>();
            int totalCount = assetGUIDs.Length;
            for (int i = 0; i < totalCount; i++)
            {
                string path = AssetDatabase.GUIDToAssetPath(assetGUIDs[i]);
                var pfb = AssetDatabase.LoadAssetAtPath<GameObject>(path);
                onProgressUpdate?.Invoke(path, totalCount, i);
                var keyArr = pfb.GetComponentsInChildren<UnityGameFramework.Runtime.UIStringKey>(true);
                foreach (var newKey in keyArr)
                {
                    if (string.IsNullOrWhiteSpace(newKey.Key) || keyList.Contains(newKey.Key)) continue;
                    keyList.Add(newKey.Key);
                }
            }
            return keyList;
        }

② 扫描数据表Excel中的多语言文本:

首先需要标记数据表多语言列,在数据表备注行用”i18n“标识,程序就自动扫描添加标识的列:

unity如何在image上制作表格 unity生成excel_游戏引擎_08

 扫描excel中的多语言文本:

/// <summary>
        /// 从DataTable Excel文件扫描本地化文本
        /// </summary>
        /// <param name="onProgressUpdate"></param>
        /// <returns></returns>
        public static List<string> ScanLocalizationTextFromDataTables(Action<string, int, int> onProgressUpdate = null)
        {
            List<string> keyList = new List<string>();
            var appConfig = AppConfigs.GetInstanceEditor();
            var mainTbFullFiles = GameDataGenerator.GameDataExcelRelative2FullPath(GameDataType.DataTable, appConfig.DataTables);
            var tbFullFiles = GameDataGenerator.GetGameDataExcelWithABFiles(GameDataType.DataTable, mainTbFullFiles);//同时扫描AB测试表
            for (int i = 0; i < tbFullFiles.Length; i++)
            {
                var excelFile = tbFullFiles[i];
                var fileInfo = new FileInfo(excelFile);
                if (!fileInfo.Exists) continue;

                onProgressUpdate?.Invoke(excelFile, tbFullFiles.Length, i);
                string tmpExcelFile = UtilityBuiltin.ResPath.GetCombinePath(fileInfo.Directory.FullName, GameFramework.Utility.Text.Format("{0}.temp", fileInfo.Name));
                try
                {
                    File.Copy(excelFile, tmpExcelFile, true);
                    using (var excelPackage = new ExcelPackage(tmpExcelFile))
                    {
                        var excelSheet = excelPackage.Workbook.Worksheets.FirstOrDefault();
                        if (excelSheet.Dimension.End.Row >= 4)
                        {
                            for (int colIndex = excelSheet.Dimension.Start.Column; colIndex <= excelSheet.Dimension.End.Column; colIndex++)
                            {
                                if (excelSheet.GetValue<string>(4, colIndex)?.ToLower() != EXCEL_I18N_TAG)
                                {
                                    continue;
                                }
                                for (int rowIndex = 5; rowIndex <= excelSheet.Dimension.End.Row; rowIndex++)
                                {
                                    string langKey = excelSheet.GetValue<string>(rowIndex, colIndex);
                                    if (string.IsNullOrWhiteSpace(langKey) || keyList.Contains(langKey)) continue;
                                    keyList.Add(langKey);
                                }
                            }

                        }
                    }
                }
                catch (Exception e)
                {
                    Debug.LogError($"扫描数据表本地化文本失败!文件:{excelFile}, Error:{e.Message}");
                }

                if (File.Exists(tmpExcelFile))
                {
                    File.Delete(tmpExcelFile);
                }
            }
            return keyList;
        }

③ 扫描代码中的多语言文本:

原理:搜索代码中所有调用国际化函数GF.Localization.GetText(string key)的地方,然后把调用时传入参数key的字符串值扫描出来。

首先只能通过静态解析cs代码,获取函数调用时传入参数的值。这比想象中复杂得多,比如:

1. 如果传入的是字符串常量很容易获取,但如果传入的是变量,就需要找到该变量的初始值赋值,变量又涉及到局部变量和全局变量。

2. 如果key中包含特殊字符会影响正则表达式的匹配,所以不能使用正则表达式。

3. 注释的代码不应该扫描。

为了工具安全完善,最终选择了用"高射炮打蚊子", 使用微软Roslyn作为CSharp静态解析库。但是这个解析库依赖dll太多直接导入Unity会有各种冲突,为了Unity工程的兼容性索性写个C#命令行程序,由Unity代码调用命令行程序扫描代码,把扫描结果存入缓存文件供Unity读取使用。而且命令行程序可以发布跨平台包,不用担心跨平台问题。

用Visual Studio新建C#命令行程序,为工程添加CodeAnalysis.CSharp库:

unity如何在image上制作表格 unity生成excel_百度翻译_09

 命令行程序代码:

其中命令行args, 第一参数是cs代码文件名(完整路径),第二个参数是扫描结果输出到的文件(通过文本追加的方式把扫描结果列表追加到文本文件),剩余参数是目标函数名,因为获取国际化文本的函数可能有多个。

internal class Program
    {
        static int Main(string[] args)
        {
            try
            {
                string csFile = args[0];
                string outputFile = args[1];
                List<string> funcNames = new List<string>();
                for (int i = 2; i < args.Length; i++)
                {
                    funcNames.Add(args[i]);
                }
                List<string> resultList = new List<string>();
                if ((File.GetAttributes(csFile) & FileAttributes.Directory) == FileAttributes.Directory)
                {
                    //如果传的是文件夹,扫描该文件夹下的所有cs文件
                    var csFiles = Directory.GetFiles(csFile, "*.cs", SearchOption.AllDirectories);
                    foreach (var item in csFiles)
                    {
                        var codeText = File.ReadAllText(item);
                        var strList = GetTextArgumentValues(codeText, funcNames);
                        if (strList.Count > 0)
                        {
                            resultList.AddRange(strList);
                        }
                    }

                }
                else
                {
                    if (File.Exists(csFile))
                    {
                        var codeText = File.ReadAllText(csFile);
                        var strList = GetTextArgumentValues(codeText, funcNames);
                        if (strList.Count > 0)
                        {
                            resultList.AddRange(strList);
                        }
                    }
                }

                resultList.Distinct();//去重
                resultList.RemoveAll(x => string.IsNullOrWhiteSpace(x));
                Console.WriteLine($"\n\n--------------Result List Count:{resultList.Count}--------------");
                for (int i = 0; i < resultList.Count; i++)
                {
                    var str = resultList[i];
                    Console.WriteLine($"{i + 1}.\t[{str}]");
                }
                Console.WriteLine("--------------Result List End--------------");
                if (resultList.Count > 0)
                {
                    File.AppendAllLines(outputFile, resultList);
                }
                return 0;
            }
            catch (Exception err)
            {
                Console.WriteLine($"Error:{err}");
            }
            return 1;
        }
        public static List<string> GetTextArgumentValues(string codeText, List<string> funcNames)
        {
            List<string> argumentValues = new List<string>();

            SyntaxTree tree = CSharpSyntaxTree.ParseText(codeText);

            var root = (CompilationUnitSyntax)tree.GetRoot();

            var methodCalls = root.DescendantNodes().OfType<InvocationExpressionSyntax>().Where(i =>
            {
                return funcNames.Contains(i.Expression.ToString());
            });
            var compilation = CSharpCompilation.Create(typeof(object).Assembly.FullName, new SyntaxTree[] { tree })
            .WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
            .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
            var semanticModel = compilation.GetSemanticModel(tree);


            var methodCallsArr = methodCalls.ToArray();
            for (int i = 0; i < methodCallsArr.Length; i++)
            {
                var call = methodCallsArr[i];
                var argumentList = call.ArgumentList;
                if (argumentList.Arguments.Count >= 1)
                {
                    var argExp = argumentList.Arguments[0].Expression;
                    if (argExp is LiteralExpressionSyntax literal)
                    {
                        Console.WriteLine($"{call} ------> {literal.Token.ValueText}");
                        argumentValues.Add(literal.Token.ValueText);
                    }
                    else if (argExp is IdentifierNameSyntax variable)
                    {
                        SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(variable);
                        if (symbolInfo.Symbol is IFieldSymbol fieldSymbol)
                        {
                            if (fieldSymbol.HasConstantValue)
                            {
                                argumentValues.Add((string)fieldSymbol.ConstantValue);
                                Console.WriteLine($"{call} ------> {fieldSymbol.ConstantValue}");
                            }
                        }
                        else if (symbolInfo.Symbol is ILocalSymbol localSymbol)
                        {
                            var localVar = localSymbol.DeclaringSyntaxReferences.Last()?.GetSyntax() as VariableDeclaratorSyntax;
                            if (localVar != null && localVar.Initializer != null)
                            {
                                var localVarValue = semanticModel.GetConstantValue(localVar.Initializer.Value);
                                if (localVarValue.Value != null)
                                {
                                    argumentValues.Add((string)localVarValue.Value);
                                    Console.WriteLine($"{call} ------> {localVarValue.Value}");
                                }
                            }
                        }
                    }
                }
            }

            return argumentValues;
        }
    }

2.  接入百度翻译开放API,实现一键翻译多语言

百度翻译官方接入文档:百度翻译开放平台

 注册后在开发者后台可以看到App id和密钥,用于发送翻译WebRequest请求参数。

开发者实名认证后可以变更为高级版,高级版每月可享受免费翻译100万个字符,相当于50万个汉字。一次请求能翻译6000个字符(3000汉字),每秒请求上限10次。

unity如何在image上制作表格 unity生成excel_unity_10

 以上限制就需要翻译时需要一次性塞入多条待翻译句子并且不能超过每次请求的上限字节。

比较坑的是百度翻译以换行符拆分句子,如果国际化文本中包含换行符翻译结果就不是我们想要的:

unity如何在image上制作表格 unity生成excel_百度翻译_11

 所以我使用一个特殊字符"↕"做为自己的多条句子之间的分割符,拿到翻译结果再用"↕"分割字符串得到句子数组。

百度翻译上行字段:

var randomCode = System.DateTime.Now.Ticks.ToString();
var strBuilder = new StringBuilder();
            strBuilder.Append(BAIDU_TRANS_URL);
            strBuilder.AppendFormat("q={0}", UnityWebRequest.EscapeURL(srcText));
            strBuilder.AppendFormat("&from={0}", GetBaiduLanguage(srcLang) ?? "auto"); //自动识别源文字语言
            strBuilder.AppendFormat("&to={0}", GetBaiduLanguage(targetLang));//翻译到目标语言
            strBuilder.AppendFormat("&appid={0}", EditorToolSettings.Instance.BaiduTransAppId);
            strBuilder.AppendFormat("&salt={0}", randomCode);
            strBuilder.AppendFormat("&sign={0}", GenerateBaiduSign(srcText, randomCode));

生成签名:

/// <summary>
        /// 生成百度翻译请求签名
        /// </summary>
        /// <param name="srcText"></param>
        /// <returns></returns>
        private static string GenerateBaiduSign(string srcText, string randomCode)
        {
            MD5 md5 = MD5.Create();
            var fullStr = GameFramework.Utility.Text.Format("{0}{1}{2}{3}", EditorToolSettings.Instance.BaiduTransAppId, srcText, randomCode, EditorToolSettings.Instance.BaiduTransSecretKey);
            byte[] byteOld = Encoding.UTF8.GetBytes(fullStr);
            byte[] byteNew = md5.ComputeHash(byteOld);
            StringBuilder sb = new StringBuilder();
            foreach (byte b in byteNew)
            {
                sb.Append(b.ToString("x2"));
            }
            return sb.ToString();
        }

百度翻译语言代号获取,用ChatGPT帮我生成函数,结果只有几种是对的,无奈只能人工找对照表修改代号:

中文首字母

名称

代码

语种检测

名称

代码

语种检测

名称

代码

语种检测

A

阿拉伯语

ara


爱尔兰语

gle


奥克语

oci


阿尔巴尼亚语

alb


阿尔及利亚阿拉伯语

arq


阿肯语

aka


阿拉贡语

arg


阿姆哈拉语

amh


阿萨姆语

asm


艾马拉语

aym


阿塞拜疆语

aze


阿斯图里亚斯语

ast


奥塞梯语

oss


爱沙尼亚语

est


奥杰布瓦语

oji


奥里亚语

ori


奥罗莫语

orm


B

波兰语

pl


波斯语

per


布列塔尼语

bre


巴什基尔语

bak


巴斯克语

baq


巴西葡萄牙语

pot


白俄罗斯语

bel


柏柏尔语

ber


邦板牙语

pam


保加利亚语

bul


北方萨米语

sme


北索托语

ped


本巴语

bem


比林语

bli


比斯拉马语

bis


俾路支语

bal


冰岛语

ice


波斯尼亚语

bos


博杰普尔语

bho


C

楚瓦什语

chv


聪加语

tso


D

丹麦语

dan


德语

de


鞑靼语

tat


掸语

sha


德顿语

tet


迪维希语

div


低地德语

log


E

俄语

ru


F

法语

fra


菲律宾语

fil


芬兰语

fin


梵语

san


弗留利语

fri


富拉尼语

ful


法罗语

fao


G

盖尔语

gla


刚果语

kon


高地索布语

ups


高棉语

hkm


格陵兰语

kal


格鲁吉亚语

geo


古吉拉特语

guj


古希腊语

gra


古英语

eno


瓜拉尼语

grn


H

韩语

kor


荷兰语

nl


胡帕语

hup


哈卡钦语

hak


海地语

ht


黑山语

mot


豪萨语

hau


J

吉尔吉斯语

kir


加利西亚语

glg


加拿大法语

frn


加泰罗尼亚语

cat


捷克语

cs


K

卡拜尔语

kab


卡纳达语

kan


卡努里语

kau


卡舒比语

kah


康瓦尔语

cor


科萨语

xho


科西嘉语

cos


克里克语

cre


克里米亚鞑靼语

cri


克林贡语

kli


克罗地亚语

hrv


克丘亚语

que


克什米尔语

kas


孔卡尼语

kok


库尔德语

kur


L

拉丁语

lat


老挝语

lao


罗马尼亚语

rom


拉特加莱语

lag


拉脱维亚语

lav


林堡语

lim


林加拉语

lin


卢干达语

lug


卢森堡语

ltz


卢森尼亚语

ruy


卢旺达语

kin


立陶宛语

lit


罗曼什语

roh


罗姆语

ro


逻辑语

loj


M

马来语

may


缅甸语

bur


马拉地语

mar


马拉加斯语

mg


马拉雅拉姆语

mal


马其顿语

mac


马绍尔语

mah


迈蒂利语

mai


曼克斯语

glv


毛里求斯克里奥尔语

mau


毛利语

mao


孟加拉语

ben


马耳他语

mlt


苗语

hmn


N

挪威语

nor


那不勒斯语

nea


南恩德贝莱语

nbl


南非荷兰语

afr


南索托语

sot


尼泊尔语

nep


P

葡萄牙语

pt


旁遮普语

pan


帕皮阿门托语

pap


普什图语

pus


Q

齐切瓦语

nya


契维语

twi


切罗基语

chr


R

日语

jp


瑞典语

swe


S

萨丁尼亚语

srd


萨摩亚语

sm


塞尔维亚-克罗地亚语

sec


塞尔维亚语

srp


桑海语

sol


僧伽罗语

sin


世界语

epo


书面挪威语

nob


斯洛伐克语

sk


斯洛文尼亚语

slo


斯瓦希里语

swa


塞尔维亚语(西里尔)

src


索马里语

som


T

泰语

th


土耳其语

tr


塔吉克语

tgk


泰米尔语

tam


他加禄语

tgl


提格利尼亚语

tir


泰卢固语

tel


突尼斯阿拉伯语

tua


土库曼语

tuk


W

乌克兰语

ukr


瓦隆语

wln


威尔士语

wel


文达语

ven


沃洛夫语

wol


乌尔都语

urd


X

西班牙语

spa


希伯来语

heb


希腊语

el


匈牙利语

hu


西弗里斯语

fry


西里西亚语

sil


希利盖农语

hil


下索布语

los


夏威夷语

haw


新挪威语

nno


西非书面语

nqo


信德语

snd


修纳语

sna


宿务语

ceb


叙利亚语

syr


巽他语

sun


Y

英语

en


印地语

hi


印尼语

id


意大利语

it


越南语

vie


意第绪语

yid


因特语

ina


亚齐语

ach


印古什语

ing


伊博语

ibo


伊多语

ido


约鲁巴语

yor


亚美尼亚语

arm


伊努克提图特语

iku


伊朗语

ir


Z

中文(简体)

zh


中文(繁体)

cht


中文(文言文)

wyw


中文(粤语)

yue


扎扎其语

zaz


中古法语

frm


祖鲁语

zul


爪哇语

jav


无私献上获取百度翻译语言代码:

/// <summary>
        /// 根据语言类型返回对应的百度语言缩写
        /// </summary>
        /// <param name="lang"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        public static string GetBaiduLanguage(Language lang)
        {
            switch (lang)
            {
                case Language.Afrikaans:
                    return "afr";
                case Language.Albanian:
                    return "alb";
                case Language.Arabic:
                    return "ara";
                case Language.Basque:
                    return "baq";
                case Language.Belarusian:
                    return "bel";
                case Language.Bulgarian:
                    return "bul";
                case Language.Catalan:
                    return "cat";
                case Language.ChineseSimplified:
                    return "zh";
                case Language.ChineseTraditional:
                    return "cht";
                case Language.Croatian:
                    return "hrv";
                case Language.Czech:
                    return "cs";
                case Language.Danish:
                    return "dan";
                case Language.Dutch:
                    return "nl";
                case Language.English:
                    return "en";
                case Language.Estonian:
                    return "est";
                case Language.Faroese:
                    return "fao";
                case Language.Finnish:
                    return "fin";
                case Language.French:
                    return "fra";
                case Language.Georgian:
                    return "geo";
                case Language.German:
                    return "de";
                case Language.Greek:
                    return "el";
                case Language.Hebrew:
                    return "heb";
                case Language.Hungarian:
                    return "hu";
                case Language.Icelandic:
                    return "ice";
                case Language.Indonesian:
                    return "id";
                case Language.Italian:
                    return "it";
                case Language.Japanese:
                    return "jp";
                case Language.Korean:
                    return "kor";
                case Language.Latvian:
                    return "lav";
                case Language.Lithuanian:
                    return "lit";
                case Language.Macedonian:
                    return "mac";
                case Language.Malayalam:
                    return "may";
                case Language.Norwegian:
                    return "nor";
                case Language.Persian:
                    return "per";
                case Language.Polish:
                    return "pl";
                case Language.PortugueseBrazil:
                    return "pt";
                case Language.PortuguesePortugal:
                    return "pt";
                case Language.Romanian:
                    return "rom";
                case Language.Russian:
                    return "ru";
                case Language.SerboCroatian:
                    return "sec";
                case Language.SerbianCyrillic:
                    return "src";
                case Language.SerbianLatin:
                    return "srp";
                case Language.Slovak:
                    return "sk";
                case Language.Slovenian:
                    return "slo";
                case Language.Spanish:
                    return "spa";
                case Language.Swedish:
                    return "swe";
                case Language.Thai:
                    return "th";
                case Language.Turkish:
                    return "tr";
                case Language.Ukrainian:
                    return "ukr";
                case Language.Vietnamese:
                    return "vie";
                default:
                    throw new NotSupportedException($"暂不支持该语言:{lang}");
            }
        }

接入百度翻译示例代码:

private static void TranslateAndSave(List<LocalizationText> mainLangTexts, Language srcLang, List<LocalizationText> langTexts, Language targetLang, bool forceAll)
        {
            int curTransIdx = 0;
            while (curTransIdx < langTexts.Count)
            {
                string totalText = "";
                List<int> totalTextIdx = new List<int>();
                for (; curTransIdx < langTexts.Count; curTransIdx++)
                {
                    var text = langTexts[curTransIdx];
                    string srcText = "";
                    if (forceAll)
                    {
                        var mainText = mainLangTexts.FirstOrDefault(tmpItm => tmpItm.Key.CompareTo(text.Key) == 0);
                        if (mainText != null && !string.IsNullOrWhiteSpace(mainText.Value))
                        {
                            srcText = mainText.Value;
                        }
                    }
                    else
                    {
                        if (string.IsNullOrWhiteSpace(text.Value))
                        {
                            var mainText = mainLangTexts.FirstOrDefault(tmpItm => tmpItm.Key.CompareTo(text.Key) == 0);
                            if (mainText != null && !string.IsNullOrWhiteSpace(mainText.Value))
                            {
                                srcText = mainText.Value;
                            }
                        }
                    }
                    if (!string.IsNullOrWhiteSpace(srcText))
                    {
                        if ((totalText.Length + srcText.Length) > EditorToolSettings.Instance.BaiduTransMaxLength)
                        {
                            curTransIdx -= 1; //如果长度超了下个请求接着这行
                            break;
                        }
                        totalText += srcText + TRANS_SPLIT_TAG;
                        totalTextIdx.Add(curTransIdx);
                    }
                }
                if (string.IsNullOrWhiteSpace(totalText))
                {
                    curTransIdx++;//如果一行字数就超过上限则跳过翻译这行
                    continue;
                }
                totalText = totalText.Substring(0, totalText.Length - TRANS_SPLIT_TAG.Length);//去掉结分隔符
                TMP_EditorCoroutine.StartCoroutine(TranslateCoroutine(totalText, srcLang, targetLang, (success, trans, userDt) =>
                {
                    if (success)
                    {
                        ParseAndSaveTransResults(langTexts, targetLang, trans, userDt as int[]);
                    }
                }, totalTextIdx.ToArray()));
            }
        }
        /// <summary>
        /// 解析翻译结果并保存到语言Excel
        /// </summary>
        /// <param name="targetTexts"></param>
        /// <param name="targetLang"></param>
        /// <param name="resultStr"></param>
        /// <param name="resultTextIdxArr"></param>
        private static void ParseAndSaveTransResults(List<LocalizationText> targetTexts, Language targetLang, TranslationResult trans, int[] resultTextIdxArr)
        {
            if (string.IsNullOrWhiteSpace(trans.dst) || resultTextIdxArr == null) return;
            var srcTexts = trans.src.Split(TRANS_SPLIT_TAG);
            var resultTexts = trans.dst.Split(TRANS_SPLIT_TAG);
            if (resultTexts.Length != resultTextIdxArr.Length || resultTexts.Length != srcTexts.Length)
            {
                Debug.LogError($"翻译失败, 翻译结果数量和索引数不一致.result count:{resultTexts.Length}, but index count:{resultTextIdxArr.Length}\n 翻译结果:{trans.dst}");
                return;
            }
            for (int i = 0; i < resultTextIdxArr.Length; i++)
            {
                var idx = resultTextIdxArr[i];
                var srcStr = srcTexts[i];
                var dstStr = resultTexts[i].Trim();
                int leadingSpaces = srcStr.Length - srcStr.TrimStart().Length;
                int trailingSpaces = srcStr.Length - srcStr.TrimEnd().Length;

                dstStr = dstStr.PadLeft(dstStr.Length + leadingSpaces);
                dstStr = dstStr.PadRight(dstStr.Length + trailingSpaces);
                targetTexts[idx].Value = dstStr;
            }

            SaveLanguage(targetLang, targetTexts);
        }
        private static IEnumerator TranslateCoroutine(string srcText, Language srcLang, Language targetLang, Action<bool, TranslationResult, object> onComplete, object userData)
        {
            var randomCode = System.DateTime.Now.Ticks.ToString();

            var strBuilder = new StringBuilder();
            strBuilder.Append(BAIDU_TRANS_URL);
            strBuilder.AppendFormat("q={0}", UnityWebRequest.EscapeURL(srcText));
            strBuilder.AppendFormat("&from={0}", GetBaiduLanguage(srcLang) ?? "auto"); //自动识别源文字语言
            strBuilder.AppendFormat("&to={0}", GetBaiduLanguage(targetLang));//翻译到目标语言
            strBuilder.AppendFormat("&appid={0}", EditorToolSettings.Instance.BaiduTransAppId);
            strBuilder.AppendFormat("&salt={0}", randomCode);
            strBuilder.AppendFormat("&sign={0}", GenerateBaiduSign(srcText, randomCode));

            //Debug.Log($"发送:{strBuilder}");
            // 发送请求
            using (var webRequest = UnityEngine.Networking.UnityWebRequest.Get(strBuilder.ToString()))
            {
                webRequest.SetRequestHeader("Content-Type", "text/html;charset=UTF-8");
                webRequest.certificateHandler = new WebRequestCertNoValidate();
                webRequest.SendWebRequest();
                while (!webRequest.isDone) yield return null;

                if (webRequest.result != UnityEngine.Networking.UnityWebRequest.Result.Success)
                {
                    Debug.LogError($"---------翻译{targetLang}请求失败:{webRequest.error}---------");
                    onComplete?.Invoke(false, null, userData);
                }
                else
                {
                    var json = webRequest.downloadHandler.text;
                    //Debug.Log($"接收:{json}");
                    try
                    {
                        var responseJson = UtilityBuiltin.Json.ToObject<JObject>(json);
                        if (responseJson.ContainsKey("trans_result"))
                        {
                            var resultArray = responseJson["trans_result"].ToObject<TranslationResult[]>();
                            if (resultArray != null && resultArray.Length > 0)
                            {
                                var resultTrans = resultArray[0];
                                onComplete?.Invoke(true, resultTrans, userData);
                            }
                            else
                            {
                                Debug.LogError($"---------翻译{targetLang}失败:{responseJson}---------");
                                onComplete?.Invoke(false, null, userData);
                            }
                        }
                        else
                        {
                            Debug.LogError($"---------翻译{targetLang}失败:{responseJson}---------");
                            onComplete?.Invoke(false, null, userData);
                        }
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogError($"---------翻译{targetLang}返回数据解析失败:{e.Message}---------");
                        onComplete?.Invoke(false, null, userData);
                    }
                }
            }

        }

internal class TranslationResult
    {
        public string src;
        public string dst;
    }

工具完整代码参考:GitHub - sunsvip/GF_HybridCLR