/* * @Author: 白哉 * @Description: * @Date: 2025年05月26日 星期一 11:05:13 * @Modify: */ using System; using System.Collections.Generic; using System.Text.RegularExpressions; using Logic.CrashSight; using TMPro; using Unity.VisualScripting; using UnityEngine; using ColorUtility = UnityEngine.ColorUtility; namespace Logic.Multilingual { public enum MultilingualType { None = 0, ZH = 1, // 简体中文 / Simplified Chinese TDZH = 2, // 繁体中文 / Traditional Chinese EN = 3, // 英文 / English JP = 4, // 日语 / Japanese KR = 5, // 韩语 / Korean RU = 10, // 俄语 / Russian ES = 11, // 西班牙语 / Spanish PT = 12, // 葡萄牙语 / Portuguese FR = 13, // 法语 / French DE = 14, // 德语 / German ID = 15, // 印尼语 / Indonesian TH = 16, // 泰语 / Thai PL = 17, // 波兰语 / Polish VI = 18, // 越南语 / Vietnamese MS = 19, // 马来语 / Malay UK = 20, // 乌克兰语 / Ukrainian KZ = 21, // 哈萨克语 / Kazakh TR = 22, // 土耳其语 / Turkish IT = 23, // 意大利语 / Italian NL = 24, // 荷兰语 / Dutch FI = 25, // 芬兰语 / Finnish SV = 26, // 瑞典语 / Swedish NO = 27, // 挪威语 / Norwegian CS = 28, // 捷克语 / Czech HU = 29, // 匈牙利语 / Hungarian EL = 30, // 希腊语 / Greek RO = 31, // 罗马尼亚语 / Romanian ET = 32, // 爱沙尼亚语 / Estonian LT = 33, // 立陶宛语 / Lithuanian HR = 34, // 克罗地亚语 / Croatian SR = 35, // 塞尔维亚语 / Serbian SL = 36, // 斯洛文尼亚语 / Slovenian SK = 37, // 斯洛伐克语 / Slovak BE = 38, // 白俄罗斯语 / Belarusian HE = 39, // 希伯来语 / Hebrew BG = 40, // 保加利亚语 / Bulgarian UZ = 41, // 乌兹别克语 / Uzbek KY = 42, // 吉尔吉斯语 / Kyrgyz MN = 43, // 蒙古语 / Mongolian AR = 44, // 阿拉伯语 / Arabic DA = 45, // 丹麦语 / Danish TL = 46, // 菲律宾语 / Tagalog (Filipino) Custom = 999, // 自定义语种 / Custom Language Max = 1000, // 最大值 / Max value } public class MultilingualData : ScriptableObject { public List FontGroups = new List(); public List Items = new List(); public List TargetTypes = new List(); private Dictionary _itemDict; public Dictionary ItemDict => _itemDict; public string GetMultilingualStr(uint id, MultilingualType type) { if (_itemDict == null) RefreshDict(); if (_itemDict == null) return string.Empty; if (!_itemDict.TryGetValue(id, out var item)) return string.Empty; var ret = item.GetStrByType(type); // Fallback 链: // 1. 5 种主语言(除 EN):自己有就用自己,没有就直接返回空(不 fallback) // 2. EN:自己没有 → 兜底到 ZH // 3. 其他语言(含 Custom):自己没有 → 兜底到 EN,EN 还没有 → 兜底到 ZH // ZH 是终极兜底,自己不 fallback if (string.IsNullOrEmpty(ret) && NeedsEnglishFallback(type)) { ret = item.GetStrByType(MultilingualType.EN); } if (string.IsNullOrEmpty(ret) && NeedsChineseFallback(type)) { ret = item.GetStrByType(MultilingualType.ZH); } if (string.IsNullOrEmpty(ret)) return ret; ret = ResolveEmbeddedStringsRunning(ret, type); return ret; } // 是否需要回落英文:仅 5 种主语言(中英日韩繁)和 None/Max 不回落 private static bool NeedsEnglishFallback(MultilingualType type) { return type != MultilingualType.None && type != MultilingualType.Max && type != MultilingualType.ZH && type != MultilingualType.TDZH && type != MultilingualType.EN && type != MultilingualType.JP && type != MultilingualType.KR; } // 是否需要回落中文:除 ZH 自身外都需要(包括 EN 自己 — EN 没翻译时也兜底中文) // ZH/None/Max 不 fallback,避免无意义查询 private static bool NeedsChineseFallback(MultilingualType type) { return type != MultilingualType.None && type != MultilingualType.Max && type != MultilingualType.ZH; } public TMP_FontAsset GetMultilingualFont(uint fontId, MultilingualType type) { if (fontId == 0) return null; foreach (var group in FontGroups) { if (group.FontID != fontId) continue; return type switch { MultilingualType.ZH => group.ZHFont, MultilingualType.TDZH => group.TDZHFont, MultilingualType.EN => group.ENFont, MultilingualType.JP => group.JPFont, MultilingualType.KR => group.KRFont, MultilingualType.RU => group.RUFont, MultilingualType.ES => group.ESFont, MultilingualType.PT => group.PTFont, MultilingualType.FR => group.FRFont, MultilingualType.Custom => group.ENFont, // Custom 语种回退到 EN 字体 _ => null, }; } return null; } public uint GetFontGroupID(TMP_FontAsset font) { foreach (var group in FontGroups) { if (group.ZHFont == font) return group.FontID; } return 0; } public void RefreshDict() { if (_itemDict == null) _itemDict = new Dictionary(); if (_itemDict.Count == Items.Count) return; _itemDict.Clear(); foreach (var item in Items) _itemDict[item.ID] = item; } public MultilingualType GetSystemLanguageTargetMultilingual(MultilingualType type) { var index = (int)type - 1; if (index >= TargetTypes.Count) return type; return TargetTypes[index]; } public string GetMultilingualStrEditor(uint id, MultilingualType type) { if (_itemDict == null) RefreshDict(); if (_itemDict == null) return string.Empty; if (!_itemDict.TryGetValue(id, out var item)) return string.Empty; var ret = item.GetStrByType(type); if (string.IsNullOrEmpty(ret)) return ret; ret = ResolveEmbeddedStrings(ret, type); return ret; } // 获取字符串中嵌套的 ID 列表 (按对应语种的顺序) public List GetSubStringIdRunning(uint id, MultilingualType type) { var result = new List(); if (_itemDict == null) RefreshDict(); if (_itemDict == null) return result; if (!_itemDict.TryGetValue(id, out var item)) return result; var origin = item.GetStrByType(type); if (string.IsNullOrEmpty(origin)) return result; var regex = new Regex(@"\*\*<(?:!\[(\d+)\])?(\d+)>\*\*"); var matches = regex.Matches(origin); foreach (Match m in matches) { if (uint.TryParse(m.Groups[2].Value, out var subId)) { result.Add(subId); } } return result; } // ID 转 String (运行时) public string ResolveEmbeddedStringsRunning(string origin, MultilingualType type) { if (string.IsNullOrEmpty(origin)) return string.Empty; var regex = new Regex(@"\*\*<(?:!\[(\d+)\])?(\d+)>\*\*"); var result = regex.Replace(origin, m => { var prefixGroup = m.Groups[1]; // [n] 中的 n var idStr = m.Groups[2].Value; // id RefreshDict(); if (!uint.TryParse(idStr, out var subId)) return m.Value; if (_itemDict == null || !_itemDict.TryGetValue(subId, out var subItem)) return m.Value; var str = subItem.GetStrByType(type); // 检测嵌套:展开后的字符串不应再包含 **<...>** if (Regex.IsMatch(str, @"\*\*<.+?>\*\*")) { LogSystem.LogError($"ResolveEmbeddedStrings: 检测到嵌套引用,ID: {subId},内容: {str}"); return m.Value; } var hex = subItem.Color; if (string.IsNullOrEmpty(hex)) { var color = new Color(1f, 0.647f, 0f); hex = ColorUtility.ToHtmlStringRGB(color); } if (!hex.StartsWith("#")) hex = "#" + hex; if (!string.IsNullOrEmpty(subItem.Icon)) return $"{str}"; return $"{str}"; }); return result; } // ID 转 String public string ResolveEmbeddedStrings(string origin, MultilingualType type) { if (string.IsNullOrEmpty(origin)) return string.Empty; var regex = new Regex(@"\*\*<(?:!\[(\d+)\])?(\d+)>\*\*"); var result = regex.Replace(origin, m => { var prefixGroup = m.Groups[1]; // [n] 中的 n var idStr = m.Groups[2].Value; // id RefreshDict(); if (!uint.TryParse(idStr, out var subId)) return m.Value; if (_itemDict == null || !_itemDict.TryGetValue(subId, out var subItem)) return m.Value; var str = subItem.GetStrByType(type); // 检测嵌套:展开后的字符串不应再包含 **<...>** if (Regex.IsMatch(str, @"\*\*<.+?>\*\*")) { LogSystem.LogError($"ResolveEmbeddedStrings: 检测到嵌套引用,ID: {subId},内容: {str}"); return m.Value; } if (prefixGroup.Success) { return $"**<[{prefixGroup.Value}]{str}>**"; } else { return $"**<{str}>**"; } }); return result; } // String 转 ID public string UnResolveEmbeddedStrings(string origin, MultilingualType type) { if (string.IsNullOrEmpty(origin)) return string.Empty; var regex = new Regex(@"\*\*<(?:!\[(\d+)\])?(.+?)>\*\*"); var result = regex.Replace(origin, m => { var prefixGroup = m.Groups[1]; // [n] 中的 n var str = m.Groups[2].Value; // 内部字符串 // 已经是 id 则跳过 if (uint.TryParse(str, out _)) return m.Value; // 检测嵌套:内部字符串不应再包含 **<...>** if (Regex.IsMatch(str, @"\*\*<.+?>\*\*")) { LogSystem.LogError($"UnResolveEmbeddedStrings: 检测到嵌套引用,内容: {str}"); return m.Value; } RefreshDict(); // 在 _itemDict 中反查 id uint id = 0; bool found = false; foreach (var kv in _itemDict) { if (kv.Value.GetStrByType(type) == str) { id = kv.Key; found = true; break; } } if (!found) { LogSystem.LogError($"UnResolveEmbeddedStrings: 找不到字符串对应的ID,字符串: origin:{origin} str:{str}"); return m.Value; } if (prefixGroup.Success) { return $"**<[{prefixGroup.Value}]{id}>**"; } else { return $"**<{id}>**"; } }); return result; } /// /// 将其他语言的嵌入字符串对齐到中文已转换好的ID格式 /// zhResolved: 中文已经通过 UnResolveEmbeddedStrings 转换后的字符串(包含 **** 和 **<[n]id>**) /// otherOrigin: 其他语言的原始字符串(包含 **** 和 **<[n]str>**) /// type: 当前语言类型(用于日志) /// itemId: 当前item的ID(用于日志) /// public string AlignEmbeddedStringsToZH(string zhResolved, string otherOrigin, MultilingualType type, uint itemId) { if (string.IsNullOrEmpty(otherOrigin)) return otherOrigin; if (string.IsNullOrEmpty(zhResolved)) return otherOrigin; // 解析中文已转换的嵌入引用 var zhRegex = new Regex(@"\*\*<(?:!\[(\d+)\])?(\d+)>\*\*"); var zhMatches = zhRegex.Matches(zhResolved); // 构建中文的 n->完整匹配 字典(带前缀n的)和 无前缀的有序列表 var zhPrefixDict = new Dictionary(); // n -> **<[n]id>** var zhNoPrefixList = new List(); // **** 有序列表 foreach (Match m in zhMatches) { var prefixGroup = m.Groups[1]; var idStr = m.Groups[2].Value; if (prefixGroup.Success) { zhPrefixDict[prefixGroup.Value] = m.Value; // n -> **<[n]id>** } else { zhNoPrefixList.Add(m.Value); // **** } } // 解析其他语言的嵌入引用 var otherRegex = new Regex(@"\*\*<(?:!\[(\d+)\])?(.+?)>\*\*"); var otherMatches = otherRegex.Matches(otherOrigin); // 分类其他语言的匹配:带前缀n的 和 不带前缀的 var otherPrefixMatches = new List(); var otherNoPrefixMatches = new List(); foreach (Match m in otherMatches) { var prefixGroup = m.Groups[1]; var content = m.Groups[2].Value; // 如果内容已经是纯数字(id),跳过 if (uint.TryParse(content, out _)) continue; if (prefixGroup.Success) { otherPrefixMatches.Add(m); } else { otherNoPrefixMatches.Add(m); } } // 从后往前替换,避免索引偏移 // 收集所有需要替换的操作 var replacements = new List<(int index, int length, string replacement)>(); // 1. 处理带前缀 **<[n]str>** 的情况:使用中文对应n的 **<[n]id>** foreach (var m in otherPrefixMatches) { var n = m.Groups[1].Value; if (zhPrefixDict.TryGetValue(n, out var zhReplacement)) { replacements.Add((m.Index, m.Length, zhReplacement)); } else { LogSystem.LogError( $"AlignEmbeddedStringsToZH: 语言{type} ID:{itemId} 中文找不到前缀[{n}]对应的引用,删除此项: {m.Value}"); replacements.Add((m.Index, m.Length, string.Empty)); } } // 2. 处理不带前缀 **** 的情况:按顺序对齐中文的 **** int zhCount = zhNoPrefixList.Count; int otherCount = otherNoPrefixMatches.Count; if (zhCount != otherCount) { LogSystem.LogError( $"AlignEmbeddedStringsToZH: 语言{type} ID:{itemId} ****数量不匹配,中文:{zhCount} 当前语言:{otherCount}"); } for (int i = 0; i < otherNoPrefixMatches.Count; i++) { var m = otherNoPrefixMatches[i]; if (i < zhCount) { // 对齐中文的 **** replacements.Add((m.Index, m.Length, zhNoPrefixList[i])); } else { // 中文少,删除其他语言多余的 **** replacements.Add((m.Index, m.Length, string.Empty)); } } // 按索引从后往前排序并替换 replacements.Sort((a, b) => b.index.CompareTo(a.index)); var result = otherOrigin; foreach (var (index, length, replacement) in replacements) { result = result.Substring(0, index) + replacement + result.Substring(index + length); } return result; } } [Serializable] public class MultilingualItem { public uint ID; public string ZH; public string TDZH; public string EN; public string JP; public string KR; public string RU; public string ES; public string PT; public string FR; public string Custom; // 自定义语种(仅供 Mod 应用使用) public bool IsProperNoun; public bool IsDialogue; public string DialogueSpeaker; public bool IsDeprecated; public bool IsCustom; public bool IsSpecialTerm; // 次要文案:版本说明 / 地理科普等不重要文本,导出时可整体排除以减少送翻量 public bool IsSecondary; // 默认橘色 public string Color; public string Icon; [NonSerialized] public string Desc; public MultilingualItem() { IsCustom = false; } public void Refresh() { ZH = ZH?.Replace("\r\n", "\n") ?? string.Empty; TDZH = TDZH?.Replace("\r\n", "\n") ?? string.Empty; EN = EN?.Replace("\r\n", "\n") ?? string.Empty; JP = JP?.Replace("\r\n", "\n") ?? string.Empty; KR = KR?.Replace("\r\n", "\n") ?? string.Empty; RU = RU?.Replace("\r\n", "\n") ?? string.Empty; ES = ES?.Replace("\r\n", "\n") ?? string.Empty; PT = PT?.Replace("\r\n", "\n") ?? string.Empty; FR = FR?.Replace("\r\n", "\n") ?? string.Empty; Custom = Custom?.Replace("\r\n", "\n") ?? string.Empty; DialogueSpeaker = DialogueSpeaker?.Replace("\r\n", "\n") ?? string.Empty; Desc = Desc?.Replace("\r\n", "\n") ?? string.Empty; } public string GetStrByType(MultilingualType type) { return type switch { MultilingualType.ZH => ZH, MultilingualType.TDZH => TDZH, MultilingualType.EN => EN, MultilingualType.JP => JP, MultilingualType.KR => KR, MultilingualType.RU => RU, MultilingualType.ES => ES, MultilingualType.PT => PT, MultilingualType.FR => FR, MultilingualType.Custom => Custom, _ => string.Empty, }; } public bool IsTranslate(MultilingualType type) { if (type == MultilingualType.ZH) return true; if (type == MultilingualType.None) { return !string.IsNullOrEmpty(TDZH) && !string.IsNullOrEmpty(EN) && !string.IsNullOrEmpty(JP) && !string.IsNullOrEmpty(KR) && !string.IsNullOrEmpty(RU) && !string.IsNullOrEmpty(ES) && !string.IsNullOrEmpty(PT) && !string.IsNullOrEmpty(FR); } return type switch { MultilingualType.TDZH => !string.IsNullOrEmpty(TDZH), MultilingualType.EN => !string.IsNullOrEmpty(EN), MultilingualType.JP => !string.IsNullOrEmpty(JP), MultilingualType.KR => !string.IsNullOrEmpty(KR), MultilingualType.RU => !string.IsNullOrEmpty(RU), MultilingualType.ES => !string.IsNullOrEmpty(ES), MultilingualType.PT => !string.IsNullOrEmpty(PT), MultilingualType.FR => !string.IsNullOrEmpty(FR), MultilingualType.Custom => !string.IsNullOrEmpty(Custom), _ => false, }; } // 不分大小写将 True 或者 False 字符串转换为布尔值 public static bool ParseBoolStr(string str) { return string.Equals(str, "True", StringComparison.OrdinalIgnoreCase); } } [Serializable] public class MultilingualFontGroup { public uint FontID; public TMP_FontAsset ZHFont; public TMP_FontAsset TDZHFont; public TMP_FontAsset ENFont; public TMP_FontAsset JPFont; public TMP_FontAsset KRFont; public TMP_FontAsset RUFont; public TMP_FontAsset ESFont; public TMP_FontAsset PTFont; public TMP_FontAsset FRFont; } }