From 004f62eb77e7b7708ef86a4059315dd32e928af2 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:15:30 -0500 Subject: [PATCH] Tweak system for registering changes --- MiraAPI/Roles/ICustomRole.cs | 2 +- MiraAPI/Translation/MiraLocaleManager.cs | 213 ++++++++++-------- .../Translation/MiraLocalizationProvider.cs | 6 + 3 files changed, 126 insertions(+), 95 deletions(-) diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 13020ff..d01b108 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -22,7 +22,7 @@ public interface ICustomRole : IOptionable /// /// Gets the id part used to build the role's translation keys. It is recommended to call it ModGuid.Role.Faction /// - string IdPrefix => GetType().Namespace!; + string IdPrefix => MiraLocaleManager.BuildTranslationId(GetType().Namespace!, "Role", Team.ToString()); /// /// Gets the name of the role. diff --git a/MiraAPI/Translation/MiraLocaleManager.cs b/MiraAPI/Translation/MiraLocaleManager.cs index d0a642a..90f0df4 100644 --- a/MiraAPI/Translation/MiraLocaleManager.cs +++ b/MiraAPI/Translation/MiraLocaleManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Xml; using MiraAPI.Utilities; +using MonoMod.Utils; using Reactor.Localization.Utilities; using UnityEngine; @@ -14,7 +15,9 @@ public static class MiraLocaleManager { private const string LangDirectory = "mira_languages"; - public static readonly Dictionary>> Locale = []; + // Language, Xml Name, then Value + public static Dictionary> Locale { get; } = []; + public static readonly List RegisteredModIds = []; public static readonly Dictionary RegisteredStringNames = []; internal static readonly Dictionary StringNamesLookup = []; @@ -88,22 +91,19 @@ public static class MiraLocaleManager } /// - /// Registers translations for a mod. Copies embedded XML to mira_languages/{modGuid}/ on first run, - /// then loads all XML files from that directory into memory. + /// Registers translations for a mod. /// Call once per mod during plugin Load(). /// /// The mod GUID (e.g., "mira.example"). public static void Register(string modGuid) { var callingAssembly = Assembly.GetCallingAssembly(); - var dir = GetModLangDir(modGuid); - LoadInternalStrings(callingAssembly, callingAssembly.GetName().Name!, modGuid, dir); + LoadInternalStrings(callingAssembly, callingAssembly.GetName().Name!, modGuid); } /// - /// Registers translations for a mod. Copies embedded XML to mira_languages/{modGuid}/ on first run, - /// then loads all XML files from that directory into memory. + /// Registers translations for a mod. /// Call once per mod during plugin Load(). /// /// The mod GUID (e.g., "mira.example"). @@ -111,80 +111,95 @@ public static class MiraLocaleManager public static void Register(string modGuid, string internalName) { var callingAssembly = Assembly.GetCallingAssembly(); - var dir = GetModLangDir(modGuid); - LoadInternalStrings(callingAssembly, internalName, modGuid, dir); + LoadInternalStrings(callingAssembly, internalName, modGuid); } /// /// Translates a key into the current language. - /// Searches all mods, with reverse lookup fallback. /// /// The string id to find. /// Fallback string to use if no translation is found. /// A based on the key provided. public static string Get(string key, string fallback = "") { - var currentLang = CurrentLanguage; + return Get(CurrentLanguage, key, fallback != string.Empty ? fallback : key); + } - foreach (var (_, modLocale) in Locale) + /// + /// Translates a key into the current language. + /// + /// The specific language to prioritize. + /// The string id to find. + /// Fallback string to use if no translation is found. + /// A based on the key provided. + public static string Get(MiraLanguage language, string key, string fallback = "") + { + if (Locale.TryGetValue(language, out var translations) && + translations.TryGetValue(key, out var translation)) { - if (modLocale.TryGetValue(currentLang, out var langDict) && - langDict.TryGetValue(key, out var translated)) - { - return translated; - } + return translation; } - foreach (var (_, modLocale) in Locale) + if (Locale.TryGetValue(MiraLanguage.English, out var translationsEng) && + translationsEng.TryGetValue(key, out var translationEng)) { - if (modLocale.TryGetValue(MiraLanguage.English, out var engDict) && - engDict.TryGetValue(key, out var english)) - { - return english; - } + return translationEng; } - string? bestStructuredKey = null; - foreach (var (_, modLocale) in Locale) - { - if (modLocale.TryGetValue(MiraLanguage.English, out var engDict)) - { - foreach (var (structuredKey, englishValue) in engDict) - { - if (englishValue == key) - { - bestStructuredKey = structuredKey; - break; - } - } - } + return fallback; + } - if (bestStructuredKey != null) break; + /// + /// Translates a key into the current language. + /// + /// The string id to find. + /// List of keys to change into other text. + /// Fallback string to use if no translation is found. + /// A based on the key provided. + public static string GetParsed( + string key, + Dictionary parseList, + string fallback = "") + { + return GetParsed(CurrentLanguage, key, parseList, fallback != string.Empty ? fallback : key); + } + + /// + /// Translates a key into the current language. + /// + /// The specific language to prioritize. + /// The string id to find. + /// List of keys to change into other text. + /// Fallback string to use if no translation is found. + /// A based on the key provided. + public static string GetParsed( + MiraLanguage language, + string key, + Dictionary parseList, + string fallback = "") + { + var text = fallback; + + if (Locale.TryGetValue(MiraLanguage.English, out var translationsEng) && + translationsEng.TryGetValue(key, out var translationEng)) + { + text = translationEng; } - if (bestStructuredKey != null) + if (language is not MiraLanguage.English && + Locale.TryGetValue(language, out var translations) && + translations.TryGetValue(key, out var translation)) { - foreach (var (_, modLocale) in Locale) - { - if (modLocale.TryGetValue(currentLang, out var langDict) && - langDict.TryGetValue(bestStructuredKey, out var revTranslated)) - { - return revTranslated; - } - } - - foreach (var (_, modLocale) in Locale) - { - if (modLocale.TryGetValue(MiraLanguage.English, out var engDict) && - engDict.TryGetValue(bestStructuredKey, out var revEnglish)) - { - return revEnglish; - } - } + text = translation; } - return fallback != string.Empty ? fallback : key; + foreach (var tmpText in parseList.Where(x => text.Contains(x.Key))) + { + text = text.Replace(tmpText.Key, tmpText.Value); + } + + return text; } public static string BuildTranslationId(string modId, string idPart, string suffix) @@ -215,17 +230,49 @@ public static class MiraLocaleManager return newString; } - private static void LoadInternalStrings(Assembly assembly, string internalName, string modGuid, string dir) + public static void LoadExternalLocale() + { + CheckExternalDirectory("mira.api", BepInEx.Paths.PluginPath); + CheckExternalDirectory("mira.api", BepInEx.Paths.BepInExRootPath); + CheckExternalDirectory("mira.api", BepInEx.Paths.GameRootPath); + foreach (var mod in RegisteredModIds) + { + CheckExternalDirectory(mod, GetModLangDir(mod)); + } + } + + public static void CheckExternalDirectory(string modGuid, string dir) { Directory.CreateDirectory(dir); - if (!Locale.ContainsKey(modGuid)) + foreach (var filePath in Directory.GetFiles(dir, "*.xml")) { - Locale[modGuid] = []; - } + var fileName = Path.GetFileNameWithoutExtension(filePath); + var lang = LangList.ContainsValue(fileName) + ? LangList.First(x => x.Value == fileName).Key + : (MiraLanguage)(-1); + if ((int)lang == -1) continue; + + try + { + Locale.TryAdd(lang, []); + var xmlContent = File.ReadAllText(filePath); + ParseXmlFile(xmlContent, lang, false); + } + catch (Exception e) + { + Error($"Failed to load external translation {filePath}: {e.Message}"); + } + } + } + + private static void LoadInternalStrings(Assembly assembly, string internalName, string modGuid) + { var resourcePrefix = $"{internalName}.Resources.Locale."; + RegisteredModIds.Add(modGuid); + var atLeastOneLoaded = false; foreach (var locale in LangList) { @@ -241,9 +288,8 @@ public static class MiraLocaleManager string xmlContent = reader.ReadToEnd(); try { - var dict = ParseXmlFile(xmlContent); - Locale[modGuid][locale.Key] = dict; - Info($"Loaded {locale.Key.ToDisplayString()} translation for mod {modGuid} ({dict.Count} keys)"); + Locale.TryAdd(locale.Key, []); + ParseXmlFile(xmlContent, locale.Key, true); atLeastOneLoaded = true; } catch (Exception e) @@ -256,37 +302,11 @@ public static class MiraLocaleManager { Error($"No internal strings were found for {internalName}!"); } - - if (!Locale.TryGetValue(modGuid, out _)) - { - Locale[modGuid] = []; - } - - foreach (var filePath in Directory.GetFiles(dir, "*")) - { - var fileName = Path.GetFileNameWithoutExtension(filePath); - - var lang = LangList.ContainsValue(fileName) - ? LangList.First(x => x.Value == fileName).Key - : (MiraLanguage)(-1); - if ((int)lang == -1) continue; - - try - { - var dict = ParseXmlFile(filePath); - Locale[modGuid][lang] = dict; - Info($"Loaded external {lang.ToDisplayString()} translation for mod {modGuid} ({dict.Count} keys)"); - } - catch (Exception e) - { - Error($"Failed to load external translation {filePath}: {e.Message}"); - } - } } - private static Dictionary ParseXmlFile(string xmlContent) + private static void ParseXmlFile(string xmlContent, MiraLanguage language, bool loadingInternal) { - var dict = new Dictionary(); + var dict = Locale[language]; XmlDocument xmlDoc = new(); try { @@ -295,6 +315,7 @@ public static class MiraLocaleManager if (stringNodes != null) { + var total = 0; foreach (XmlNode node in stringNodes) { if (node.Attributes?["name"] != null) @@ -316,16 +337,20 @@ public static class MiraLocaleManager value = value.Replace("", "\n").Replace("", "&"); + if (loadingInternal && Locale[language].ContainsKey(name)) + { + Error($"String for \"{name}\" in {language} was overwritten by duplicate!"); + } dict[name] = value; + total++; } } + Info($"Loaded {language.ToDisplayString()} translation with ({total} keys)"); } } catch (XmlException ex) { Error($"XML parsing error: {ex.Message}"); } - - return dict; } } diff --git a/MiraAPI/Translation/MiraLocalizationProvider.cs b/MiraAPI/Translation/MiraLocalizationProvider.cs index 47cf747..3ac1133 100644 --- a/MiraAPI/Translation/MiraLocalizationProvider.cs +++ b/MiraAPI/Translation/MiraLocalizationProvider.cs @@ -11,6 +11,7 @@ public class MiraLocalizationProvider : LocalizationProvider { public override int Priority => ReactorPriority.Normal; private static LocalizationProvider? _reactorProvider; + private static bool _loadedStrings; public override bool TryGetText(StringNames stringName, out string? result) { @@ -39,6 +40,11 @@ public class MiraLocalizationProvider : LocalizationProvider { MiraApiPlugin.Culture = new(culture); } + if (!_loadedStrings) + { + MiraLocaleManager.LoadExternalLocale(); + _loadedStrings = true; + } foreach (var tab in LocalSettingsTab.TabGroups) {