【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的第一列生成了【锁定】勾选框方便策划操作。

多语言”母语“

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

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

一键生成的多语言Excel

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

功能实现:

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

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

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

 扫描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“标识,程序就自动扫描添加标识的列:

 扫描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库:

 命令行程序代码:

其中命令行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次。

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

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

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

百度翻译上行字段:

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

posted @ 2023-04-03 17:41  sunsvip  阅读(153)  评论(0)    收藏  举报  来源