using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using HowtoFishLocalizationAPI.Api; using HowtoFishLocalizationAPI.Integrations.HTFModManager; using HowtoFishLocalizationAPI.Integrations.ModMenu; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("HowtoFishLocalizationAPI")] [assembly: AssemblyDescription("How to Fish Localization API by Ice Box Studio")] [assembly: AssemblyCompany("Ice Box Studio")] [assembly: AssemblyProduct("HowtoFishLocalizationAPI")] [assembly: AssemblyCopyright("Copyright © 2026 Ice Box Studio All rights reserved.")] [assembly: ComVisible(false)] [assembly: Guid("79870812-2aca-43d5-b058-5844d23fb3b4")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace HowtoFishLocalizationAPI { [BepInPlugin("IceBoxStudio.HowToFish.LocalizationAPI", "HowtoFishLocalizationAPI", "1.0.0")] public sealed class HowtoFishLocalizationAPI : BaseUnityPlugin { private Harmony _harmony; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; LocalizationApi.NotifyLanguageChanged(); _harmony = new Harmony("IceBoxStudio.HowToFish.LocalizationAPI"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); ModMenuSupport.Init(_harmony); Log.LogInfo((object)"HowtoFishLocalizationAPI 1.0.0 initialized."); } } public static class PluginInfo { public const string PLUGIN_GUID = "IceBoxStudio.HowToFish.LocalizationAPI"; public const string PLUGIN_NAME = "HowtoFishLocalizationAPI"; public const string PLUGIN_VERSION = "1.0.0"; } } namespace HowtoFishLocalizationAPI.Patches { [HarmonyPatch(typeof(LocalizationManager), "SetLanguage")] internal static class GameInitializationPatch { [HarmonyPostfix] private static void Postfix(int __0) { LocalizationApi.NotifyLanguageChanged(__0); } } } namespace HowtoFishLocalizationAPI.Integrations.ModMenu { internal static class ModMenuPatch { private static void Row(Transform __0, ConfigEntryBase __1) { ModMenuSupport.LocalizeRow(__0, __1); } } internal static class ModMenuSupport { private const string ModMenuAssembly = "HowToFish.ModMenu"; private const string ModMenuType = "HowToFish.ModMenu.NativeModMenu"; private const string ModMenuGuid = "community.howtofish.modmenu"; private const string LabelPrefix = "modmenu.config."; private static readonly Dictionary Owners = new Dictionary(); private static Harmony _harmony; private static bool _patched; private static bool _initialized; internal static void Init(Harmony harmony) { if (!_initialized) { _initialized = true; _harmony = harmony; LocalizationApi.LanguageChanged += LangChanged; AppDomain.CurrentDomain.AssemblyLoad += Loaded; Patch(); HTFModManagerSupport.Init(harmony); } } internal static void LocalizeRow(Transform parent, ConfigEntryBase entry) { try { if ((Object)(object)parent == (Object)null || entry == null || !Owner(entry, out var guid)) { return; } ConfigDefinition definition = entry.Definition; string text = "modmenu.config." + definition.Section + "." + definition.Key; ModLocalizer modLocalizer = Localizer(guid); if (modLocalizer == null) { return; } string localizedText = modLocalizer.GetLocalizedText(text); if (string.Equals(localizedText, text, StringComparison.Ordinal)) { return; } string b = "SettingRow_" + definition.Key; Transform val = null; for (int num = parent.childCount - 1; num >= 0; num--) { Transform child = parent.GetChild(num); if (!((Object)(object)child == (Object)null) && string.Equals(((Object)child).name, b, StringComparison.Ordinal)) { val = child.Find("Label"); if ((Object)(object)val != (Object)null) { break; } } } if ((Object)(object)val == (Object)null) { return; } Component[] components = ((Component)val).GetComponents(); foreach (Component val2 in components) { if (!((Object)(object)val2 == (Object)null)) { PropertyInfo property = ((object)val2).GetType().GetProperty("text", BindingFlags.Instance | BindingFlags.Public); if (!(property == null) && property.CanWrite && !(property.PropertyType != typeof(string))) { property.SetValue(val2, localizedText, null); break; } } } } catch { } } private static void Patch() { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Expected O, but got Unknown if (_patched || _harmony == null) { return; } Type type = Find("HowToFish.ModMenu.NativeModMenu"); if (type == null) { return; } MethodInfo methodInfo = AccessTools.Method(type, "CreateConfigSettingRow", new Type[4] { typeof(Transform), typeof(ConfigEntryBase), typeof(string), typeof(float) }, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(ModMenuPatch), "Row", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { return; } try { _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patched = true; AppDomain.CurrentDomain.AssemblyLoad -= Loaded; } catch { } } private static Type Find(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (string.Equals(assembly.GetName().Name, "HowToFish.ModMenu", StringComparison.OrdinalIgnoreCase)) { return assembly.GetType(fullName, throwOnError: false); } } return null; } private static void Loaded(object sender, AssemblyLoadEventArgs args) { if (args.LoadedAssembly.GetName().Name.Equals("HowToFish.ModMenu", StringComparison.OrdinalIgnoreCase)) { Patch(); } HTFModManagerSupport.Patch(args.LoadedAssembly); } private static void LangChanged(string language) { object obj = Menu(); if (obj == null) { return; } Type type = obj.GetType(); FieldInfo fieldInfo = AccessTools.Field(type, "_open"); if (!(fieldInfo == null)) { object value = fieldInfo.GetValue(obj); if (value is bool && (bool)value) { AccessTools.Method(type, "RenderSettings", (Type[])null, (Type[])null)?.Invoke(obj, null); } } } private static object Menu() { if (!Chainloader.PluginInfos.TryGetValue("community.howtofish.modmenu", out var value) || value == null || (Object)(object)value.Instance == (Object)null) { return null; } return AccessTools.Field(((object)value.Instance).GetType(), "_menu")?.GetValue(value.Instance); } private static bool Owner(ConfigEntryBase entry, out string guid) { guid = null; ConfigFile configFile = entry.ConfigFile; if (configFile == null) { return false; } if (Owners.TryGetValue(configFile, out guid)) { return true; } foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; if (value != null && !((Object)(object)value.Instance == (Object)null) && value.Instance.Config != null && value.Instance.Config == configFile) { guid = pluginInfo.Key; Owners[configFile] = guid; return true; } } return false; } internal static bool OwnerFor(ConfigFile config, out string guid) { guid = null; if (config == null) { return false; } if (Owners.TryGetValue(config, out guid)) { return true; } foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; if (value != null && !((Object)(object)value.Instance == (Object)null) && value.Instance.Config != null && value.Instance.Config == config) { guid = pluginInfo.Key; Owners[config] = guid; return true; } } return false; } private static ModLocalizer Localizer(string guid) { object obj = AccessTools.Field(typeof(LocalizationApi), "Localizers")?.GetValue(null); if (obj == null) { return null; } MethodInfo methodInfo = AccessTools.Method(obj.GetType(), "TryGetValue", (Type[])null, (Type[])null); if (methodInfo == null) { return null; } object[] array = new object[2] { guid, null }; object obj2 = methodInfo.Invoke(obj, array); if (!(obj2 is bool) || !(bool)obj2) { return null; } return array[1] as ModLocalizer; } internal static ModLocalizer GetLocalizer(string guid) { return Localizer(guid); } } } namespace HowtoFishLocalizationAPI.Integrations.HTFModManager { internal static class HTFModManagerPatch { private static void Row(Transform __0, ConfigDefinition __1, ConfigEntryBase __2) { HTFModManagerSupport.LocalizeRow(__0, __1, __2); } } internal static class HTFModManagerSupport { private const string AssemblyName = "HTFModManager"; private const string TypeName = "HTFModManager.Plugin"; private const string PluginGuid = "com.howtofish.htfmodmanager"; private const string LabelPrefix = "modmenu.config."; private static Harmony _harmony; private static bool _patched; private static bool _initialized; internal static void Init(Harmony harmony) { if (!_initialized) { _initialized = true; _harmony = harmony; LocalizationApi.LanguageChanged += LangChanged; Patch(); } } internal static void Patch(Assembly loaded = null) { //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Expected O, but got Unknown if (_patched || _harmony == null || (loaded != null && !string.Equals(loaded.GetName().Name, "HTFModManager", StringComparison.OrdinalIgnoreCase))) { return; } Type type = Find(); if (type == null) { return; } MethodInfo methodInfo = AccessTools.Method(type, "AddGenericConfigRow", new Type[4] { typeof(Transform), typeof(ConfigDefinition), typeof(ConfigEntryBase), typeof(ConfigFile) }, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(HTFModManagerPatch), "Row", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { return; } try { _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patched = true; } catch { } } internal static void LocalizeRow(Transform layout, ConfigDefinition definition, ConfigEntryBase entry) { try { if ((Object)(object)layout == (Object)null || entry == null || !Owner(entry, out var guid)) { return; } ModLocalizer modLocalizer = Localizer(guid); if (modLocalizer == null) { return; } string text = "modmenu.config." + definition.Section + "." + definition.Key; string localizedText = modLocalizer.GetLocalizedText(text); if (string.Equals(localizedText, text, StringComparison.Ordinal)) { return; } string[] array = new string[4] { "CfgBool_", "CfgNumber_", "CfgText_", "CfgChoice_" }; Transform val = null; for (int i = 0; i < array.Length; i++) { string b = array[i] + Sanitize(definition.Key); for (int num = layout.childCount - 1; num >= 0; num--) { Transform child = layout.GetChild(num); if ((Object)(object)child != (Object)null && string.Equals(((Object)child).name, b, StringComparison.Ordinal)) { val = child; break; } } if ((Object)(object)val != (Object)null) { break; } } if ((Object)(object)val == (Object)null) { return; } Component[] componentsInChildren = ((Component)val).GetComponentsInChildren(true); Component val2 = null; foreach (Component val3 in componentsInChildren) { if ((Object)(object)val3 == (Object)null) { continue; } PropertyInfo property = ((object)val3).GetType().GetProperty("text", BindingFlags.Instance | BindingFlags.Public); if (!(property == null) && property.CanWrite && !(property.PropertyType != typeof(string))) { if ((Object)(object)val2 == (Object)null) { val2 = val3; } string name = ((Object)val3.gameObject).name; if (name.IndexOf("Text", StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf("Title", StringComparison.OrdinalIgnoreCase) >= 0) { property.SetValue(val3, localizedText, null); return; } } } if ((Object)(object)val2 != (Object)null) { ((object)val2).GetType().GetProperty("text", BindingFlags.Instance | BindingFlags.Public)?.SetValue(val2, localizedText, null); } } catch { } } private static Type Find() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (string.Equals(assembly.GetName().Name, "HTFModManager", StringComparison.OrdinalIgnoreCase)) { return assembly.GetType("HTFModManager.Plugin", throwOnError: false); } } return null; } private static void LangChanged(string language) { if (!Chainloader.PluginInfos.TryGetValue("com.howtofish.htfmodmanager", out var value) || value == null || (Object)(object)value.Instance == (Object)null) { return; } try { Type type = ((object)value.Instance).GetType(); FieldInfo fieldInfo = AccessTools.Field(type, "_configViewOpen"); if (fieldInfo == null) { return; } object value2 = fieldInfo.GetValue(value.Instance); if (!(value2 is bool) || !(bool)value2) { return; } FieldInfo fieldInfo2 = AccessTools.Field(type, "_selectedPlugin"); FieldInfo fieldInfo3 = AccessTools.Field(type, "_modsContent"); object? obj = fieldInfo2?.GetValue(value.Instance); PluginInfo val = (PluginInfo)((obj is PluginInfo) ? obj : null); object? obj2 = fieldInfo3?.GetValue(value.Instance); Transform val2 = (Transform)((obj2 is Transform) ? obj2 : null); object obj3; if (val == null) { obj3 = null; } else { BaseUnityPlugin instance = val.Instance; obj3 = ((instance != null) ? instance.Config : null); } ConfigFile val3 = (ConfigFile)obj3; if ((Object)(object)val2 == (Object)null || val3 == null) { return; } foreach (KeyValuePair item in val3) { LocalizeRow(val2, item.Key, item.Value); } } catch { } } private static bool Owner(ConfigEntryBase entry, out string guid) { guid = null; ConfigFile configFile = entry.ConfigFile; if (configFile == null) { return false; } return ModMenuSupport.OwnerFor(configFile, out guid); } private static ModLocalizer Localizer(string guid) { return ModMenuSupport.GetLocalizer(guid); } private static string Sanitize(string value) { if (string.IsNullOrEmpty(value)) { return "Unknown"; } char[] array = value.ToCharArray(); for (int i = 0; i < array.Length; i++) { if (!char.IsLetterOrDigit(array[i])) { array[i] = '_'; } } return new string(array); } } } namespace HowtoFishLocalizationAPI.Api { public static class LocalizationApi { [Serializable] private sealed class LocalSave { public int SavedLanguage = -1; } public const string DefaultLanguage = "en"; private static readonly Dictionary Localizers = new Dictionary(StringComparer.OrdinalIgnoreCase); private static string _currentLanguage = "en"; public static string CurrentLanguage => _currentLanguage; public static event Action LanguageChanged; public static ModLocalizer For(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { throw new ArgumentException("Plugin GUID cannot be empty.", "pluginGuid"); } if (!Localizers.TryGetValue(pluginGuid, out var value)) { value = new ModLocalizer(); Localizers.Add(pluginGuid, value); } return value; } public static bool TryGetCurrentGameLanguage(out string language) { language = CurrentLanguage; return true; } internal static void NotifyLanguageChanged(int? languageIndex = null) { _currentLanguage = (languageIndex.HasValue ? (Lang(languageIndex.Value) ?? "en") : Read()); Action languageChanged = LocalizationApi.LanguageChanged; if (languageChanged == null) { return; } Delegate[] invocationList = languageChanged.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(_currentLanguage); } catch (Exception ex) { ManualLogSource log = HowtoFishLocalizationAPI.Log; if (log != null) { log.LogError((object)("Language callback failed: " + ex)); } } } } private static string Read() { //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) string text = ReadSave(); if (!string.IsNullOrEmpty(text)) { return text; } switch (LocalizationManager.CurLanguage) { case 0: return "en"; case 1: return "sv"; case 2: return "zh-CN"; case 3: return "zh-TW"; case 4: return "fr"; case 5: return "de"; case 6: return "it"; case 7: return "ja"; case 8: return "ko"; case 9: return "pl"; case 10: return "pt-BR"; case 11: return "ru"; case 12: return "es-MX"; case 13: return "es-ES"; case 14: return "tr"; case 15: return "uk"; default: if (LocalizationSettings.InitializationOperation.IsDone && (Object)(object)LocalizationSettings.SelectedLocale != (Object)null) { LocaleIdentifier identifier = LocalizationSettings.SelectedLocale.Identifier; string code = ((LocaleIdentifier)(ref identifier)).Code; if (!string.IsNullOrWhiteSpace(code)) { if (code.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) { if (code.IndexOf("TW", StringComparison.OrdinalIgnoreCase) < 0 && code.IndexOf("HK", StringComparison.OrdinalIgnoreCase) < 0) { return "zh-CN"; } return "zh-TW"; } return code; } } return "en"; } } private static string ReadSave() { string path = Path.Combine(Application.persistentDataPath, "Saves", "local.txt"); if (!File.Exists(path)) { return null; } try { LocalSave localSave = JsonConvert.DeserializeObject(File.ReadAllText(path)); return (localSave == null) ? null : Lang(localSave.SavedLanguage); } catch { return null; } } private static string Lang(int index) { return index switch { 0 => "en", 1 => "sv", 2 => "zh-CN", 3 => "zh-TW", 4 => "fr", 5 => "de", 6 => "it", 7 => "ja", 8 => "ko", 9 => "pl", 10 => "pt-BR", 11 => "ru", 12 => "es-MX", 13 => "es-ES", 14 => "tr", 15 => "uk", _ => null, }; } } public sealed class ModLocalizer { private readonly Dictionary> _localizations = new Dictionary>(StringComparer.OrdinalIgnoreCase); internal ModLocalizer() { } public void RegisterJson(string jsonPath) { if (string.IsNullOrWhiteSpace(jsonPath)) { throw new ArgumentException("Localization file path cannot be empty.", "jsonPath"); } if (!File.Exists(jsonPath)) { throw new FileNotFoundException("Localization file not found.", jsonPath); } Dictionary> dictionary = JsonConvert.DeserializeObject>>(File.ReadAllText(jsonPath)); if (dictionary == null || !dictionary.ContainsKey("en")) { throw new InvalidDataException("Localization file must contain a en section."); } _localizations.Clear(); foreach (KeyValuePair> item in dictionary) { if (!string.IsNullOrWhiteSpace(item.Key) && item.Value != null) { _localizations[item.Key] = item.Value; } } } public string GetLocalizedText(string key, params object[] args) { if (string.IsNullOrEmpty(key)) { return string.Empty; } if (TryText(LocalizationApi.CurrentLanguage, key, out var text) || TryText("en", key, out text)) { return Fmt(text, args); } return key; } private bool TryText(string language, string key, out string text) { text = null; if (_localizations.TryGetValue(language, out var value)) { return value.TryGetValue(key, out text); } return false; } private static string Fmt(string text, object[] args) { if (args == null || args.Length == 0) { return text; } try { return string.Format(text, args); } catch (FormatException) { return text; } } } }