工具效果如图:
多语言是个非常简单且常用的功能。但是重复工作量大,程序手动把多语言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值为空白的行,如果想强制翻译所有行可以通过一键翻译的下拉按钮强制翻译全部行。
功能实现:
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;}