diff --git a/Localization/LocalizationManager.cs b/Localization/LocalizationManager.cs
index 15bb674..d449e91 100644
--- a/Localization/LocalizationManager.cs
+++ b/Localization/LocalizationManager.cs
@@ -9,29 +9,49 @@ namespace ADOFAIRenderer.Localization;
///
/// Provides access to localized strings and handles language synchronization with the game.
+///
+/// Locale files are flat JSON objects ("key": "text") under Localization/Locale/, embedded in the mod
+/// assembly as resources; the file name without extension is the locale code (en.json -> "en").
+/// The active locale follows the game language: LanguageSyncPatch calls after the game's
+/// RDString.Setup and RDString.ChangeLanguage. Only Korean maps to its own locale ("ko"); every other
+/// language uses English ("en").
+///
+///
+/// Lookup order in : active locale, then English, then the key itself.
+/// Intended for the Unity main thread; the dictionaries are filled once during initialization and only read afterwards.
+///
///
internal static class LocalizationManager
{
+ /// Loaded locales: locale code (case-insensitive) -> key -> text. Keys are case-sensitive.
private static readonly Dictionary> Locales = new(StringComparer.OrdinalIgnoreCase);
+ /// Locale used when the language has no locale file and for keys missing in the active locale.
private static readonly string FallbackLocaleCode = "en";
///
/// Event triggered when the active language changes.
+ /// Raised by with notify: true when the language or the resolved locale differs from
+ /// the current one; the argument is the new game language. Exceptions from handlers are caught and logged
+ /// (an exception stops the remaining handlers of that invocation).
///
public static event Action? OnLanguageChanged;
///
- /// Currently active SystemLanguage.
+ /// Currently active SystemLanguage: the game language as passed to , also when it has no
+ /// locale file of its own (e.g. Japanese, which displays English text).
///
public static SystemLanguage CurrentLanguage { get; private set; } = SystemLanguage.English;
///
- /// Currently active locale code (e.g. "en", "ko").
+ /// Currently active locale code (e.g. "en", "ko"): the locale whose strings returns first.
+ /// Always a loaded locale or the fallback "en".
///
public static string CurrentLocaleCode { get; private set; } = "en";
+ /// Set by the first call; later calls return immediately.
private static bool _initialized;
+ /// Initializes on first access to any member, so lookups work before RenderMod calls .
static LocalizationManager()
{
Initialize();
@@ -39,6 +59,8 @@ internal static class LocalizationManager
///
/// Initializes the localization system and loads embedded language dictionaries.
+ /// Runs once; further calls do nothing. Selects the initial language without raising
+ /// .
///
public static void Initialize()
{
@@ -47,19 +69,26 @@ internal static class LocalizationManager
LoadEmbeddedLocales();
- // Detect initial language from game settings
+ // Detect initial language from game settings.
+ // At mod initialization RDString.Setup has usually not run yet, so this reads the field's initial value
+ // (English); LanguageSyncPatch applies the real language when the game calls RDString.Setup.
var initialLang = DetectGameLanguage();
SetLanguage(initialLang, notify: false);
LogMsg($"[Localization] Initialized. Active language: {CurrentLanguage} ({CurrentLocaleCode})");
}
+ ///
+ /// Logs through the MelonLoader logger of the mod, or to the console when that logger is not available
+ /// (e.g. when this class is used outside the running mod).
+ ///
private static void LogMsg(string msg)
{
try { Melon.Logger.Msg(msg); }
catch { Console.WriteLine(msg); }
}
+ /// Error counterpart of ; falls back to standard error.
private static void LogError(string msg)
{
try { Melon.Logger.Error(msg); }
@@ -68,6 +97,9 @@ internal static class LocalizationManager
///
/// Loads all *.json locale files embedded in this assembly.
+ /// A resource is used when its name ends with ".json" and contains "Localization.Locale" (the manifest name built
+ /// from the folder path, see the EmbeddedResource item in the .csproj). A file that fails to read or parse is
+ /// logged and skipped; a later resource with the same locale code replaces an earlier one.
///
private static void LoadEmbeddedLocales()
{
@@ -109,7 +141,13 @@ internal static class LocalizationManager
///
/// Sets the active language and notifies subscribers if changed.
+ /// The locale is chosen by ; when that locale is not loaded, "en" is used.
///
+ /// Game language (as used by RDString.language).
+ ///
+ /// Whether to raise when the language or locale changes; false is used for
+ /// the initial selection.
+ ///
public static void SetLanguage(SystemLanguage language, bool notify = true)
{
var localeCode = LanguageToLocaleCode(language);
@@ -140,7 +178,10 @@ internal static class LocalizationManager
///
/// Retrieves a localized string for the specified key.
+ /// Order: active locale, then English, then the key itself (so a missing key is visible in the UI).
///
+ /// Localization key, e.g. "ui.render_config.window_title"; case-sensitive.
+ /// The localized text, the key when no locale has it, or an empty string for a null/empty key.
public static string Get(string key)
{
if (string.IsNullOrEmpty(key)) return string.Empty;
@@ -166,7 +207,12 @@ internal static class LocalizationManager
///
/// Retrieves a formatted localized string.
+ /// The text from is used as a template
+ /// ({0}, {1}, ...), formatted with the current culture. When the template does not match the
+ /// arguments (), the unformatted template is returned.
///
+ /// Localization key.
+ /// Format arguments; with none, the template is returned as is.
public static string Get(string key, params object[] args)
{
var template = Get(key);
@@ -184,6 +230,7 @@ internal static class LocalizationManager
///
/// Maps Unity's SystemLanguage enum to an internal locale code.
+ /// Korean -> "ko"; every other language -> "en". A new locale file also needs an entry here.
///
private static string LanguageToLocaleCode(SystemLanguage language)
{
@@ -196,19 +243,22 @@ internal static class LocalizationManager
///
/// Attempts to read the game's current language from RDString or Persistence.
+ /// Falls back to English when both reads throw.
///
private static SystemLanguage DetectGameLanguage()
{
try
{
- // First attempt: RDString.language
+ // First attempt: RDString.language, a public static field of the game (initial value English,
+ // assigned from Persistence.language in RDString.Setup)
return RDString.language;
}
catch
{
try
{
- // Fallback attempt: Persistence.language
+ // Fallback attempt: Persistence.language, the saved "language" preference (defaults to
+ // Application.systemLanguage)
return Persistence.language;
}
catch
@@ -221,6 +271,7 @@ internal static class LocalizationManager
///
/// Convenient shorthand alias for .
+/// Every member forwards to the member of the same name in .
///
// ReSharper disable once InconsistentNaming
internal static class I18n
diff --git a/UI/CommonWarningDialogUI.cs b/UI/CommonWarningDialogUI.cs
index 86a8b8b..54a04c5 100644
--- a/UI/CommonWarningDialogUI.cs
+++ b/UI/CommonWarningDialogUI.cs
@@ -11,14 +11,29 @@ using Image = UnityEngine.UI.Image;
namespace ADOFAIRenderer.UI;
+///
+/// Description of one button of a dialog: label, click action and optional color.
+///
internal class DialogButton
{
// ReSharper disable FieldCanBeMadeReadOnly.Global
+ /// Button label, shown as given (callers pass already localized text).
public string Text;
+
+ ///
+ /// Action run on the Unity main thread when the button is clicked. The dialog is destroyed after the action
+ /// returns, also when the action throws (the exception is logged).
+ ///
public Action OnClick;
+
+ /// Normal (idle) background color of the button; null keeps the O5Kit theme button color.
public Color? BackgroundColor;
// ReSharper restore FieldCanBeMadeReadOnly.Global
+ /// Creates a button description.
+ /// Button label.
+ /// Action run on click, before the dialog closes.
+ /// Optional normal background color; null uses the theme color.
public DialogButton(string text, Action onClick, Color? backgroundColor = null)
{
Text = text;
@@ -27,10 +42,29 @@ internal class DialogButton
}
}
+///
+/// Modal message dialog with a title, a description and one or more buttons.
+///
+/// Each call creates its own GameObject (kept across scene loads) with a screen-space overlay
+/// canvas: a translucent full-screen backdrop that blocks input to everything below, and an O5Kit window on top of it.
+/// While the dialog exists, every enabled UI Toolkit root () is disabled so that it cannot
+/// receive input; the roots are enabled again in . Clicking any button runs its action and then
+/// destroys the dialog. All members run on the Unity main thread.
+///
+///
internal class CommonWarningDialogUI : MonoBehaviour
{
+ ///
+ /// The most recently created dialog, or null when that dialog has been destroyed.
+ /// FirstUpdateHook checks it to wait until a startup dialog is closed.
+ /// If several dialogs are open at once, this refers to the newest one only.
+ ///
public static CommonWarningDialogUI? Instance { get; private set; }
+
+ /// UI Toolkit root elements disabled by this dialog, enabled again when the dialog is destroyed.
private readonly List _disabledElements = new();
+
+ /// O5Kit window holding the title, description and buttons; disposed in .
private O5Window? _window;
private void Awake()
@@ -38,6 +72,10 @@ internal class CommonWarningDialogUI : MonoBehaviour
Instance = this;
}
+ ///
+ /// Disposes the window, enables the UI Toolkit roots disabled in and clears
+ /// if it still refers to this dialog.
+ ///
private void OnDestroy()
{
_window?.Dispose();
@@ -58,6 +96,15 @@ internal class CommonWarningDialogUI : MonoBehaviour
}
}
+ ///
+ /// Creates and shows a new dialog. Must be called on the Unity main thread.
+ ///
+ /// Window title text.
+ /// Body text, centered and word-wrapped; TextMeshPro rich text tags are allowed.
+ ///
+ /// Buttons laid out left to right with equal widths. With no buttons nothing is shown, because the dialog
+ /// has no close button and could not be dismissed.
+ ///
public static void ShowDialog(string title, string description, params DialogButton[] buttons)
{
if (buttons.Length == 0) return;
@@ -68,8 +115,14 @@ internal class CommonWarningDialogUI : MonoBehaviour
dialog.Initialize(title, description, buttons);
}
+ ///
+ /// Builds the canvas, backdrop, window and contents. Layout sizes are in canvas units at the 1920x1080 reference
+ /// resolution of the .
+ ///
private void Initialize(string title, string description, DialogButton[] buttons)
{
+ // uGUI buttons need an EventSystem to receive clicks. When the current scene has none, a child EventSystem
+ // is created; it is destroyed together with the dialog.
if (!UnityEngine.EventSystems.EventSystem.current)
{
var eventSystemObj = new GameObject("EventSystem");
@@ -78,6 +131,8 @@ internal class CommonWarningDialogUI : MonoBehaviour
eventSystemObj.AddComponent();
}
+ // Disable every enabled UI Toolkit root so that it ignores input while the dialog is open.
+ // Roots that are already disabled are left alone and are not re-enabled in OnDestroy.
foreach (var doc in FindObjectsByType(FindObjectsSortMode.None))
{
if (doc && doc.rootVisualElement is { enabledSelf: true })
@@ -87,14 +142,17 @@ internal class CommonWarningDialogUI : MonoBehaviour
}
}
- // Set UI layer (5)
+ // Set UI layer (5, Unity's built-in "UI" layer)
gameObject.layer = 5;
- // Canvas setup
+ // Canvas setup.
+ // Sorting order 32766 draws the dialog above the RendererUIManager window canvas (32000), the render overlays
+ // (DebugMonitorUI 9999, EscapeHandlerUI 10000) and below FFmpegDownloadUI (32767, the largest sorting order).
var canvas = gameObject.AddComponent