Decompiled source of UniversalModLocalizer v0.1.13

UniversalModLocalizer.dll

Decompiled 3 days ago
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