Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of UniversalModLocalizer v0.1.13
UniversalModLocalizer.dll
Decompiled 3 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; using UniversalModLocalizer.Console; using UniversalModLocalizer.Core; using UniversalModLocalizer.Discovery; using UniversalModLocalizer.Localization; using UniversalModLocalizer.Networking; using UniversalModLocalizer.Persistence; using UniversalModLocalizer.Scanning; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("UniversalModLocalizer")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("UniversalModLocalizer")] [assembly: AssemblyTitle("UniversalModLocalizer")] [assembly: AssemblyVersion("1.0.0.0")] namespace UniversalModLocalizer { [BepInPlugin("com.michal.valheim.universalmodlocalizer", "UniversalModLocalizer", "0.1.13")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.michal.valheim.universalmodlocalizer"; public const string PluginName = "UniversalModLocalizer"; public const string PluginVersion = "0.1.13"; private const float ImportReloadDelay = 0.75f; private const float RuntimeFlushInterval = 8f; private const float NetworkTickInterval = 2f; private static ManualLogSource logger; private Harmony harmony; private LocalizationFileScanner localizationFileScanner; private PrefabScanner prefabScanner; private ImportManager importManager; private ExportManager exportManager; private RuntimeStringCollector runtimeCollector; private readonly Dictionary<string, string> runtimeTranslationCache = new Dictionary<string, string>(StringComparer.Ordinal); private readonly Dictionary<int, string> runtimeOwnerCache = new Dictionary<int, string>(); private bool importChanged; private float importReloadAt; private bool autoScanQueued; private bool autoScanDone; private float autoScanAt; private float nextRuntimeFlush; private float nextNetworkTick; private string activeLanguage; internal static Plugin Instance { get; private set; } internal Settings Settings { get; private set; } internal UmlPaths Paths { get; private set; } internal PluginDiscovery Discovery { get; private set; } internal ModOwnershipResolver OwnershipResolver { get; private set; } internal TranslationRegistry Translations { get; private set; } internal LocalizationNetworkManager Network { get; private set; } private void Awake() { //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Expected O, but got Unknown Instance = this; logger = ((BaseUnityPlugin)this).Logger; Paths = new UmlPaths(Paths.ConfigPath); Paths.Ensure(); VanillaTokenRegistry.Load(Paths.Root); Settings = new Settings(Paths.ConfigFilePath); activeLanguage = Settings.EffectiveLanguage(); Discovery = new PluginDiscovery(); Discovery.Discover(); OwnershipResolver = new ModOwnershipResolver(Discovery, Settings); OwnershipResolver.EnableJotunnModQuery(); Translations = new TranslationRegistry(Settings); localizationFileScanner = new LocalizationFileScanner(); prefabScanner = new PrefabScanner(Discovery, OwnershipResolver, Settings); importManager = new ImportManager(Paths, Settings); exportManager = new ExportManager(Paths, Settings); runtimeCollector = new RuntimeStringCollector(Paths, Settings, OwnershipResolver, Discovery); Network = new LocalizationNetworkManager(Settings, importManager); ReloadTranslations(); LocalizationCommands.Register(); harmony = new Harmony("com.michal.valheim.universalmodlocalizer"); harmony.PatchAll(); Network.TryRegister(); LogInfo("Loaded UniversalModLocalizer 0.1.13. Config root: " + Paths.Root); } private void OnDestroy() { try { runtimeCollector?.Flush(activeLanguage); importManager?.Dispose(); Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } } catch { } } private void Update() { if (Settings != null && Settings.Enabled.Value) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (importChanged && importReloadAt <= 0f) { importReloadAt = realtimeSinceStartup + 0.75f; } if (importChanged && realtimeSinceStartup >= importReloadAt) { importChanged = false; importReloadAt = 0f; ReloadTranslations(); } if (autoScanQueued && !autoScanDone && realtimeSinceStartup >= autoScanAt) { autoScanQueued = false; autoScanDone = true; RunFullScanAndExport(); } if (realtimeSinceStartup >= nextRuntimeFlush) { nextRuntimeFlush = realtimeSinceStartup + 8f; runtimeCollector.Flush(activeLanguage); } if (realtimeSinceStartup >= nextNetworkTick) { nextNetworkTick = realtimeSinceStartup + 2f; Network.Tick(activeLanguage); } } } internal void ReloadTranslations() { if (Settings != null) { activeLanguage = Settings.EffectiveLanguage(); List<TranslationEntry> list = importManager.Load(activeLanguage); Translations.ReplaceAll(list); ClearRuntimeCaches(); importManager.StartWatching(activeLanguage, MarkImportsChanged); LogInfo($"Loaded {list.Count} translation entries for {activeLanguage}."); } } internal ExportSummary RunFullScanAndExport(bool deepMode = false) { Stopwatch stopwatch = Stopwatch.StartNew(); string text = Settings.EffectiveLanguage(); LogInfo("Starting " + (deepMode ? "deep " : "") + "export scan for " + text + "."); List<ModInfoRecord> mods = Discovery.Discover(); List<LocalizationFileRecord> localizationRecords = localizationFileScanner.Scan(mods); List<TranslationCandidate> prefabCandidates = prefabScanner.Scan(deepMode); ExportSummary exportSummary = exportManager.Export(text, mods, localizationRecords, prefabCandidates, prefabScanner.Unresolved, deepMode); stopwatch.Stop(); LogInfo($"Scan complete for {text}: {exportSummary.ExportedEntries} missing entries from {exportSummary.ExportedMods} mods. Deep mode: {deepMode}. Duration: {stopwatch.Elapsed.TotalSeconds:F1}s. Export root: {exportSummary.ExportRoot}"); return exportSummary; } internal void OnWorldDataChanged(string source) { if (!autoScanDone && !autoScanQueued && Settings != null && Settings.AutoScanOnWorldLoad.Value) { autoScanQueued = true; autoScanAt = Time.realtimeSinceStartup + 3f; LogDebug("World data changed by " + source + ". Initial export scan queued."); } } internal void OnLanguageChanged(string language) { if (string.Equals(Settings?.TargetLanguage.Value, "Auto", StringComparison.OrdinalIgnoreCase)) { activeLanguage = (string.IsNullOrWhiteSpace(language) ? Settings.EffectiveLanguage() : language); MarkImportsChanged(); Network?.ResetClientRequest(); } } internal bool TranslateToken(string tokenWithoutDollar, string ownerGuid, out string translated) { translated = null; if (Settings.EnableImports.Value) { return Translations.TryTranslateToken(tokenWithoutDollar, ownerGuid, out translated); } return false; } internal bool TranslateRuntimeText(string source, Component owner, out string translated) { translated = null; if (!Settings.EnableImports.Value || !Settings.EnableHardcodedStringFallback.Value || string.IsNullOrWhiteSpace(source)) { return false; } string key = "global\n" + source; if (TryGetCachedRuntimeTranslation(key, out translated)) { return translated != null; } if (Translations.TryTranslateRuntime(source, "", out translated)) { CacheRuntimeTranslation(key, translated); return true; } CacheRuntimeTranslation(key, null); if (!Settings.EnableScopedRuntimeComponentLookup.Value || (Object)(object)owner == (Object)null) { return false; } string text = ResolveRuntimeOwnerGuid(owner); if (string.IsNullOrWhiteSpace(text)) { return false; } string key2 = text + "\n" + source; if (TryGetCachedRuntimeTranslation(key2, out translated)) { return translated != null; } if (Translations.TryTranslateRuntime(source, text, out translated)) { CacheRuntimeTranslation(key2, translated); return true; } CacheRuntimeTranslation(key2, null); return false; } internal void RecordRuntimeString(string source, Component owner) { if (Settings.ExportRuntimeStrings.Value) { runtimeCollector.Record(source, owner); } } internal string BuildServerPackageJson(string language) { string path = Paths.LanguageImportRoot(language); List<TranslationEntry> list = new List<TranslationEntry>(); if (Directory.Exists(path)) { foreach (string item in Directory.EnumerateFiles(path, "*.json", SearchOption.AllDirectories)) { list.AddRange(ImportManager.LoadFile(item)); } } return JsonConvert.SerializeObject((object)new TranslationDocument { schemaVersion = 1, modGuid = "_server_package", modName = "Server Translation Package", language = language, generatedAt = DateTime.UtcNow.ToString("O"), category = "server_package", entries = list.Select(ToDto).ToList() }, (Formatting)0); } private void MarkImportsChanged() { importChanged = true; } private string ResolveRuntimeOwnerGuid(Component owner) { int instanceID; try { instanceID = ((Object)owner).GetInstanceID(); } catch { return ""; } if (runtimeOwnerCache.TryGetValue(instanceID, out var value)) { return value; } OwnershipResult ownershipResult = OwnershipResolver.ResolveComponent(owner); string text = (ownershipResult.IsKnown ? ownershipResult.ModGuid : ""); runtimeOwnerCache[instanceID] = text; return text; } private bool TryGetCachedRuntimeTranslation(string key, out string translated) { return runtimeTranslationCache.TryGetValue(key, out translated); } private void CacheRuntimeTranslation(string key, string translated) { int num = Math.Max(1000, Settings.RuntimeTranslationCacheLimit.Value); if (runtimeTranslationCache.Count >= num) { runtimeTranslationCache.Clear(); } runtimeTranslationCache[key] = translated; } private void ClearRuntimeCaches() { runtimeTranslationCache.Clear(); runtimeOwnerCache.Clear(); } private static TranslationEntryDto ToDto(TranslationEntry entry) { return new TranslationEntryDto { modGuid = entry.ModGuid, modName = entry.ModName, key = entry.Key, source = entry.Source, translation = entry.Translation, category = entry.Category, origin = entry.Origin, sourceLanguage = entry.SourceLanguage, context = entry.Context, sourceHash = entry.SourceHash, status = entry.Status, ownerConfidence = entry.OwnerConfidence }; } internal static void LogInfo(string message) { ManualLogSource obj = logger; if (obj != null) { obj.LogInfo((object)message); } } internal static void LogWarning(string message) { ManualLogSource obj = logger; if (obj != null) { obj.LogWarning((object)message); } } internal static void LogDebug(string message) { if ((Object)(object)Instance == (Object)null || Instance.Settings == null) { ManualLogSource obj = logger; if (obj != null) { obj.LogDebug((object)message); } return; } string text = Instance.Settings.LogLevel.Value ?? "Info"; if (Instance.Settings.DiagnosticsMode.Value || text.Equals("Debug", StringComparison.OrdinalIgnoreCase) || text.Equals("Trace", StringComparison.OrdinalIgnoreCase)) { ManualLogSource obj2 = logger; if (obj2 != null) { obj2.LogInfo((object)("[Debug] " + message)); } } } } } namespace UniversalModLocalizer.Scanning { internal sealed class LocalizationFileRecord { public string ModGuid { get; set; } public string ModName { get; set; } public string Language { get; set; } public string Key { get; set; } public string Value { get; set; } public string SourceFile { get; set; } public string Format { get; set; } } internal sealed class LocalizationFileScanner { private static readonly HashSet<string> Extensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".json", ".yaml", ".yml", ".csv", ".tsv", ".txt" }; private static readonly string[] FolderTokens = new string[10] { "localization", "localisations", "localization", "translations", "translation", "languages", "language", "lang", "locale", "locales" }; public List<LocalizationFileRecord> Scan(IEnumerable<ModInfoRecord> mods) { List<LocalizationFileRecord> list = new List<LocalizationFileRecord>(); foreach (ModInfoRecord mod in mods) { if (mod == null || mod.IsUniversalModLocalizer || string.IsNullOrWhiteSpace(mod.RootDirectory) || !Directory.Exists(mod.RootDirectory)) { continue; } foreach (string item in SafeEnumerateFiles(mod.RootDirectory)) { string extension = Path.GetExtension(item); if (Extensions.Contains(extension) && IsLikelyLocalizationFile(item, mod.RootDirectory)) { try { list.AddRange(ParseFile(mod, item, extension)); } catch (Exception ex) { Plugin.LogWarning("Failed to parse localization candidate file '" + item + "': " + ex.Message); } } } } return (from x in list.Where((LocalizationFileRecord x) => !string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Value)).GroupBy<LocalizationFileRecord, string>((LocalizationFileRecord x) => x.ModGuid + "\n" + x.Language + "\n" + x.Key + "\n" + x.Value, StringComparer.Ordinal) select x.First()).ToList(); } private static IEnumerable<string> SafeEnumerateFiles(string root) { Stack<string> stack = new Stack<string>(); stack.Push(root); while (stack.Count > 0) { string path = stack.Pop(); IEnumerable<string> enumerable = Array.Empty<string>(); IEnumerable<string> dirs = Array.Empty<string>(); try { enumerable = Directory.EnumerateFiles(path); dirs = Directory.EnumerateDirectories(path); } catch { } foreach (string item in enumerable) { yield return item; } foreach (string item2 in dirs) { if (!Path.GetFileName(item2).Equals("AssetBundles", StringComparison.OrdinalIgnoreCase)) { stack.Push(item2); } } } } private static bool IsLikelyLocalizationFile(string file, string root) { string text = file.Substring(root.Length).TrimStart('\\', '/').ToLowerInvariant(); if (FolderTokens.Any(text.Contains)) { return true; } if (LanguageHelper.IsKnown(LanguageHelper.DetectFromPath(file))) { return true; } string text2 = Path.GetFileNameWithoutExtension(file).ToLowerInvariant(); if (!text2.Contains("translation") && !text2.Contains("localization") && !text2.Contains("localisation")) { return text2.Contains("language"); } return true; } private static IEnumerable<LocalizationFileRecord> ParseFile(ModInfoRecord mod, string file, string extension) { string text = File.ReadAllText(file); string fileLanguage = LanguageHelper.DetectFromPath(file); return extension.ToLowerInvariant() switch { ".json" => ParseJson(mod, file, text, fileLanguage), ".yaml" => ParseKeyValueLines(mod, file, text, fileLanguage, "yaml", ':'), ".yml" => ParseKeyValueLines(mod, file, text, fileLanguage, "yaml", ':'), ".csv" => ParseSeparated(mod, file, text, fileLanguage, "csv", ','), ".tsv" => ParseSeparated(mod, file, text, fileLanguage, "tsv", '\t'), ".txt" => ParseText(mod, file, text, fileLanguage), _ => Array.Empty<LocalizationFileRecord>(), }; } private static IEnumerable<LocalizationFileRecord> ParseJson(ModInfoRecord mod, string file, string text, string fileLanguage) { List<LocalizationFileRecord> list = new List<LocalizationFileRecord>(); JToken val; try { val = JToken.Parse(text); } catch { return ParseLooseJsonDictionary(mod, file, text, fileLanguage); } JObject val2 = (JObject)(object)((val is JObject) ? val : null); if (val2 != null) { JToken obj2 = val2["entries"]; JArray val3 = (JArray)(object)((obj2 is JArray) ? obj2 : null); if (val3 != null) { string language = LanguageHelper.Normalize((string)val2["language"]) ?? fileLanguage; { foreach (JObject item in ((IEnumerable)val3).OfType<JObject>()) { string key = FirstString(item, "key", "token", "id", "name"); string value = FirstString(item, "translation", "value", "text", "source"); if (!string.IsNullOrWhiteSpace(value)) { list.Add(Make(mod, file, language, key, value, "json")); } } return list; } } foreach (JProperty item2 in val2.Properties()) { string text2 = LanguageHelper.Normalize(item2.Name); if (LanguageHelper.IsKnown(text2)) { JToken value2 = item2.Value; JObject val4 = (JObject)(object)((value2 is JObject) ? value2 : null); if (val4 != null) { FlattenJsonObject(mod, file, text2, val4, "", list); } } } if (list.Count > 0) { return list; } FlattenJsonObject(mod, file, fileLanguage, val2, "", list); return list; } JArray val5 = (JArray)(object)((val is JArray) ? val : null); if (val5 != null) { foreach (JObject item3 in ((IEnumerable)val5).OfType<JObject>()) { string language2 = LanguageHelper.Normalize(FirstString(item3, "language", "lang")) ?? fileLanguage; string text3 = FirstString(item3, "key", "token", "id", "name"); string value3 = FirstString(item3, "translation", "value", "text", "source", "english"); if (!string.IsNullOrWhiteSpace(text3) && !string.IsNullOrWhiteSpace(value3)) { list.Add(Make(mod, file, language2, text3, value3, "json")); } } } return list; } private static IEnumerable<LocalizationFileRecord> ParseLooseJsonDictionary(ModInfoRecord mod, string file, string text, string fileLanguage) { List<LocalizationFileRecord> list = new List<LocalizationFileRecord>(); string language = (LanguageHelper.IsKnown(fileLanguage) ? fileLanguage : "English"); string[] array = text.Split(new string[2] { "\r\n", "\n" }, StringSplitOptions.None); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0 || text2 == "{" || text2 == "}" || text2.StartsWith("//", StringComparison.Ordinal) || text2.StartsWith("#", StringComparison.Ordinal)) { continue; } text2 = text2.TrimEnd(','); Match match = Regex.Match(text2, "^\"(?<key>(?:\\\\.|[^\"])*)\"\\s*:\\s*\"(?<value>(?:\\\\.|[^\"])*)\"\\s*$"); if (!match.Success) { match = Regex.Match(text2, "^(?<key>[^:=]+?)\\s*[:=]\\s*(?<value>.+?)\\s*$"); } if (match.Success) { string text3 = UnescapeLooseJson(match.Groups["key"].Value.Trim()); string text4 = UnescapeLooseJson(Unquote(match.Groups["value"].Value.Trim())); if (!string.IsNullOrWhiteSpace(text3) && TranslationRegistry.LooksTranslatable(text4)) { list.Add(Make(mod, file, language, text3, text4, "loose-json")); } } } return list; } private static void FlattenJsonObject(ModInfoRecord mod, string file, string language, JObject obj, string prefix, List<LocalizationFileRecord> output) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Invalid comparison between Unknown and I4 foreach (JProperty item in obj.Properties()) { string text = (string.IsNullOrEmpty(prefix) ? item.Name : (prefix + "." + item.Name)); if ((int)item.Value.Type == 8) { string text2 = (string)item.Value; if (TranslationRegistry.LooksTranslatable(text2)) { output.Add(Make(mod, file, language, text, text2, "json")); } continue; } JToken value = item.Value; JObject val = (JObject)(object)((value is JObject) ? value : null); if (val != null) { FlattenJsonObject(mod, file, language, val, text, output); } } } private static IEnumerable<LocalizationFileRecord> ParseText(ModInfoRecord mod, string file, string text, string fileLanguage) { List<LocalizationFileRecord> list = ParseKeyValueLines(mod, file, text, fileLanguage, "txt", '=').ToList(); if (list.Count <= 0) { return ParseKeyValueLines(mod, file, text, fileLanguage, "txt", ':'); } return list; } private static IEnumerable<LocalizationFileRecord> ParseKeyValueLines(ModInfoRecord mod, string file, string text, string fileLanguage, string format, char separator) { List<LocalizationFileRecord> list = new List<LocalizationFileRecord>(); string[] array = text.Split(new string[2] { "\r\n", "\n" }, StringSplitOptions.None); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0 || text2.StartsWith("#", StringComparison.Ordinal) || text2.StartsWith("//", StringComparison.Ordinal)) { continue; } int num = text2.IndexOf(separator); if (num > 0) { string key = Unquote(text2.Substring(0, num).Trim()); string text3 = Unquote(text2.Substring(num + 1).Trim()); if (TranslationRegistry.LooksTranslatable(text3)) { list.Add(Make(mod, file, fileLanguage, key, text3, format)); } } } return list; } private static IEnumerable<LocalizationFileRecord> ParseSeparated(ModInfoRecord mod, string file, string text, string fileLanguage, string format, char separator) { List<LocalizationFileRecord> list = new List<LocalizationFileRecord>(); List<string[]> list2 = (from x in text.Split(new string[2] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries) select SplitLine(x, separator) into x where x.Length >= 2 select x).ToList(); if (list2.Count == 0) { return list; } string[] array = list2[0]; int num = Array.FindIndex(array, (string x) => IsKeyHeader(x)); Dictionary<int, string> dictionary = new Dictionary<int, string>(); for (int num2 = 0; num2 < array.Length; num2++) { string value = LanguageHelper.Normalize(array[num2]); if (LanguageHelper.IsKnown(value)) { dictionary[num2] = value; } } if (num >= 0 && dictionary.Count > 0) { foreach (string[] item in list2.Skip(1)) { if (item.Length > num) { string key = item[num]; foreach (KeyValuePair<int, string> item2 in dictionary) { if (item.Length > item2.Key && TranslationRegistry.LooksTranslatable(item[item2.Key])) { list.Add(Make(mod, file, item2.Value, key, item[item2.Key], format)); } } } } return list; } string language = (LanguageHelper.IsKnown(fileLanguage) ? fileLanguage : "English"); foreach (string[] item3 in list2) { if (TranslationRegistry.LooksTranslatable(item3[1])) { list.Add(Make(mod, file, language, item3[0], item3[1], format)); } } return list; } private static LocalizationFileRecord Make(ModInfoRecord mod, string file, string language, string key, string value, string format) { return new LocalizationFileRecord { ModGuid = mod.Guid, ModName = mod.Name, Language = (LanguageHelper.IsKnown(language) ? LanguageHelper.Normalize(language) : ""), Key = key?.Trim(), Value = value?.Trim(), SourceFile = file, Format = format }; } private static string FirstString(JObject obj, params string[] names) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Invalid comparison between Unknown and I4 foreach (string name in names) { JProperty? obj2 = obj.Properties().FirstOrDefault((Func<JProperty, bool>)((JProperty x) => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))); JToken val = ((obj2 != null) ? obj2.Value : null); if (val != null && (int)val.Type == 8) { return ((string)val)?.Trim(); } } return ""; } private static string[] SplitLine(string line, char separator) { if (separator == '\t') { return line.Split('\t').Select(Unquote).ToArray(); } List<string> list = new List<string>(); bool flag = false; StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '"') { if (flag && i + 1 < line.Length && line[i + 1] == '"') { stringBuilder.Append('"'); i++; } else { flag = !flag; } } else if (c == separator && !flag) { list.Add(stringBuilder.ToString()); stringBuilder.Clear(); } else { stringBuilder.Append(c); } } list.Add(stringBuilder.ToString()); return list.Select(Unquote).ToArray(); } private static bool IsKeyHeader(string value) { value = value?.Trim().ToLowerInvariant() ?? ""; switch (value) { case "key": case "token": case "id": case "name": return true; default: return false; } } private static string Unquote(string value) { return (value ?? "").Trim().Trim('"', '\''); } private static string UnescapeLooseJson(string value) { if (string.IsNullOrEmpty(value) || value.IndexOf('\\') < 0) { return value; } try { return Extensions.Value<string>((IEnumerable<JToken>)JToken.Parse("\"" + value.Replace("\"", "\\\"") + "\"")); } catch { return value.Replace("\\\"", "\"").Replace("\\\\", "\\"); } } } internal sealed class PrefabScanner { private static readonly BindingFlags StringFieldFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo LocalizationTranslationsField = typeof(Localization).GetField("m_translations", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly Dictionary<Type, List<FieldInfo>> StringFieldCache = new Dictionary<Type, List<FieldInfo>>(); private static readonly ModInfoRecord UnresolvedPrefabMod = new ModInfoRecord { Guid = "_unresolved_prefabs", Name = "Unresolved Prefabs", Version = "", AssemblyName = "", AssemblyPath = "", RootDirectory = "" }; private readonly PluginDiscovery discovery; private readonly ModOwnershipResolver ownershipResolver; private readonly Settings settings; private readonly List<object> unresolved = new List<object>(); private bool deepScan; public IReadOnlyList<object> Unresolved => unresolved; public PrefabScanner(PluginDiscovery discovery, ModOwnershipResolver ownershipResolver, Settings settings) { this.discovery = discovery; this.ownershipResolver = ownershipResolver; this.settings = settings; } public List<TranslationCandidate> Scan(bool deepMode = false) { unresolved.Clear(); deepScan = deepMode; List<TranslationCandidate> list = new List<TranslationCandidate>(); try { if ((Object)(object)ObjectDB.instance != (Object)null) { ScanObjectDb(list, ObjectDB.instance); } if ((Object)(object)ZNetScene.instance != (Object)null) { ScanZNetScene(list, ZNetScene.instance); } if (deepMode) { ScanLoadedUiText(list); } return (from x in list.GroupBy<TranslationCandidate, string>((TranslationCandidate x) => x.ModGuid + "\n" + x.Category + "\n" + x.Key + "\n" + x.Source, StringComparer.Ordinal) select x.First()).ToList(); } finally { deepScan = false; } } private void ScanObjectDb(List<TranslationCandidate> candidates, ObjectDB objectDb) { foreach (GameObject item in objectDb.m_items ?? new List<GameObject>()) { if ((Object)(object)item == (Object)null) { continue; } OwnershipResult ownershipResult = ownershipResolver.ResolvePrefab(item); ModInfoRecord modInfoRecord = (ownershipResult.IsKnown ? discovery.FindByGuid(ownershipResult.ModGuid) : ResolveUnresolvedMod()); if (!ownershipResult.IsKnown) { AddUnresolved(((Object)item).name, "item", ownershipResult.Method, modInfoRecord != null); if (modInfoRecord == null) { continue; } } if (modInfoRecord == null) { continue; } SharedData val = item.GetComponent<ItemDrop>()?.m_itemData?.m_shared; if (val != null) { Add(candidates, modInfoRecord, "item", val.m_name, val.m_name, "ObjectDB ItemDrop", ((Object)item).name, ownershipResult.Confidence); Add(candidates, modInfoRecord, "item", TokenKey(val.m_name, "description", val.m_description), val.m_description, "ObjectDB ItemDrop", ((Object)item).name, ownershipResult.Confidence); Add(candidates, modInfoRecord, "item", TokenKey(val.m_name, "subtitle", val.m_subtitle), val.m_subtitle, "ObjectDB ItemDrop", ((Object)item).name, ownershipResult.Confidence); Add(candidates, modInfoRecord, "item", TokenKey(val.m_name, "set", val.m_setName), val.m_setName, "ObjectDB ItemDrop", ((Object)item).name, ownershipResult.Confidence); if (settings.ExportGenericPrefabStrings.Value) { ScanGenericObjectStrings(candidates, val, modInfoRecord, "item", "ObjectDB SharedData string field", ((Object)item).name + "/SharedData", ownershipResult.Confidence); } } if (settings.ExportGenericPrefabStrings.Value) { ScanGenericPrefabStrings(candidates, item, modInfoRecord, ownershipResult.Confidence); } } ScanPieceTables(candidates, objectDb); foreach (StatusEffect item2 in objectDb.m_StatusEffects ?? new List<StatusEffect>()) { if ((Object)(object)item2 == (Object)null) { continue; } string text = ResolveScriptableObjectOwner((ScriptableObject)(object)item2); ModInfoRecord modInfoRecord2 = (string.IsNullOrWhiteSpace(text) ? ResolveUnresolvedMod() : discovery.FindByGuid(text)); if (modInfoRecord2 != null && !modInfoRecord2.IsTechnicalLibrary) { string context = "StatusEffect/" + ((Object)item2).name; Add(candidates, modInfoRecord2, "status_effect", item2.m_name, item2.m_name, "ObjectDB StatusEffect", context, 0.6f); Add(candidates, modInfoRecord2, "status_effect", TokenKey(item2.m_name, "tooltip", item2.m_tooltip), item2.m_tooltip, "ObjectDB StatusEffect", context, 0.6f); Add(candidates, modInfoRecord2, "status_effect", TokenKey(item2.m_name, "start", item2.m_startMessage), item2.m_startMessage, "ObjectDB StatusEffect", context, 0.6f); Add(candidates, modInfoRecord2, "status_effect", TokenKey(item2.m_name, "stop", item2.m_stopMessage), item2.m_stopMessage, "ObjectDB StatusEffect", context, 0.6f); Add(candidates, modInfoRecord2, "status_effect", TokenKey(item2.m_name, "repeat", item2.m_repeatMessage), item2.m_repeatMessage, "ObjectDB StatusEffect", context, 0.6f); if (settings.ExportGenericPrefabStrings.Value) { ScanGenericObjectStrings(candidates, item2, modInfoRecord2, "status_effect", "StatusEffect string field", context, 0.5f); } } } } private void ScanPieceTables(List<TranslationCandidate> candidates, ObjectDB objectDb) { foreach (GameObject item in objectDb.m_items ?? new List<GameObject>()) { if ((Object)(object)item == (Object)null) { continue; } PieceTable val = item.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_buildPieces; if (val?.m_pieces == null || val.m_pieces.Count == 0) { continue; } OwnershipResult tableOwner = ownershipResolver.ResolvePrefab(item); foreach (GameObject piece in val.m_pieces) { if ((Object)(object)piece == (Object)null) { continue; } Piece component = piece.GetComponent<Piece>(); if ((Object)(object)component == (Object)null) { continue; } OwnershipResult owner = ownershipResolver.ResolvePrefab(piece); ModInfoRecord modInfoRecord = ResolvePieceTableMod(owner, tableOwner); if (modInfoRecord == null || modInfoRecord.IsTechnicalLibrary) { if (!owner.IsKnown) { AddUnresolved(((Object)piece).name, "piece_table", owner.Method, exported: false); } } else { float confidence = (owner.IsKnown ? owner.Confidence : Math.Min(tableOwner.Confidence, 0.68f)); string context = ((Object)item).name + "/" + ((Object)piece).name; Add(candidates, modInfoRecord, "piece", component.m_name, component.m_name, "ObjectDB PieceTable", context, confidence); Add(candidates, modInfoRecord, "piece", TokenKey(component.m_name, "description", component.m_description), component.m_description, "ObjectDB PieceTable", context, confidence); } } } } private ModInfoRecord ResolvePieceTableMod(OwnershipResult owner, OwnershipResult tableOwner) { if (owner.IsKnown) { return discovery.FindByGuid(owner.ModGuid); } if (tableOwner.IsKnown) { return discovery.FindByGuid(tableOwner.ModGuid); } return ResolveUnresolvedMod(); } private void ScanZNetScene(List<TranslationCandidate> candidates, ZNetScene scene) { foreach (GameObject item in scene.m_prefabs ?? new List<GameObject>()) { if ((Object)(object)item == (Object)null) { continue; } OwnershipResult ownershipResult = ownershipResolver.ResolvePrefab(item); ModInfoRecord modInfoRecord = (ownershipResult.IsKnown ? discovery.FindByGuid(ownershipResult.ModGuid) : ResolveUnresolvedMod()); if (!ownershipResult.IsKnown) { AddUnresolved(((Object)item).name, "znet_prefab", ownershipResult.Method, modInfoRecord != null); if (modInfoRecord == null) { continue; } } if (modInfoRecord != null && !modInfoRecord.IsTechnicalLibrary) { Piece component = item.GetComponent<Piece>(); if ((Object)(object)component != (Object)null) { Add(candidates, modInfoRecord, "piece", component.m_name, component.m_name, "ZNetScene Piece", ((Object)item).name, ownershipResult.Confidence); Add(candidates, modInfoRecord, "piece", TokenKey(component.m_name, "description", component.m_description), component.m_description, "ZNetScene Piece", ((Object)item).name, ownershipResult.Confidence); } Character component2 = item.GetComponent<Character>(); if ((Object)(object)component2 != (Object)null) { Add(candidates, modInfoRecord, "creature", component2.m_name, component2.m_name, "ZNetScene Character", ((Object)item).name, ownershipResult.Confidence); } if (settings.ExportGenericPrefabStrings.Value) { ScanGenericPrefabStrings(candidates, item, modInfoRecord, ownershipResult.Confidence); } } } } private void ScanLoadedUiText(List<TranslationCandidate> candidates) { TMP_Text[] array = FindLoadedObjects<TMP_Text>(); foreach (TMP_Text val in array) { AddLoadedUiText(candidates, (Component)(object)val, (val != null) ? val.text : null, "Loaded TMP_Text"); } Text[] array2 = FindLoadedObjects<Text>(); foreach (Text val2 in array2) { AddLoadedUiText(candidates, (Component)(object)val2, (val2 != null) ? val2.text : null, "Loaded Unity Text"); } } private void AddLoadedUiText(List<TranslationCandidate> candidates, Component component, string value, string origin) { if ((Object)(object)component == (Object)null || string.IsNullOrWhiteSpace(value) || !TranslationRegistry.LooksTranslatable(value)) { return; } OwnershipResult ownershipResult = ownershipResolver.ResolveComponent(component, allowRelatedComponentSearch: true); if (!ownershipResult.IsKnown) { GameObject gameObject = component.gameObject; AddUnresolved(((gameObject != null) ? ((Object)gameObject).name : null) ?? ((object)component).GetType().Name, "ui", ownershipResult.Method, exported: false); return; } ModInfoRecord modInfoRecord = discovery.FindByGuid(ownershipResult.ModGuid); if (modInfoRecord != null && !modInfoRecord.IsTechnicalLibrary && !modInfoRecord.IsUniversalModLocalizer) { Add(candidates, modInfoRecord, "ui", value, value, origin, BuildObjectContext(component.gameObject, component), Math.Max(0.6f, ownershipResult.Confidence)); } } private static T[] FindLoadedObjects<T>() where T : Object { try { return Resources.FindObjectsOfTypeAll<T>() ?? Array.Empty<T>(); } catch (Exception ex) { Plugin.LogWarning("Deep UI text scan failed for " + typeof(T).Name + ": " + ex.Message); return Array.Empty<T>(); } } private void ScanGenericObjectStrings(List<TranslationCandidate> candidates, object target, ModInfoRecord mod, string category, string origin, string context, float confidence) { if (target == null) { return; } foreach (FieldInfo stringField in GetStringFields(target.GetType())) { foreach (string item in ReadStringValues(target, stringField)) { Add(candidates, mod, category, item, item, origin, context + "." + stringField.Name, confidence); } } } private void ScanGenericPrefabStrings(List<TranslationCandidate> candidates, GameObject prefab, ModInfoRecord mod, float confidence) { Component[] componentsInChildren; try { componentsInChildren = prefab.GetComponentsInChildren<Component>(true); } catch { return; } Component[] array = componentsInChildren; foreach (Component val in array) { if ((Object)(object)val == (Object)null) { continue; } foreach (FieldInfo stringField in GetStringFields(((object)val).GetType())) { foreach (string item in ReadStringValues(val, stringField)) { AddGenericField(candidates, mod, prefab, val, stringField, item, confidence); } } } } private static List<FieldInfo> GetStringFields(Type type) { if (type == null) { return new List<FieldInfo>(); } if (StringFieldCache.TryGetValue(type, out var value)) { return value; } List<FieldInfo> list = new List<FieldInfo>(); Type type2 = type; while (type2 != null && type2 != typeof(object)) { FieldInfo[] fields = type2.GetFields(StringFieldFlags); foreach (FieldInfo fieldInfo in fields) { if (!fieldInfo.IsStatic) { Type fieldType = fieldInfo.FieldType; if (fieldType == typeof(string) || typeof(IEnumerable<string>).IsAssignableFrom(fieldType)) { list.Add(fieldInfo); } } } type2 = type2.BaseType; } StringFieldCache[type] = list; return list; } private static IEnumerable<string> ReadStringValues(object target, FieldInfo field) { object value; try { value = field.GetValue(target); } catch { yield break; } if (value is string text) { yield return text; } else { if (!(value is IEnumerable<string> enumerable)) { yield break; } foreach (string item in enumerable) { yield return item; } } } private void AddGenericField(List<TranslationCandidate> candidates, ModInfoRecord mod, GameObject prefab, Component component, FieldInfo field, string value, float confidence) { if (!string.IsNullOrWhiteSpace(value) && TranslationRegistry.LooksTranslatable(value)) { string text = value.Trim(); string category = GuessGenericCategory(component, field); string context = ((Object)prefab).name + "/" + ((object)component).GetType().FullName + "." + field.Name; Add(candidates, mod, category, text, text, "Prefab string field", context, Math.Max(0.45f, confidence * 0.85f)); } } private static string GuessGenericCategory(Component component, FieldInfo field) { string text = (((object)component).GetType().Name + "." + field.Name).ToLowerInvariant(); if (text.Contains("text") || text.Contains("label") || text.Contains("hud") || text.Contains("ui")) { return "ui"; } return "prefab_field"; } private static Component[] GetComponents(GameObject obj) { try { return obj.GetComponents<Component>(); } catch { return Array.Empty<Component>(); } } private static Component[] GetParentComponents(GameObject obj) { try { return obj.GetComponentsInParent<Component>(true); } catch { return Array.Empty<Component>(); } } private static string BuildObjectContext(GameObject obj, Component component) { string text = (((Object)(object)obj == (Object)null) ? ((object)component).GetType().Name : ((Object)obj).name); try { if ((Object)(object)obj == (Object)null) { return text; } Transform val = obj.transform; int num = 0; while ((Object)(object)val.parent != (Object)null && num < 12) { val = val.parent; text = ((Object)val).name + "/" + text; num++; } } catch { } return text + "/" + ((object)component).GetType().FullName; } private string ResolveScriptableObjectOwner(ScriptableObject obj) { string location = ((object)obj).GetType().Assembly.Location; return discovery.FindByAssemblyPath(location)?.Guid; } private void Add(List<TranslationCandidate> candidates, ModInfoRecord mod, string category, string key, string source, string origin, string context, float confidence) { if (TranslationRegistry.LooksTranslatable(key) || TranslationRegistry.LooksTranslatable(source)) { string text = (string.IsNullOrWhiteSpace(key) ? "" : key.Trim()); string source2 = NormalizeSource(source); if (TranslationRegistry.LooksTranslatable(source2) || text.StartsWith("$", StringComparison.Ordinal)) { candidates.Add(new TranslationCandidate { ModGuid = mod.Guid, ModName = mod.Name, Key = text, Source = source2, Category = category, Origin = origin, SourceLanguage = "English", Context = context, OwnerConfidence = confidence }); } } } private string NormalizeSource(string value) { if (string.IsNullOrWhiteSpace(value)) { return ""; } string text = value.Trim(); if (settings.ResolveTokenSourcesFromCurrentLocalization.Value && LooksLikeLocalizationToken(text) && TryLookupCurrentLocalization(text, out var localized)) { return localized.Trim(); } return text; } private static bool LooksLikeLocalizationToken(string value) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim().StartsWith("$", StringComparison.Ordinal); } return false; } private static bool TryLookupCurrentLocalization(string token, out string localized) { localized = null; try { object instance = Localization.instance; if (instance == null || LocalizationTranslationsField == null) { return false; } if (!(LocalizationTranslationsField.GetValue(instance) is IDictionary<string, string> dictionary)) { return false; } string text = token.Trim(); string text2 = (text.StartsWith("$", StringComparison.Ordinal) ? text.Substring(1) : text); string[] array = new string[3] { text, text2, "$" + text2 }; foreach (string key in array) { if (dictionary.TryGetValue(key, out var value) && IsUsefulLocalizedSource(value, text, text2)) { localized = value; return true; } } } catch { } return false; } private static bool IsUsefulLocalizedSource(string value, string token, string tokenWithoutDollar) { if (string.IsNullOrWhiteSpace(value)) { return false; } string text = value.Trim(); if (!text.StartsWith("$", StringComparison.Ordinal) && !text.StartsWith("[", StringComparison.Ordinal) && !string.Equals(text, token, StringComparison.Ordinal) && !string.Equals(text, tokenWithoutDollar, StringComparison.Ordinal)) { return TranslationRegistry.LooksTranslatable(text); } return false; } private static string TokenKey(string baseKey, string suffix, string source = null) { if (source != null && string.IsNullOrWhiteSpace(source)) { return ""; } if (!string.IsNullOrWhiteSpace(source) && source.Trim().StartsWith("$", StringComparison.Ordinal)) { return source.Trim(); } if (string.IsNullOrWhiteSpace(baseKey)) { return ""; } return (baseKey.StartsWith("$", StringComparison.Ordinal) ? baseKey : ("$" + baseKey)) + "." + suffix; } private void AddUnresolved(string prefab, string category, string reason, bool exported) { unresolved.Add(new { prefab = prefab, category = category, reason = reason, exportedAs = (exported ? UnresolvedPrefabMod.Guid : "") }); } private ModInfoRecord ResolveUnresolvedMod() { if (settings == null || (!deepScan && !settings.AggressiveExportMode.Value && !settings.ExportUnresolvedPrefabs.Value)) { return null; } return UnresolvedPrefabMod; } } internal sealed class RuntimeStringCollector { private readonly UmlPaths paths; private readonly Settings settings; private readonly ModOwnershipResolver ownershipResolver; private readonly PluginDiscovery discovery; private readonly Dictionary<string, Dictionary<string, TranslationCandidate>> pending = new Dictionary<string, Dictionary<string, TranslationCandidate>>(StringComparer.OrdinalIgnoreCase); public RuntimeStringCollector(UmlPaths paths, Settings settings, ModOwnershipResolver ownershipResolver, PluginDiscovery discovery) { this.paths = paths; this.settings = settings; this.ownershipResolver = ownershipResolver; this.discovery = discovery; } public void Record(string source, Component owner) { if (settings.ExportRuntimeStrings.Value && TranslationRegistry.LooksTranslatable(source)) { OwnershipResult ownershipResult = ownershipResolver.ResolveComponent(owner); string text = (ownershipResult.IsKnown ? ownershipResult.ModGuid : "_runtime_unknown"); string modName = (ownershipResult.IsKnown ? discovery.FindByGuid(text) : null)?.Name ?? ((text == "_runtime_unknown") ? "Runtime Unknown UI" : text); string key = "runtime." + HashHelper.Short(source); if (!pending.TryGetValue(text, out var value)) { value = new Dictionary<string, TranslationCandidate>(StringComparer.Ordinal); pending[text] = value; } value[key] = new TranslationCandidate { ModGuid = text, ModName = modName, Key = key, Source = source.Trim(), Category = "ui", Origin = "Runtime UI", SourceLanguage = "Unknown", Context = (((Object)(object)owner == (Object)null) ? "unknown component" : ((object)owner).GetType().FullName), OwnerConfidence = ownershipResult.Confidence }; } } public void Flush(string language) { if (pending.Count == 0) { return; } foreach (KeyValuePair<string, Dictionary<string, TranslationCandidate>> item in pending.ToList()) { string text = Path.Combine(paths.LanguageExportRoot(language), UmlPaths.SanitizeSegment(item.Key)); Directory.CreateDirectory(text); string value = item.Value.Values.FirstOrDefault()?.ModName ?? item.Key; string text2 = Path.Combine(text, UmlPaths.SanitizeSegment(value) + "_ui.json"); Dictionary<string, string> dictionary = ReadExisting(text2); foreach (TranslationCandidate value2 in item.Value.Values) { string text3 = ExportKey(value2); if (!string.IsNullOrWhiteSpace(text3) && !dictionary.ContainsKey(text3)) { dictionary[text3] = (string.IsNullOrWhiteSpace(value2.Source) ? text3 : value2.Source.Trim()); } } File.WriteAllText(text2, JsonConvert.SerializeObject((object)new SortedDictionary<string, string>(dictionary, StringComparer.OrdinalIgnoreCase), (Formatting)1), Encoding.UTF8); } pending.Clear(); } private static Dictionary<string, string> ReadExisting(string file) { //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Invalid comparison between Unknown and I4 Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); if (!File.Exists(file)) { return dictionary; } try { JObject val = JObject.Parse(File.ReadAllText(file)); JToken obj = val["entries"]; JArray val2 = (JArray)(object)((obj is JArray) ? obj : null); if (val2 != null) { foreach (JObject item in ((IEnumerable)val2).OfType<JObject>()) { string text = ((string)item["source"])?.Trim(); string value = ((string)item["source"])?.Trim(); if (!string.IsNullOrWhiteSpace(text) && !dictionary.ContainsKey(text)) { dictionary[text] = value; } } return dictionary; } foreach (JProperty item2 in val.Properties()) { if ((int)item2.Value.Type == 8) { string value2 = ((string)item2.Value)?.Trim(); if (!string.IsNullOrWhiteSpace(item2.Name) && !string.IsNullOrWhiteSpace(value2)) { dictionary[item2.Name] = value2; } } } } catch { } return dictionary; } private static string ExportKey(TranslationCandidate candidate) { string text = candidate.Source?.Trim() ?? ""; object obj; if (!TranslationRegistry.LooksTranslatable(text)) { obj = candidate.Key?.Trim(); if (obj == null) { return ""; } } else { obj = text; } return (string)obj; } } internal sealed class TranslationCandidate { public string ModGuid { get; set; } public string ModName { get; set; } public string Key { get; set; } public string Source { get; set; } public string Category { get; set; } public string Origin { get; set; } public string SourceLanguage { get; set; } public string Context { get; set; } public float OwnerConfidence { get; set; } } } namespace UniversalModLocalizer.Persistence { internal sealed class ExportManager { private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private readonly UmlPaths paths; private readonly Settings settings; public ExportManager(UmlPaths paths, Settings settings) { this.paths = paths; this.settings = settings; } public ExportSummary Export(string language, IEnumerable<ModInfoRecord> mods, IEnumerable<LocalizationFileRecord> localizationRecords, IEnumerable<TranslationCandidate> prefabCandidates, IReadOnlyList<object> unresolvedPrefabs, bool deepMode = false) { List<ModInfoRecord> modList = mods.ToList(); List<LocalizationFileRecord> list = localizationRecords.ToList(); List<TranslationCandidate> list2 = new List<TranslationCandidate>(); if (settings.ExportLocalizationFiles.Value) { list2.AddRange(BuildLocalizationFileCandidates(list, language)); } if (settings.ExportPrefabs.Value) { list2.AddRange(prefabCandidates); } int reassignedSyntheticOwners = ReassignSyntheticCandidates(list2, modList, list); ApplyKnownEnglishSources(list2, list); Dictionary<string, HashSet<string>> targetKeys = list.Where((LocalizationFileRecord x) => string.Equals(x.Language, language, StringComparison.OrdinalIgnoreCase)).GroupBy<LocalizationFileRecord, string>((LocalizationFileRecord x) => x.ModGuid, StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, LocalizationFileRecord>, string, HashSet<string>>((IGrouping<string, LocalizationFileRecord> g) => g.Key, (IGrouping<string, LocalizationFileRecord> g) => new HashSet<string>(g.SelectMany((LocalizationFileRecord x) => KeyAliases(x.Key)), StringComparer.Ordinal), StringComparer.OrdinalIgnoreCase); List<IGrouping<string, TranslationCandidate>> list3 = (from x in (from x in list2 where ShouldExportMod(modList, x.ModGuid, deepMode) where !HasTargetRecord(targetKeys, x) where !settings.CheckNativeTargetLanguageDuringExport.Value || !HasNativeTargetTranslation(x, language) where TranslationRegistry.LooksTranslatable(x.Source) || TranslationRegistry.LooksTranslatable(x.Key) where !TranslationRegistry.IsTokenPassthrough(x.Key, x.Source, x.Source) where !TranslationRegistry.IsVanillaToken(x.Key) && !TranslationRegistry.IsVanillaToken(x.Source) select x).GroupBy<TranslationCandidate, string>((TranslationCandidate x) => x.ModGuid + "\n" + x.Category + "\n" + x.Key + "\n" + x.Source, StringComparer.Ordinal) select x.First()).GroupBy<TranslationCandidate, string>((TranslationCandidate x) => x.ModGuid, StringComparer.OrdinalIgnoreCase).ToList(); string text = paths.LanguageExportRoot(language); Directory.CreateDirectory(text); TranslationReport translationReport = new TranslationReport { language = language, generatedAt = DateTime.UtcNow.ToString("O"), totalPluginsDetected = modList.Count, totalLocalizationFileRecords = list.Count, unresolvedPrefabOwners = (unresolvedPrefabs?.Count ?? 0), reassignedSyntheticOwners = reassignedSyntheticOwners }; int num = 0; int num2 = 0; foreach (IGrouping<string, TranslationCandidate> item in list3) { ModInfoRecord mod = ResolveExportMod(modList, item.Key); if (mod == null) { continue; } List<TranslationCandidate> list4 = item.ToList(); if (list4.Count == 0) { continue; } num2++; num += list4.Count; string text2 = Path.Combine(text, UmlPaths.SanitizeSegment(mod.Guid)); Directory.CreateDirectory(text2); WriteManifest(text2, language, mod, list, list4); foreach (IGrouping<string, TranslationCandidate> item2 in from x in list4 group x by ExportGroupCategory(x.Category)) { WriteSimpleCategoryFile(Path.Combine(text2, FileNameForCategory(mod, item2.Key)), item2.ToList()); } translationReport.mods.Add(new TranslationReport.ModReport { guid = mod.Guid, name = mod.Name, version = mod.Version, exportedMissingEntries = list4.Count, detectedSourceEntries = list2.Count((TranslationCandidate x) => string.Equals(x.ModGuid, mod.Guid, StringComparison.OrdinalIgnoreCase)), hasEnglish = list.Any((LocalizationFileRecord x) => string.Equals(x.ModGuid, mod.Guid, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Language, "English", StringComparison.OrdinalIgnoreCase)), hasTargetLanguage = list.Any((LocalizationFileRecord x) => string.Equals(x.ModGuid, mod.Guid, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Language, language, StringComparison.OrdinalIgnoreCase)) }); } translationReport.exportedMods = num2; translationReport.exportedEntries = num; translationReport.skippedTechnicalLibraries = modList.Count((ModInfoRecord x) => x.IsTechnicalLibrary); WriteJson(Path.Combine(paths.DiagnosticsRoot, "translation_report.json"), translationReport); WriteJson(Path.Combine(paths.DiagnosticsRoot, "unresolved_prefabs.json"), unresolvedPrefabs ?? Array.Empty<object>()); return new ExportSummary { Language = language, ExportRoot = text, ExportedMods = num2, ExportedEntries = num, TotalPluginsDetected = modList.Count, LocalizationFileRecords = list.Count, UnresolvedPrefabs = (unresolvedPrefabs?.Count ?? 0) }; } private IEnumerable<TranslationCandidate> BuildLocalizationFileCandidates(List<LocalizationFileRecord> records, string targetLanguage) { Dictionary<string, HashSet<string>> targetKeys = records.Where((LocalizationFileRecord x) => string.Equals(x.Language, targetLanguage, StringComparison.OrdinalIgnoreCase)).GroupBy<LocalizationFileRecord, string>((LocalizationFileRecord x) => x.ModGuid, StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, LocalizationFileRecord>, string, HashSet<string>>((IGrouping<string, LocalizationFileRecord> x) => x.Key, (IGrouping<string, LocalizationFileRecord> x) => new HashSet<string>(x.SelectMany((LocalizationFileRecord y) => KeyAliases(y.Key)), StringComparer.Ordinal), StringComparer.OrdinalIgnoreCase); foreach (LocalizationFileRecord record in records) { if ((string.Equals(record.Language, "English", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(record.Language)) && (!targetKeys.TryGetValue(record.ModGuid, out var value) || !KeyAliases(record.Key).Any(value.Contains))) { yield return new TranslationCandidate { ModGuid = record.ModGuid, ModName = record.ModName, Key = record.Key, Source = record.Value, Category = "localization_files", Origin = "Mod localization file", SourceLanguage = (string.IsNullOrWhiteSpace(record.Language) ? "Unknown" : record.Language), Context = record.SourceFile, OwnerConfidence = 1f }; } } } private static void ApplyKnownEnglishSources(List<TranslationCandidate> candidates, List<LocalizationFileRecord> records) { Dictionary<string, string> sources = (from x in records where string.Equals(x.Language, "English", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(x.Language) where !string.IsNullOrWhiteSpace(x.ModGuid) && !string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Value) select x).SelectMany((LocalizationFileRecord x) => from key in KeyAliases(x.Key) select new { Record = x, Key = x.ModGuid + "\n" + key }).GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase).ToDictionary(x => x.Key, x => x.First().Record.Value, StringComparer.OrdinalIgnoreCase); foreach (TranslationCandidate candidate in candidates) { if (candidate != null && !string.IsNullOrWhiteSpace(candidate.ModGuid) && !string.IsNullOrWhiteSpace(candidate.Key)) { string text = candidate.Source?.Trim() ?? ""; if ((text.StartsWith("$", StringComparison.Ordinal) || string.Equals(text, candidate.Key, StringComparison.Ordinal)) && TryGetSourceByKeyAlias(sources, candidate.ModGuid, candidate.Key, out var source) && TranslationRegistry.LooksTranslatable(source)) { candidate.Source = source.Trim(); } } } } private static bool TryGetSourceByKeyAlias(Dictionary<string, string> sources, string modGuid, string key, out string source) { source = null; foreach (string item in KeyAliases(key)) { if (sources.TryGetValue(modGuid + "\n" + item, out source)) { return true; } } return false; } private int ReassignSyntheticCandidates(List<TranslationCandidate> candidates, List<ModInfoRecord> mods, List<LocalizationFileRecord> records) { Dictionary<string, ModInfoRecord> dictionary = mods.Where((ModInfoRecord x) => x != null && !x.IsUniversalModLocalizer && !x.IsTechnicalLibrary && !string.IsNullOrWhiteSpace(x.Guid)).GroupBy<ModInfoRecord, string>((ModInfoRecord x) => x.Guid, StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, ModInfoRecord>, string, ModInfoRecord>((IGrouping<string, ModInfoRecord> x) => x.Key, (IGrouping<string, ModInfoRecord> x) => x.First(), StringComparer.OrdinalIgnoreCase); Dictionary<string, OwnerHint> dictionary2 = BuildUniqueOwnerLookup(records.SelectMany((LocalizationFileRecord record) => from alias in KeyAliases(record.Key) select new OwnerHint(alias, record.ModGuid, record.ModName))); Dictionary<string, OwnerHint> dictionary3 = BuildUniqueOwnerLookup(from x in records where TranslationRegistry.LooksTranslatable(x.Value) select new OwnerHint(x.Value, x.ModGuid, x.ModName)); int num = 0; foreach (TranslationCandidate candidate in candidates) { if (candidate == null || !IsSyntheticExportGuid(candidate.ModGuid)) { continue; } OwnerHint value = default(OwnerHint); bool flag = false; foreach (string item in KeyAliases(candidate.Key)) { if (dictionary2.TryGetValue(item, out value)) { flag = true; break; } } if (!flag && dictionary3.TryGetValue(candidate.Source?.Trim() ?? "", out value)) { flag = true; } if (flag && !string.IsNullOrWhiteSpace(value.ModGuid) && dictionary.TryGetValue(value.ModGuid, out var value2)) { candidate.ModGuid = value2.Guid; candidate.ModName = value2.Name; candidate.OwnerConfidence = Math.Max(candidate.OwnerConfidence, 0.82f); candidate.Origin = (string.IsNullOrWhiteSpace(candidate.Origin) ? "owner recovered from localization records" : (candidate.Origin + "; owner recovered from localization records")); num++; } } return num; } private static Dictionary<string, OwnerHint> BuildUniqueOwnerLookup(IEnumerable<OwnerHint> hints) { Dictionary<string, OwnerHint> dictionary = new Dictionary<string, OwnerHint>(StringComparer.Ordinal); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (OwnerHint hint in hints) { if (string.IsNullOrWhiteSpace(hint.Key) || string.IsNullOrWhiteSpace(hint.ModGuid)) { continue; } string text = hint.Key.Trim(); if (!hashSet.Contains(text)) { if (!dictionary.TryGetValue(text, out var value)) { dictionary[text] = new OwnerHint(text, hint.ModGuid, hint.ModName); } else if (!string.Equals(value.ModGuid, hint.ModGuid, StringComparison.OrdinalIgnoreCase)) { dictionary.Remove(text); hashSet.Add(text); } } } return dictionary; } private bool ShouldExportMod(List<ModInfoRecord> mods, string modGuid, bool deepMode) { if (IsSyntheticExportGuid(modGuid)) { if (string.Equals(modGuid, "_unresolved_prefabs", StringComparison.OrdinalIgnoreCase)) { return settings.ExportUnresolvedPrefabs.Value; } return true; } ModInfoRecord modInfoRecord = mods.FirstOrDefault((ModInfoRecord x) => string.Equals(x.Guid, modGuid, StringComparison.OrdinalIgnoreCase)); if (modInfoRecord == null || modInfoRecord.IsUniversalModLocalizer) { return false; } if (settings.SkipTechnicalLibraries.Value) { return !modInfoRecord.IsTechnicalLibrary; } return true; } private static ModInfoRecord ResolveExportMod(List<ModInfoRecord> mods, string modGuid) { ModInfoRecord modInfoRecord = mods.FirstOrDefault((ModInfoRecord x) => string.Equals(x.Guid, modGuid, StringComparison.OrdinalIgnoreCase)); if (modInfoRecord != null) { return modInfoRecord; } if (!IsSyntheticExportGuid(modGuid)) { return null; } string text = modGuid; string text2 = ((text == "_runtime_unknown") ? "Runtime Unknown UI" : ((!(text == "_unresolved_prefabs")) ? modGuid : "Unresolved Prefabs")); string name = text2; return new ModInfoRecord { Guid = modGuid, Name = name, Version = "", AssemblyName = "", AssemblyPath = "", RootDirectory = "" }; } private static bool IsSyntheticExportGuid(string modGuid) { if (!string.IsNullOrWhiteSpace(modGuid)) { return modGuid.StartsWith("_", StringComparison.Ordinal); } return false; } private static bool HasTargetRecord(Dictionary<string, HashSet<string>> targetKeys, TranslationCandidate candidate) { if (string.IsNullOrWhiteSpace(candidate.Key)) { return false; } if (targetKeys.TryGetValue(candidate.ModGuid, out var value)) { return KeyAliases(candidate.Key).Any(value.Contains); } return false; } private static IEnumerable<string> KeyAliases(string key) { if (!string.IsNullOrWhiteSpace(key)) { string trimmed = key.Trim(); yield return trimmed; if (trimmed.StartsWith("$", StringComparison.Ordinal)) { yield return trimmed.Substring(1); } else { yield return "$" + trimmed; } } } private static bool HasNativeTargetTranslation(TranslationCandidate candidate, string language) { string text = candidate?.Key?.Trim(); if (string.IsNullOrWhiteSpace(text) || !text.StartsWith("$", StringComparison.Ordinal)) { return false; } try { Localization instance = Localization.instance; string text2 = ((instance != null) ? instance.TranslateSingleId(text, language) : null); if (string.IsNullOrWhiteSpace(text2) || text2.StartsWith("[", StringComparison.Ordinal) || string.Equals(text2, text, StringComparison.Ordinal)) { return false; } Localization instance2 = Localization.instance; string b = ((instance2 != null) ? instance2.TranslateSingleId(text, "English") : null); return !string.Equals(text2, b, StringComparison.Ordinal); } catch { return false; } } private static void WriteManifest(string modDir, string language, ModInfoRecord mod, List<LocalizationFileRecord> records, List<TranslationCandidate> candidates) { var value = new { guid = mod.Guid, name = mod.Name, version = mod.Version, assembly = mod.AssemblyName, targetLanguage = language, detectedLocalizationSystem = (records.Any((LocalizationFileRecord x) => string.Equals(x.ModGuid, mod.Guid, StringComparison.OrdinalIgnoreCase)) ? "Mod localization files" : "Prefab/config/runtime scan"), hasEnglish = records.Any((LocalizationFileRecord x) => string.Equals(x.ModGuid, mod.Guid, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Language, "English", StringComparison.OrdinalIgnoreCase)), hasTargetLanguage = records.Any((LocalizationFileRecord x) => string.Equals(x.ModGuid, mod.Guid, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Language, language, StringComparison.OrdinalIgnoreCase)), scanDate = DateTime.UtcNow.ToString("O"), exportedEntries = candidates.Count }; WriteJson(Path.Combine(modDir, "manifest.json"), value); } private static void WriteSimpleCategoryFile(string file, List<TranslationCandidate> candidates) { SortedDictionary<string, string> sortedDictionary = ReadExistingSimpleCategoryFile(file); foreach (TranslationCandidate item in candidates.OrderBy<TranslationCandidate, string>((TranslationCandidate x) => ExportDictionaryKey(x), StringComparer.OrdinalIgnoreCase)) { string text = ExportDictionaryKey(item); if (string.IsNullOrWhiteSpace(text)) { continue; } string value = (string.IsNullOrWhiteSpace(item.Source) ? text : item.Source.Trim()); if (sortedDictionary.TryGetValue(text, out var value2)) { if (string.IsNullOrWhiteSpace(value2) && !string.IsNullOrWhiteSpace(value)) { sortedDictionary[text] = value; } } else { sortedDictionary[text] = value; } } WriteJson(file, sortedDictionary); } private static SortedDictionary<string, string> ReadExistingSimpleCategoryFile(string file) { //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Invalid comparison between Unknown and I4 SortedDictionary<string, string> sortedDictionary = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (!File.Exists(file)) { return sortedDictionary; } try { JToken obj = JToken.Parse(File.ReadAllText(file, Utf8NoBom)); JObject val = (JObject)(object)((obj is JObject) ? obj : null); if (val == null) { return sortedDictionary; } JToken obj2 = val["entries"]; JArray val2 = (JArray)(object)((obj2 is JArray) ? obj2 : null); if (val2 != null) { foreach (JObject item in ((IEnumerable)val2).OfType<JObject>()) { string text = FirstString(item, "key", "source", "token", "id", "name"); string text2 = FirstString(item, "translation", "value", "text", "source"); if (!string.IsNullOrWhiteSpace(text) && !sortedDictionary.ContainsKey(text)) { sortedDictionary[text.Trim()] = text2?.Trim() ?? ""; } } return sortedDictionary; } foreach (JProperty item2 in val.Properties()) { if ((int)item2.Value.Type == 8) { sortedDictionary[item2.Name] = ((string)item2.Value)?.Trim() ?? ""; } } } catch (Exception ex) { string text3 = file + ".bak-" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); try { File.Copy(file, text3, overwrite: true); Plugin.LogWarning("Could not merge existing export file '" + file + "' (" + ex.Message + "). A backup was created at '" + text3 + "', then the file was regenerated."); } catch { Plugin.LogWarning("Could not merge existing export file '" + file + "' (" + ex.Message + "). The file will be regenerated."); } } return sortedDictionary; } private static string FirstString(JObject obj, params string[] names) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Invalid comparison between Unknown and I4 foreach (string name in names) { JProperty? obj2 = obj.Properties().FirstOrDefault((Func<JProperty, bool>)((JProperty x) => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))); JToken val = ((obj2 != null) ? obj2.Value : null); if (val != null && (int)val.Type == 8) { return ((string)val)?.Trim(); } } return ""; } private static TranslationEntryDto ToDto(TranslationCandidate candidate) { return new TranslationEntryDto { modGuid = candidate.ModGuid, modName = candidate.ModName, key = candidate.Key, source = candidate.Source, translation = "", category = candidate.Category, origin = candidate.Origin, sourceLanguage = candidate.SourceLanguage, context = candidate.Context, sourceHash = HashHelper.Sha256(candidate.Source ?? ""), status = "missing", ownerConfidence = candidate.OwnerConfidence }; } private static string ExportDictionaryKey(TranslationCandidate candidate) { if (candidate == null) { return ""; } string text = ExportGroupCategory(candidate.Category); string text2 = candidate.Source?.Trim() ?? ""; string text3 = candidate.Key?.Trim() ?? ""; if (text == "ui" && text3.StartsWith("runtime.", StringComparison.Ordinal) && TranslationRegistry.LooksTranslatable(text2)) { return text2; } if (!string.IsNullOrWhiteSpace(text3)) { return text3; } return text2; } private static string ExportGroupCategory(string category) { return (category ?? "tokens").ToLowerInvariant() switch { "item" => "prefabs", "piece" => "prefabs", "creature" => "prefabs", "status_effect" => "prefabs", "prefab_field" => "prefabs", "localization_files" => "localization", "config" => "config", "ui" => "ui", _ => "tokens", }; } private static string FileNameForCategory(ModInfoRecord mod, string category) { string text = UmlPaths.SanitizeSegment(string.IsNullOrWhiteSpace(mod.Name) ? mod.Guid : mod.Name); string text2 = UmlPaths.SanitizeSegment(string.IsNullOrWhiteSpace(category) ? "tokens" : category.ToLowerInvariant()); return text + "_" + text2 + ".json"; } private static void WriteJson(string file, object value) { Directory.CreateDirectory(Path.GetDirectoryName(file)); File.WriteAllText(file, JsonConvert.SerializeObject(value, (Formatting)1), Utf8NoBom); } } internal sealed class ExportSummary { public string Language { get; set; } public string ExportRoot { get; set; } public int ExportedMods { get; set; } public int ExportedEntries { get; set; } public int TotalPluginsDetected { get; set; } public int LocalizationFileRecords { get; set; } public int UnresolvedPrefabs { get; set; } } internal sealed class TranslationReport { public sealed class ModReport { public string guid { get; set; } public string name { get; set; } public string version { get; set; } public bool hasEnglish { get; set; } public bool hasTargetLanguage { get; set; } public int detectedSourceEntries { get; set; } public int exportedMissingEntries { get; set; } } public string language { get; set; } public string generatedAt { get; set; } public int totalPluginsDetected { get; set; } public int skippedTechnicalLibraries { get; set; } public int totalLocalizationFileRecords { get; set; } public int unresolvedPrefabOwners { get; set; } public int reassignedSyntheticOwners { get; set; } public int exportedMods { get; set; } public int exportedEntries { get; set; } public List<ModReport> mods { get; set; } = new List<ModReport>(); } internal readonly struct OwnerHint { public string Key { get; } public string ModGuid { get; } public string ModName { get; } public OwnerHint(string key, string modGuid, string modName) { Key = key; ModGuid = modGuid; ModName = modName; } } internal sealed class ImportManager : IDisposable { private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private readonly UmlPaths paths; private readonly Settings settings; private FileSystemWatcher watcher; public ImportManager(UmlPaths paths, Settings settings) { this.paths = paths; this.settings = settings; } public List<TranslationEntry> Load(string language) { if (!settings.EnableImports.Value) { return new List<TranslationEntry>(); } List<TranslationEntry> list = LoadFromRoot(paths.LanguageImportRoot(language)).ToList(); List<TranslationEntry> list2 = LoadServerCache(language).ToList(); if (settings.ServerTranslationsOverrideLocal.Value) { list.AddRange(list2); return list; } list2.AddRange(list); return list2; } public void StartWatching(string language, Action onChanged) { DisposeWatcher(); if (settings.HotReload.Value) { string path = paths.LanguageImportRoot(language); Directory.CreateDirectory(path); watcher = new FileSystemWatcher(path, "*.json") { IncludeSubdirectories = true, EnableRaisingEvents = true }; FileSystemEventHandler value = delegate { onChanged?.Invoke(); }; RenamedEventHandler value2 = delegate { onChanged?.Invoke(); }; watcher.Changed += value; watcher.Created += value; watcher.Deleted += value; watcher.Renamed += value2; } } public void Dispose() { DisposeWatcher(); } public void SaveServerCache(string serverId, string language, string json) { string text = paths.ServerCacheRoot(serverId, language); Directory.CreateDirectory(text); File.WriteAllText(Path.Combine(text, "server_package.json"), json, Utf8NoBom); } private void DisposeWatcher() { if (watcher != null) { watcher.EnableRaisingEvents = false; watcher.Dispose(); watcher = null; } } private IEnumerable<TranslationEntry> LoadServerCache(string language) { if (!settings.EnableClientCache.Value) { yield break; } string path = Path.Combine(paths.CacheRoot, "server"); if (!Directory.Exists(path)) { yield break; } foreach (string item in Directory.EnumerateFiles(path, "server_package.json", SearchOption.AllDirectories)) { if (!item.Contains(Path.DirectorySeparatorChar + language + Path.DirectorySeparatorChar) && !item.Contains(Path.AltDirectorySeparatorChar + language + Path.AltDirectorySeparatorChar)) { continue; } foreach (TranslationEntry item2 in LoadFile(item)) { yield return item2; } } } private IEnumerable<TranslationEntry> LoadFromRoot(string root) { if (!Directory.Exists(root)) { yield break; } foreach (string item in Directory.EnumerateFiles(root, "*.json", SearchOption.AllDirectories)) { foreach (TranslationEntry item2 in LoadFile(item)) { yield return item2; } } } public static List<TranslationEntry> LoadFile(string file) { List<TranslationEntry> list = new List<TranslationEntry>(); try { if (string.Equals(Path.GetFileName(file), "manifest.json", StringComparison.OrdinalIgnoreCase)) { return list; } JObject val = ParseTranslationFile(file); string text = InferDocumentGuid(file, ((string)val["modGuid"])?.Trim()); string text2 = InferDocumentName(file, ((string)val["modName"])?.Trim()); ((string)val["language"])?.Trim(); JToken obj = val["entries"]; JArray val2 = (JArray)(object)((obj is JArray) ? obj : null); if (val2 != null) { foreach (JObject item in ((IEnumerable)val2).OfType<JObject>()) { string text3 = ((string)item["translation"])?.Trim(); if (!string.IsNullOrWhiteSpace(text3)) { TranslationEntry translationEntry = new TranslationEntry { ModGuid = (((string)item["modGuid"])?.Trim() ?? text), ModName = (((string)item["modName"])?.Trim() ?? text2), Key = ((string)item["key"])?.Trim(), Source = ((string)item["source"])?.Trim(), Translation = text3, Category = (((string)item["category"])?.Trim() ?? ((string)val["category"])?.Trim()), Origin = ((string)item["origin"])?.Trim(), SourceLanguage = ((string)item["sourceLanguage"])?.Trim(), Context = ((string)item["context"])?.Trim(), SourceHash = ((string)item["sourceHash"])?.Trim() }; if (!TranslationRegistry.IsTokenPassthrough(translationEntry.Key, translationEntry.Source, translationEntry.Translation) && !TranslationRegistry.IsVanillaToken(translationEntry.Key) && !TranslationRegistry.IsVanillaToken(translationEntry.Source) && ValidatePlaceholders(translationEntry, file)) { list.Add(translationEntry); } } } } JToken obj2 = val["exact"]; JObject val3 = (JObject)(object)((obj2 is JObject) ? obj2 : null); if (val3 != null) { foreach (JProperty item2 in val3.Properties()) { string text4 = ((string)item2.Value)?.Trim(); if (!string.IsNullOrWhiteSpace(text4) && !TranslationRegistry.IsTokenPassthrough(item2.Name, item2.Name, text4) && !TranslationRegistry.IsVanillaToken(item2.Name)) { list.Add(new TranslationEntry { ModGuid = text, ModName = text2, Source = item2.Name, Translation = text4, Category = "ui", Origin = "exact" }); } } } foreach (TranslationEntry item3 in LoadSimpleDictionary(val, file, text, text2)) { if (ValidatePlaceholders(item3, file)) { list.Add(item3); } } } catch (Exception ex) { Plugin.LogWarning("Failed to load translation file '" + file + "': " + ex.Message); } return list; } private static JObject ParseTranslationFile(string file) { string text = File.ReadAllText(file, Utf8NoBom); try { return JObject.Parse(text); } catch (Exception ex) { string text2 = RepairCommonJsonAuthoringIssues(text); if (string.Equals(text2, text, StringComparison.Ordinal)) { throw; } try { JObject result = JObject.Parse(text2); Plugin.LogWarning("Translation file '" + file + "' had repairable JSON formatting issues and was loaded with tolerant parsing. Save it as valid JSON when convenient."); return result; } catch (Exception ex2) { throw new InvalidDataException(ex.Message + " Tolerant parse also failed: " + ex2.Message, ex2); } } } private static string RepairCommonJsonAuthoringIssues(string text) { return AddMissingCommasBetweenProperties(Regex.Replace(Regex.Replace(text, "\\\\(?![\"\\\\/bfnrtu])", string.Empty), "\\\\{2}\"", "\\\"")); } private static string AddMissingCommasBetweenProperties(string text) { string[] array = Regex.Split(text, "\\r?\\n"); for (int i = 0; i < array.Length - 1; i++) { string text2 = array[i]; if (Regex.IsMatch(text2, "^\\s*\".*\"\\s*:\\s*\".*\"\\s*$")) { int j; for (j = i + 1; j < array.Length && string.IsNullOrWhiteSpace(array[j]); j++) { } if (j < array.Length && Regex.IsMatch(array[j], "^\\s*\"")) { array[i] = text2 + ","; } } } return string.Join(Environment.NewLine, array); } private static IEnumerable<TranslationEntry> LoadSimpleDictionary(JObject obj, string file, string documentGuid, string documentName) { string category = InferCategory(file); foreach (JProperty item in obj.Properties()) { if (!IsMetadataProperty(item.Name) && (int)item.Value.Type == 8) { string text = item.Name?.Trim(); string text2 = ((string)item.Value)?.Trim(); if (!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2) && !TranslationRegistry.IsTokenPassthrough(text, text, text2) && !TranslationRegistry.IsVanillaToken(text)) { yield return new TranslationEntry { ModGuid = documentGuid, ModName = documentName, Key = text, Source = text, Translation = text2, Category = category, Origin = "simple_json" }; } } } } private static string InferDocumentGuid(string file, string documentGuid) { if (!string.IsNullOrWhiteSpace(documentGuid)) { return documentGuid; } return Directory.GetParent(file)?.Name ?? "_unknown"; } private static string InferDocumentName(string file, string documentName) { if (!string.IsNullOrWhiteSpace(documentName)) { return documentName; } string text = Path.GetFileNameWithoutExtension(file) ?? ""; string[] array = new string[6] { "_prefabs", "_ui", "_hud", "_config", "_localization", "_tokens" }; foreach (string text2 in array) { if (text.EndsWith(text2, StringComparison.OrdinalIgnoreCase)) { return text.Substring(0, text.Length - text2.Length); } } return text; } private static string InferCategory(string file) { string text = Path.GetFileNameWithoutExtension(file) ?? ""; string[] array = new string[6] { "prefabs", "ui", "hud", "config", "localization", "tokens" }; foreach (string text2 in array) { if (text.EndsWith("_" + text2, StringComparison.OrdinalIgnoreCase)) { return text2; } } return "tokens"; } private static bool IsMetadataProperty(string name) { if (!string.Equals(name, "schemaVersion", StringComparison.OrdinalIgnoreCase) && !string.Equals(name, "modGuid", StringComparison.OrdinalIgnoreCase) && !string.Equals(name, "modName", StringComparison.OrdinalIgnoreCase) && !string.Equals(name, "language", StringComparison.OrdinalIgnoreCase) && !string.Equals(name, "generatedAt", StringComparison.OrdinalIgnoreCase) && !string.Equals(name, "category", StringComparison.OrdinalIgnoreCase) && !string.Equals(name, "entries", StringComparison.OrdinalIgnoreCase)) { return string.Equals(name, "exact", StringComparison.OrdinalIgnoreCase); } return true; } private static bool ValidatePlaceholders(TranslationEntry entry, string file) { if (string.IsNullOrWhiteSpace(entry.Source) || string.IsNullOrWhiteSpace(entry.Translation)) { return true; } foreach (string item in ExtractPlaceholders(entry.Source)) { if (!entry.Translation.Contains(item)) { Plugin.LogWarning("Translation '" + file + "' for '" + (entry.Key ?? entry.Source) + "' is missing placeholder '" + item + "'. Entry will still load."); } } return true; } private static IEnumerable<string> ExtractPlaceholders(string value) { Regex regex = new Regex("\\{[0-9]+\\}|%[sdif]|\\{\\{\\$?[A-Za-z0-9_]+\\}\\}"); foreach (Match item in regex.Matches(value)) { yield return item.Value; } } } } namespace UniversalModLocalizer.Patches { [HarmonyPatch(typeof(Localization), "Translate")] internal static class LocalizationTranslatePatch { private static void Postfix(string word, ref string __result) { try { if (!((Object)(object)Plugin.Instance == (Object)null) && Plugin.Instance.Settings.Enabled.Value) { bool flag = string.Equals(__result, "[" + word + "]", StringComparison.Ordinal); if ((flag || !TranslationRegistry.IsVanillaToken(word)) && (flag || Plugin.Instance.Settings.ImportedTranslationsOverrideNative.Value) && Plugin.Instance.TranslateToken(word, "", out var translated)) { __result = translated; } } } catch (Exception ex) { Plugin.LogWarning("Localization token patch failed: " + ex.Message); } } } [HarmonyPatch(typeof(Localization), "Localize", new Type[] { typeof(string) })] internal static class LocalizationLocalizePatch { private static void Postfix(string text, ref string __result) { try { if (!((Object)(object)Plugin.Instance == (Object)null) && Plugin.Instance.Settings.Enabled.Value && Plugin.Instance.Settings.EnableHardcodedStringFallback.Value && Plugin.Instance.TranslateRuntimeText(__result, null, out var translated)) { __result = translated; } } catch (Exception ex) { Plugin.LogWarning("Localization string patch failed: " + ex.Message); } } } [HarmonyPatch(typeof(Localization), "SetupLanguage")] internal static class LocalizationSetupLanguagePatch { private static void Postfix(string language) { try { Plugin.Instance?.OnLanguageChanged(language); } catch { } } } [HarmonyPatch(typeof(TMP_Text), "set_text")] internal static class TmpTextPatch { private static void Prefix(TMP_Text __instance, ref string value) { try { if (!((Object)(object)Plugin.Instance == (Object)null) && Plugin.Instance.Settings.Enabled.Value && Plugin.Instance.Settings.EnableTMPTextPatch.Value && (Plugin.Instance.Settings.EnableHardcodedStringFallback.Value || Plugin.Instance.Settings.ExportRuntimeStrings.Value)) { if (Plugin.Instance.TranslateRuntimeText(value, (Component)(object)__instance, out var translated)) { value = translated; } else { Plugin.Instance.RecordRuntimeString(value, (Component)(object)__instance); } } } catch (Exception ex) { Plugin.LogWarning("TMP_Text patch failed: " + ex.Message); } } } [HarmonyPatch(typeof(Text), "set_text")] internal static class UnityTextPatch { private static void Prefix(Text __instance, ref string value) { try { if (!((Object)(object)Plugin.Instance == (Object)null) && Plugin.Instance.Settings.Enabled.Value && Plugin.Instance.Settings.EnableUnityTextPatch.Value && (Plugin.Instance.Settings.EnableHardcodedStringFallback.Value || Plugin.Instance.Settings.ExportRuntimeStrings.Value)) { if (Plugin.Instance.TranslateRuntimeText(value, (Component)(object)__instance, out var translated)) { value = translated; } else { Plugin.Instance.RecordRuntimeString(value, (Component)(object)__instance); } } } catch (Exception ex) { Plugin.LogWarning("Unity Text patch failed: " + ex.Message); } } } [HarmonyPatch(typeof(ObjectDB), "Awake")] internal static class ObjectDbAwakePatch { private static void Postfix() { Plugin.Instance?.OnWorldDataChanged("ObjectDB.Awake"); } } [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] internal static class ObjectDbCopyPatch { private static void Postfix() { Plugin.Instance?.OnWorldDataChanged("ObjectDB.CopyOtherDB"); } } [HarmonyPatch(typeof(ZNetScene), "Awake")] internal static class ZNetSceneAwakePatch { private static void Postfix() { Plugin.Instance?.OnWorldDataChanged("ZNetScene.Awake"); } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class ZNetAwakePatch { private static void Postfix() { Plugin.Instance?.Network?.TryRegister(); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class ZNetPeerInfoPatch { private static void Postfix() { Plugin.Instance?.Network?.ResetClientRequest(); } } } namespace UniversalModLocalizer.Networking { internal sealed class LocalizationNetworkManager { private sealed class IncomingPackage { public string Language { get; set; } public string Hash { get; set; } public int TotalBytes { get; set; } public byte[][] Chunks { get; set; } } private const int ProtocolVersion = 1; private const int SchemaVersion = 1; private const string RpcName = "UniversalModLocalizer_RPC"; private const int ChunkSize = 24000; private readonly Settings settings; private readonly ImportManager importManager; private readonly Dictionary<string, IncomingPackage> incoming = new Dictionary<string, IncomingPackage>(StringComparer.Ordinal); private bool registered; private bool requestedForConnection; private string lastRequestedServer = ""; public LocalizationNetworkManager(Settings settings, ImportManager importManager) { this.settings = settings; this.importManager = importManager; } public void Tick(string language) { if (!settings.EnableServerSync.Value) { return; } TryRegister(); if (!registered || (Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { return; } if (ZNet.instance.IsServer()) { requestedForConnection = false; lastRequestedServer = ""; return; } string serverId = GetServerId(); if (!requestedForConnection || !string.Equals(lastRequestedServer, serverId, StringComparison.Ordinal)) { requestedForConnection = true; lastRequestedServer = serverId; RequestServerPackage(language, serverId); } } public void ResetClientRequest() { requestedForConnection = false; lastRequestedServer = ""; } public void TryRegister() { if (registered || ZRoutedRpc.instance == null) { return; } try { ZRoutedRpc.instance.Register<ZPackage>("UniversalModLocalizer_RPC", (Action<long, ZPackage>)OnRpc); registered = true; Plugin.LogDebug("Network RPC registered."); } catch (Exception ex) { Plugin.LogWarning("Failed to register network RPC: " + ex.Message); } } private void RequestServerPackage(string language, string serverId) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Expected O, but got Unknown try { string text = ReadCachedHash(serverId, language); ZPackage val = new ZPackage(); val.Write("request"); val.Write(1); val.Write("0.1.13"); val.Write(1); val.Write(language); val.Write(text ?? ""); ZRoutedRpc.instance.InvokeRoutedRPC("UniversalModLocalizer_RPC", new object[1] { val }); Plugin.LogInfo("Requested server translations for " + language + "."); } catch (Exception ex) { Plugin.LogWarning("Failed to request server translations: " + ex.Message); } } private void OnRpc(long sender, ZPackage package) { try { switch (package.ReadString()) { case "request": HandleRequest(sender, package); break; case "manifest": HandleManifest(sender, package); break; case "chunk": HandleChunk(sender, package); break; } } catch (Exception ex) { Plugin.LogWarning("Network translation RPC failed: " + ex.Message); } } private void HandleRequest(long sender, ZPackage package) { //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected O, but got Unknown //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Expected O, but got Unknown if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } int num = package.ReadInt(); string text = package.ReadString(); int num2 = package.ReadInt(); string text2 = package.ReadString(); string a = package.ReadString(); if (num != 1 || num2 != 1) { Plugin.LogWarning($"Client {sender} requested translations with incompatible protocol {num}/{num2} (plugin {text})."); return; } string text3 = Plugin.Instance.BuildServerPackageJson(text2); string text4 = HashHelper.Sha256(text3); byte[] array = Utils.Compress(Encoding.UTF8.GetBytes(text3)); int num3 = (array.Length + 24000 - 1) / 24000; ZPackage val = new ZPackage(); val.Write("manifest"); val.Write(1); val.Write(1); val.Write(text2); val.Write(text4); val.Write(array.Length); val.Write((!string.Equals(a, text4, StringComparison.Ordinal)) ? num3 : 0); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "UniversalModLocalizer_RPC", new object[1] { val }); if (string.Equals(a, text4, StringComparison.Ordinal)) { Plugin.LogDebug($"Client {sender} already has translation package {text4}."); return; } for (int i = 0; i < num3; i++) { int num4 = i * 24000; int num5 = Math.Min(24000, array.Length - num4); byte[] array2 = new byte[num5]; Buffer.BlockCopy(array, num4, array2, 0, num5); ZPackage val2 = new ZPackage(); val2.Write("chunk"); val2.Write(text2); val2.Write(text4); val2.Write(i); val2.Write(num3); val2.Write(array2); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "UniversalModLocalizer_RPC", new object[1] { val2 }); } Plugin.LogInfo($"Sent {text2} translation package to peer {sender}: {num3} chunks, SHA256 {text4}."); } private void HandleManifest(long sender, ZPackage package) { int num = package.ReadInt(); int num2 = package.ReadInt(); string text = package.ReadString(); string text2 = package.ReadString(); int totalBytes = package.ReadInt(); int num3 = package.ReadInt(); if (num != 1 || num2 != 1) { Plugin.LogWarning($"Server translation protocol mismatch: {num}/{num2}."); } else if (num3 == 0) { Plugin.LogInfo("Server translations for " + text + " are already cached (" + text2 + ")."); } else { incoming[text2] = new IncomingPackage { Language = text, Hash = text2, TotalBytes = totalBytes, Chunks = new byte[num3][] }; } } private void HandleChunk(long sender, ZPackage package) { string text = package.ReadString(); string text2 = package.ReadString(); int num = package.ReadInt(); int num2 = package.ReadInt(); byte[] array = package.ReadByteArray(); if (!incoming.TryGetValue(text2, out var value) || num < 0 || num >= value.Chunks.Length) { return; } value.Chunks[num] = array; if (value.Chunks.Any((byte[] x) => x == null)) { return; } byte[] array2 = Combine(value.Chunks); if (array2.Length != value.TotalBytes) { Plugin.LogWarning($"Server translation package size mismatch for {text}: expected {value.TotalBytes}, got {array2.Length}."); incoming.Remove(text2); return; } string text3 = Encoding.UTF8.GetString(Utils.Decompress(array2)); if (!string.Equals(HashHelper.Sha256(text3), text2, StringComparison.Ordinal)) { Plugin.LogWarning("Server translation package hash mismatch. Package ignored."); incoming.Remove(text2); return; } string serverId = GetServerId(); importManager.SaveServerCache(serverId, text, text3); WriteCachedHash(serverId, text, text2); incoming.Remove(text2); Plugin.Instance.ReloadTranslations(); Plugin.LogInfo($"Loaded server translation package for {text}: {num2} chunks, SHA256 {text2}."); } private static byte[] Combine(IEnumerable<byte[]> chunks) { byte[][] array = chunks.ToArray(); byte[] array2 = new byte[array.Sum((byte[] x) => x.Length)]; int num = 0; byte[][] array3 = array; foreach (byte[] array4 in array3) { Buffer.BlockCopy(array4, 0, array2, num, array4.Length); num += array4.Length; } return array2; } private static string GetServerId() { try { string serverString = ZNet.GetServerString(true); if (!string.IsNullOrWhiteSpace(serverString)) { return serverString; } } catch { } return "local"; } private string HashFile(string serverId, string language) { return Path.Combine(Paths.ConfigPath, "UniversalModLocalizer", "cache", "server", UmlPaths.SanitizeSegment(serverId), language, "hash.txt"); } private string ReadCachedHash(string serverId, string language) { string path = HashFile(serverId, language); if (!File.Exists(path)) { return ""; } return File.ReadAllText(path).Trim(); } private void WriteCachedHash(string serverId, string language, string hash) { string path = HashFile(serverId, language); Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllText(path, hash ?? ""); } } } namespace UniversalModLocalizer.Localization { internal static class HashHelper { public static string Sha256(string value) { using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(value ?? "")); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } public static string Short(string value) { string text = Sha256(value); if (text.Length > 12) { return text.Substring(0, 12); } return text; } } internal static class LanguageHelper { private static readonly Dictionary<string, string> Aliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["en"] = "English", ["eng"] = "English", ["english"] = "English", ["pl"] = "Polish", ["pl-pl"] = "Polish", ["polish"] = "Polish", ["polski"] = "Polish", ["de"] = "German", ["german"] = "German", ["deutsch"] = "German", ["fr"] = "French", ["french"] = "French", ["es"] = "Spanish", ["spanish"] = "Spanish", ["sv"] = "Swedish", ["swedish"] = "Swedish", ["ru"] = "Russian", ["russian"] = "Russian", ["uk"] = "Ukrainian", ["ukrainian"] = "Ukrainian", ["nl"] = "Dutch", ["dutch"] = "Dutch", ["it"] = "Italian", ["italian"] = "Italian", ["pt"] = "Portuguese_European", ["pt-br"] = "Portuguese_Brazilian", ["portuguese"] = "Portuguese_European", ["turkish"] = "Turkish", ["tr"] = "Turkish", ["czech"] = "Czech", ["cs"] = "Czech", ["japanese"] = "Japanese", ["ja"] = "Japanese", ["korean"] = "Korean", ["ko"] = "Korean", ["chinese"] = "Chinese", ["zh"] = "Chinese", ["chinese_trad"] = "Chinese_Trad", ["zh-tw"] = "Chinese_Trad" }; public static string Normalize(string value) { if (string.IsNullOrWhiteSpace(value)) { return ""; } string text = value.Trim().Trim('"', '\'', '[', ']', '(', ')').Replace('_', '-'); if (Aliases.TryGetValue(text, out var value2)) { return value2; } text = text.Replace('-', '_'); if (Aliases.TryGetValue(text, out value2)) { return value2; } return value.Trim(); } public static string DetectFromPath(string path) { if (string.IsNullOrWhiteSpace(path)) { return ""; } foreach (string item in Tokenize(Path.GetFileNameWithoutExtension(path)).Concat(Tokenize(Path.GetDirectoryName(path) ?? ""))) { string text = Normalize(item); if (IsKnown(text)) { return text; } } return ""; } public static bool IsKnown(string value) { if (!string.IsNullOrWhiteSpace(value)) { return Aliases.Values.Any((string x) => string.Equals(x, value, StringComparison.OrdinalIgnoreCase)); } return false; } private static IEnumerable<string> Tokenize(string value) { return value.Split(new char[10] { '/', '\\', '.', '_', '-', ' ', '[', ']', '(', ')' }, StringSplitOptions.RemoveEmptyEntries); } } internal sealed class TranslationDocument { public int schemaVersion { get; set; } = 1; public string modGuid { get; set; } public string modName { get; set; } public string language { get; set; } public string generatedAt { get; set; } public string category { get; set; } public List<TranslationEntryDto> entries { get; set; } = new List<TranslationEntryDto>(); } internal sealed class TranslationEntryDto { public string modGuid { get; set; } public string modName { get; set; } public string key { get; set; } public string source { get; set; } public string translation { get; set; } public string category { get; set; } public string origin { get; set; } public string sourceLanguage { get; set; } public string context { get; set; } public string sourceHash { get; set; } public string status { get; set; } public float ownerConfidence { get; set; } } internal sealed class TranslationEntry { public string ModGuid { get; set; } public string ModName { get; set; } public string Key { get; set; } public string Source { get; set; } public string Translation { get; set; } public string Category { get; set; } public string Origin { get; set; } public string SourceLanguage { get; set; } public string Context { get; set; } public string SourceHash { get; set; } public string Status { get; set; } public float OwnerConfidence { get; set; } } internal sealed class TranslationRegistry { private struct UniqueTranslation { private string modGuid; public string Translation { get; private set; } public bool CanUseGlobally { get; private set; } public UniqueTranslation(string modGuid, string translation) { this.modGuid = modGuid; Translation = translation; CanUseGlobally = true; } public void Add(string guid, string translation) { if (!string.Equals(modGuid, guid, StringComparison.OrdinalIgnoreCase)) { CanUseGlobally = false; } if (!string.Equals(Translation, translation, StringComparison.Ordinal)) { CanUseGlobally = false; } } } priva