using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text.Json; using BepInEx; using BepInEx.Core.Logging.Interpolation; using BepInEx.Logging; using BepInEx.NET.Common; using Candide; using Candide.GameModels.Helpers; using HarmonyLib; using RomesteadLocalizationAPI.Api; using Shared.Text; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("Romestead Localization API")] [assembly: AssemblyDescription("Romestead Localization API by Ice Box Studio")] [assembly: AssemblyCompany("Ice Box Studio")] [assembly: AssemblyProduct("Romestead Localization API")] [assembly: AssemblyCopyright("Copyright (C) 2026 Ice Box Studio All rights reserved.")] [assembly: ComVisible(false)] [assembly: Guid("6f52443b-3b87-402a-9965-44b01b12c078")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace RomesteadLocalizationAPI { public static class PluginInfo { public const string PLUGIN_GUID = "IceBoxStudio.Romestead.LocalizationAPI"; public const string PLUGIN_NAME = "Romestead Localization API"; public const string PLUGIN_VERSION = "1.0.0"; public const string PLUGIN_AUTHOR = "Ice Box Studio"; public const string PLUGIN_AUTHOR_URL = "https://steamcommunity.com/id/ibox666/"; } [BepInPlugin("IceBoxStudio.Romestead.LocalizationAPI", "Romestead Localization API", "1.0.0")] public sealed class RomesteadLocalizationAPI : BasePlugin { private Harmony _harmony; public static RomesteadLocalizationAPI Instance { get; private set; } internal static ManualLogSource Logger { get; private set; } public override void Load() { //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected O, but got Unknown //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown Instance = this; Logger = ((BasePlugin)this).Log; bool flag = default(bool); try { _harmony = new Harmony("IceBoxStudio.Romestead.LocalizationAPI"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); RomesteadLocalization.Apply(); ManualLogSource logger = Logger; BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(13, 1, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendFormatted("Romestead Localization API"); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" initialized."); } logger.LogInfo(val); } catch (Exception ex) { ManualLogSource logger2 = Logger; BepInExErrorLogInterpolatedStringHandler val2 = new BepInExErrorLogInterpolatedStringHandler(24, 3, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val2).AppendFormatted("Romestead Localization API"); ((BepInExLogInterpolatedStringHandler)val2).AppendLiteral(" initialization error: "); ((BepInExLogInterpolatedStringHandler)val2).AppendFormatted(ex.Message); ((BepInExLogInterpolatedStringHandler)val2).AppendLiteral("\n"); ((BepInExLogInterpolatedStringHandler)val2).AppendFormatted(ex.StackTrace); } logger2.LogError(val2); } } } } namespace RomesteadLocalizationAPI.Patches { [HarmonyPatch(typeof(LocalizationManager), "SetLocalization")] internal static class LocalizationManagerPatch { private static void Postfix(string locale) { RomesteadLocalization.OnGameLocalizationLoaded(locale); } } } namespace RomesteadLocalizationAPI.Api { public static class RomesteadLocales { public const string English = "en"; public const string ChineseSimplified = "zh_CN"; public const string ChineseTraditional = "zh_TW"; public const string Czech = "cs_CZ"; public const string German = "de_DE"; public const string Greek = "el_GR"; public const string Spanish = "es_ES"; public const string French = "fr_FR"; public const string Italian = "it_IT"; public const string Japanese = "ja_JP"; public const string Korean = "ko_KR"; public const string Dutch = "nl_NL"; public const string Polish = "pl_PL"; public const string PortugueseBrazil = "pt_BR"; public const string Russian = "ru_RU"; public const string Slovak = "sk_SK"; public const string Swedish = "sv_SE"; public const string Turkish = "tr_TR"; internal static readonly string[] Known = new string[18] { "en", "zh_CN", "zh_TW", "cs_CZ", "de_DE", "el_GR", "es_ES", "fr_FR", "it_IT", "ja_JP", "ko_KR", "nl_NL", "pl_PL", "pt_BR", "ru_RU", "sk_SK", "sv_SE", "tr_TR" }; } public static class RomesteadLocalization { private sealed class Entry { public string SourceId; public readonly Dictionary Texts = new Dictionary(StringComparer.OrdinalIgnoreCase); public Entry(string sourceId) { SourceId = sourceId; } } private const string TestingLocale = ""; private static readonly object Sync = new object(); private static readonly Dictionary Entries = new Dictionary(StringComparer.Ordinal); private static string _currentLocale = "en"; public static string CurrentLocale => DetectCurrentLocale(); public static event Action LanguageChanged; public static event Action LocalizationApplied; public static RomesteadLocalizer For(string sourceId) { ValidateSourceId(sourceId); return new RomesteadLocalizer(sourceId); } public static void RegisterJsonDirectory(string sourceId, string directory) { ValidateSourceId(sourceId); if (string.IsNullOrWhiteSpace(directory)) { throw new ArgumentException("Localization directory cannot be empty.", "directory"); } if (!Directory.Exists(directory)) { throw new DirectoryNotFoundException(directory); } Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); string[] files = Directory.GetFiles(directory, "*.json", SearchOption.TopDirectoryOnly); Array.Sort(files, (IComparer?)StringComparer.OrdinalIgnoreCase); string[] array = files; foreach (string path in array) { string localeFromJsonFile = GetLocaleFromJsonFile(path); foreach (KeyValuePair item in ReadJsonTextMap(path)) { AddBundleText(dictionary, localeFromJsonFile, item.Key, item.Value); } } RegisterBundle(sourceId, dictionary); } public static void RegisterJson(string sourceId, string path) { ValidateSourceId(sourceId); RegisterBundle(sourceId, ReadJsonBundle(path)); } public static void RegisterText(string sourceId, string locale, string key, string text) { ValidateSourceId(sourceId); ValidateLocale(locale); ValidateKey(key); if (text == null) { throw new ArgumentNullException("text"); } Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); AddBundleText(dictionary, locale, key, text); RegisterBundle(sourceId, dictionary); } public static void RegisterTexts(string sourceId, string locale, IReadOnlyDictionary texts) { ValidateSourceId(sourceId); ValidateLocale(locale); if (texts == null) { throw new ArgumentNullException("texts"); } Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair text in texts) { ValidateKey(text.Key); if (text.Value == null) { throw new ArgumentException("Localization text cannot be null: " + text.Key, "texts"); } AddBundleText(dictionary, locale, text.Key, text.Value); } RegisterBundle(sourceId, dictionary); } public static void RegisterTexts(string sourceId, params (string Locale, string Key, string Text)[] texts) { ValidateSourceId(sourceId); if (texts == null) { throw new ArgumentNullException("texts"); } Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < texts.Length; i++) { (string, string, string) tuple = texts[i]; ValidateLocale(tuple.Item1); ValidateKey(tuple.Item2); if (tuple.Item3 == null) { throw new ArgumentException("Localization text cannot be null: " + tuple.Item1 + "." + tuple.Item2, "texts"); } AddBundleText(dictionary, tuple.Item1, tuple.Item2, tuple.Item3); } RegisterBundle(sourceId, dictionary); } public static string Text(string key, params object[] args) { return GetText(key, args); } public static string TextFor(string sourceId, string key, params object[] args) { return GetTextFor(sourceId, key, args); } public static string GetText(string key, params object[] args) { //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Expected O, but got Unknown if (string.IsNullOrEmpty(key)) { return ""; } string text = TryGetRegisteredText(key, CurrentLocale) ?? TryGetGameText(key) ?? key; if (args == null || args.Length == 0) { return text; } try { return string.Format(text, args); } catch (Exception ex) { ManualLogSource logger = RomesteadLocalizationAPI.Logger; if (logger != null) { bool flag = default(bool); BepInExWarningLogInterpolatedStringHandler val = new BepInExWarningLogInterpolatedStringHandler(39, 2, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("Faulty localization format string at "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(key); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(": "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(ex.Message); } logger.LogWarning(val); } return text; } } public static string GetTextFor(string sourceId, string key, params object[] args) { ValidateSourceId(sourceId); if (string.IsNullOrEmpty(key)) { return ""; } return GetText(ScopeKey(sourceId, key), args); } public static void Apply() { Apply(DetectCurrentLocale(), notify: false); } public static IReadOnlyList GetSupportedLocales() { HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); string[] known = RomesteadLocales.Known; foreach (string item in known) { hashSet.Add(item); } try { foreach (string key in Globals.SupportedLanguageCodes.Keys) { if (!string.Equals(key, "", StringComparison.OrdinalIgnoreCase)) { hashSet.Add(NormalizeLocale(key)); } } } catch { } foreach (string contentLocale in GetContentLocales()) { hashSet.Add(contentLocale); } List list = new List(hashSet); list.Sort(StringComparer.OrdinalIgnoreCase); return list; } public static string NormalizeLocale(string locale) { string text = (locale ?? string.Empty).Trim().Replace('-', '_'); switch (text) { case "": return "en"; case "en": case "en_US": case "en_GB": return "en"; case "zh": case "zh_CN": case "zh_Hans": return "zh_CN"; case "zh_TW": case "zh_HK": case "zh_MO": case "zh_Hant": return "zh_TW"; case "cs": return "cs_CZ"; case "de": return "de_DE"; case "el": return "el_GR"; case "es": return "es_ES"; case "fr": return "fr_FR"; case "it": return "it_IT"; case "ja": return "ja_JP"; case "ko": return "ko_KR"; case "nl": return "nl_NL"; case "pl": return "pl_PL"; case "pt": case "pt_PT": return "pt_BR"; case "ru": return "ru_RU"; case "sk": return "sk_SK"; case "sv": return "sv_SE"; case "tr": return "tr_TR"; default: return text; } } internal static void OnGameLocalizationLoaded(string locale) { Apply(NormalizeLocale(locale), notify: true); } private static void RegisterBundle(string sourceId, Dictionary> bundle) { //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected O, but got Unknown lock (Sync) { bool flag = default(bool); foreach (KeyValuePair> item in bundle) { string key = NormalizeLocale(item.Key); foreach (KeyValuePair item2 in item.Value) { string text = ScopeKey(sourceId, item2.Key); if (!Entries.TryGetValue(text, out var value)) { value = new Entry(sourceId); Entries[text] = value; } else if (!string.Equals(value.SourceId, sourceId, StringComparison.Ordinal)) { ManualLogSource logger = RomesteadLocalizationAPI.Logger; if (logger != null) { BepInExWarningLogInterpolatedStringHandler val = new BepInExWarningLogInterpolatedStringHandler(35, 3, ref flag); if (flag) { ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("Localization key "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(text); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" from "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(sourceId); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" overrides "); ((BepInExLogInterpolatedStringHandler)val).AppendFormatted(value.SourceId); ((BepInExLogInterpolatedStringHandler)val).AppendLiteral("."); } logger.LogWarning(val); } value.SourceId = sourceId; } value.Texts[key] = item2.Value; } } } Apply(); } private static void Apply(string locale, bool notify) { //IL_0091: Unknown result type (might be due to invalid IL or missing references) string text = NormalizeLocale(locale); bool flag = false; lock (Sync) { flag = !string.Equals(_currentLocale, text, StringComparison.OrdinalIgnoreCase); _currentLocale = text; if (string.Equals(text, "", StringComparison.OrdinalIgnoreCase)) { return; } foreach (KeyValuePair entry in Entries) { string text2 = SelectText(entry.Value.Texts, text); if (text2 != null) { StringDefinitions.Strings[entry.Key] = new StringDefinition(text2); } } } if (notify) { RomesteadLocalization.LocalizationApplied?.Invoke(text); if (flag) { RomesteadLocalization.LanguageChanged?.Invoke(text); } } } private static string TryGetRegisteredText(string key, string locale) { lock (Sync) { Entry value; return Entries.TryGetValue(key, out value) ? SelectText(value.Texts, locale) : null; } } private static string TryGetGameText(string key) { try { string text = default(string); return StringDefinitions.TryGetString(key, ref text) ? text : null; } catch { return null; } } private static string SelectText(Dictionary texts, string locale) { string text = NormalizeLocale(locale); if (string.Equals(text, "", StringComparison.OrdinalIgnoreCase)) { return null; } if (texts.TryGetValue(text, out var value)) { return value; } if (texts.TryGetValue("en", out var value2)) { return value2; } if (texts.TryGetValue("zh_CN", out var value3)) { return value3; } using (Dictionary.ValueCollection.Enumerator enumerator = texts.Values.GetEnumerator()) { if (enumerator.MoveNext()) { return enumerator.Current; } } return null; } private static string DetectCurrentLocale() { if (!string.IsNullOrWhiteSpace(StringDefinitions.CurrentLanguageCode)) { return NormalizeLocale(StringDefinitions.CurrentLanguageCode); } try { if (!string.IsNullOrWhiteSpace(Globals.LanguageCode)) { return NormalizeLocale(Globals.LanguageCode); } } catch { } string text = ReadSavedLocale(); if (!string.IsNullOrWhiteSpace(text)) { return NormalizeLocale(text); } string text2 = NormalizeLocale(CultureInfo.CurrentCulture.Name); string[] known = RomesteadLocales.Known; foreach (string b in known) { if (string.Equals(text2, b, StringComparison.OrdinalIgnoreCase)) { return text2; } } return _currentLocale; } private static string ReadSavedLocale() { try { string path = Path.Combine(GamePathHelper.GetFolderPath((GameFolder)1, false), "localization.json"); if (!File.Exists(path)) { return null; } using FileStream utf8Json = File.OpenRead(path); using JsonDocument jsonDocument = JsonDocument.Parse((Stream)utf8Json, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); JsonElement value; return (jsonDocument.RootElement.TryGetProperty("LanguageCode", out value) && value.ValueKind == JsonValueKind.String) ? value.GetString() : null; } catch { return null; } } private static IEnumerable GetContentLocales() { string root = null; try { object manager = typeof(Globals).GetField("ContentManager")?.GetValue(null); root = manager?.GetType().GetProperty("RootDirectory")?.GetValue(manager) as string; } catch { } if (string.IsNullOrWhiteSpace(root)) { yield break; } string path = Path.Combine(root, "localization"); if (!Directory.Exists(path)) { yield break; } string[] files = Directory.GetFiles(path, "locale_*"); foreach (string file in files) { string name = Path.GetFileName(file); if (name != null && name.Length > "locale_".Length) { yield return NormalizeLocale(name.Substring("locale_".Length)); } } } private static Dictionary ReadJsonTextMap(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("Localization file path cannot be empty.", "path"); } if (!File.Exists(path)) { throw new FileNotFoundException("Localization file was not found.", path); } using FileStream utf8Json = File.OpenRead(path); using JsonDocument jsonDocument = JsonDocument.Parse((Stream)utf8Json, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); if (jsonDocument.RootElement.ValueKind != JsonValueKind.Object) { throw new JsonException("Localization file must contain a JSON object: " + path); } Dictionary dictionary = new Dictionary(StringComparer.Ordinal); foreach (JsonProperty item in jsonDocument.RootElement.EnumerateObject()) { if (item.Value.ValueKind != JsonValueKind.String) { throw new JsonException("Localization value must be a string: " + path + " -> " + item.Name); } dictionary[item.Name] = item.Value.GetString(); } return dictionary; } private static Dictionary> ReadJsonBundle(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("Localization bundle path cannot be empty.", "path"); } if (!File.Exists(path)) { throw new FileNotFoundException("Localization bundle was not found.", path); } using FileStream utf8Json = File.OpenRead(path); using JsonDocument jsonDocument = JsonDocument.Parse((Stream)utf8Json, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); if (jsonDocument.RootElement.ValueKind != JsonValueKind.Object) { throw new JsonException("Localization bundle must contain a JSON object: " + path); } Dictionary> result = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (JsonProperty item in jsonDocument.RootElement.EnumerateObject()) { if (!IsLocaleName(item.Name)) { throw new JsonException("Localization bundle top-level key must be a locale: " + path + " -> " + item.Name); } if (item.Value.ValueKind != JsonValueKind.Object) { throw new JsonException("Localization locale value must be an object: " + path + " -> " + item.Name); } foreach (JsonProperty item2 in item.Value.EnumerateObject()) { AddBundleText(result, item.Name, item2.Name, item2.Value, path); } } return result; } private static string GetLocaleFromJsonFile(string path) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); if (fileNameWithoutExtension == null) { return "en"; } return fileNameWithoutExtension.StartsWith("locale_", StringComparison.OrdinalIgnoreCase) ? NormalizeLocale(fileNameWithoutExtension.Substring("locale_".Length)) : NormalizeLocale(fileNameWithoutExtension); } private static bool IsLocaleName(string value) { string a = NormalizeLocale(value); string[] known = RomesteadLocales.Known; foreach (string b in known) { if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { return true; } } return value.Length == 2 || (value.Length == 5 && value[2] == '_') || (value.Length == 7 && value[2] == '_'); } private static void AddBundleText(Dictionary> result, string locale, string key, JsonElement value, string path) { if (value.ValueKind != JsonValueKind.String) { throw new JsonException($"Localization value must be a string: {path} -> {locale}.{key}"); } AddBundleText(result, locale, key, value.GetString()); } private static void AddBundleText(Dictionary> result, string locale, string key, string text) { if (text == null) { throw new ArgumentNullException("text"); } string key2 = NormalizeLocale(locale); if (!result.TryGetValue(key2, out var value)) { value = (result[key2] = new Dictionary(StringComparer.Ordinal)); } value[key] = text; } private static string ScopeKey(string sourceId, string key) { return key.StartsWith(sourceId + ".", StringComparison.Ordinal) ? key : (sourceId + "." + key); } private static void ValidateSourceId(string sourceId) { if (string.IsNullOrWhiteSpace(sourceId)) { throw new ArgumentException("Source id cannot be empty.", "sourceId"); } } private static void ValidateLocale(string locale) { if (string.IsNullOrWhiteSpace(locale)) { throw new ArgumentException("Locale cannot be empty.", "locale"); } } private static void ValidateKey(string key) { if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentException("Localization key cannot be empty.", "key"); } } } public sealed class RomesteadLocalizer { public string SourceId { get; } public string CurrentLocale => RomesteadLocalization.CurrentLocale; public event Action LanguageChanged { add { RomesteadLocalization.LanguageChanged += value; } remove { RomesteadLocalization.LanguageChanged -= value; } } internal RomesteadLocalizer(string sourceId) { SourceId = sourceId; } public RomesteadLocalizer RegisterJson(string path) { RomesteadLocalization.RegisterJson(SourceId, path); return this; } public RomesteadLocalizer RegisterJsonDirectory(string directory) { RomesteadLocalization.RegisterJsonDirectory(SourceId, directory); return this; } public RomesteadLocalizer RegisterText(string locale, string key, string text) { RomesteadLocalization.RegisterText(SourceId, locale, key, text); return this; } public RomesteadLocalizer RegisterTexts(string locale, IReadOnlyDictionary texts) { RomesteadLocalization.RegisterTexts(SourceId, locale, texts); return this; } public RomesteadLocalizer RegisterTexts(params (string Locale, string Key, string Text)[] texts) { RomesteadLocalization.RegisterTexts(SourceId, texts); return this; } public string Text(string key, params object[] args) { return RomesteadLocalization.TextFor(SourceId, key, args); } public string GetText(string key, params object[] args) { return RomesteadLocalization.GetTextFor(SourceId, key, args); } } }