2026-05-10 11:52:37 +08:00

581 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @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<MultilingualFontGroup> FontGroups = new List<MultilingualFontGroup>();
public List<MultilingualItem> Items = new List<MultilingualItem>();
public List<MultilingualType> TargetTypes = new List<MultilingualType>();
private Dictionary<uint, MultilingualItem> _itemDict;
public Dictionary<uint, MultilingualItem> 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自己没有 → 兜底到 ENEN 还没有 → 兜底到 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<uint, MultilingualItem>();
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<uint> GetSubStringIdRunning(uint id, MultilingualType type)
{
var result = new List<uint>();
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 $"<sprite name=\"{subItem.Icon}\"><color={hex}>{str}</color>";
return $"<color={hex}>{str}</color>";
});
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;
}
/// <summary>
/// 将其他语言的嵌入字符串对齐到中文已转换好的ID格式
/// zhResolved: 中文已经通过 UnResolveEmbeddedStrings 转换后的字符串(包含 **<id>** 和 **<[n]id>**
/// otherOrigin: 其他语言的原始字符串(包含 **<str>** 和 **<[n]str>**
/// type: 当前语言类型(用于日志)
/// itemId: 当前item的ID用于日志
/// </summary>
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<string, string>(); // n -> **<[n]id>**
var zhNoPrefixList = new List<string>(); // **<id>** 有序列表
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); // **<id>**
}
}
// 解析其他语言的嵌入引用
var otherRegex = new Regex(@"\*\*<(?:!\[(\d+)\])?(.+?)>\*\*");
var otherMatches = otherRegex.Matches(otherOrigin);
// 分类其他语言的匹配带前缀n的 和 不带前缀的
var otherPrefixMatches = new List<Match>();
var otherNoPrefixMatches = new List<Match>();
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. 处理不带前缀 **<str>** 的情况:按顺序对齐中文的 **<id>**
int zhCount = zhNoPrefixList.Count;
int otherCount = otherNoPrefixMatches.Count;
if (zhCount != otherCount)
{
LogSystem.LogError(
$"AlignEmbeddedStringsToZH: 语言{type} ID:{itemId} **<str>**数量不匹配,中文:{zhCount} 当前语言:{otherCount}");
}
for (int i = 0; i < otherNoPrefixMatches.Count; i++)
{
var m = otherNoPrefixMatches[i];
if (i < zhCount)
{
// 对齐中文的 **<id>**
replacements.Add((m.Index, m.Length, zhNoPrefixList[i]));
}
else
{
// 中文少,删除其他语言多余的 **<str>**
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;
}
}