SHA256
Add Language System
This commit is contained in:
4 files changed
+437
No files matched your search
@@ -0,0 +1,241 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using MelonLoader;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to localized strings and handles language synchronization with the game.
|
||||
/// </summary>
|
||||
internal static class LocalizationManager
|
||||
{
|
||||
private static readonly Dictionary<string, Dictionary<string, string>> Locales = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly string FallbackLocaleCode = "en";
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when the active language changes.
|
||||
/// </summary>
|
||||
public static event Action<SystemLanguage>? OnLanguageChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Currently active SystemLanguage.
|
||||
/// </summary>
|
||||
public static SystemLanguage CurrentLanguage { get; private set; } = SystemLanguage.English;
|
||||
|
||||
/// <summary>
|
||||
/// Currently active locale code (e.g. "en", "ko").
|
||||
/// </summary>
|
||||
public static string CurrentLocaleCode { get; private set; } = "en";
|
||||
|
||||
private static bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the localization system and loads embedded language dictionaries.
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
LoadEmbeddedLocales();
|
||||
|
||||
// Detect initial language from game settings
|
||||
var initialLang = DetectGameLanguage();
|
||||
SetLanguage(initialLang, notify: false);
|
||||
|
||||
LogMsg($"[Localization] Initialized. Active language: {CurrentLanguage} ({CurrentLocaleCode})");
|
||||
}
|
||||
|
||||
private static void LogMsg(string msg)
|
||||
{
|
||||
try { Melon<RenderMod>.Logger.Msg(msg); }
|
||||
catch { Console.WriteLine(msg); }
|
||||
}
|
||||
|
||||
private static void LogError(string msg)
|
||||
{
|
||||
try { Melon<RenderMod>.Logger.Error(msg); }
|
||||
catch { Console.Error.WriteLine(msg); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all *.json locale files embedded in this assembly.
|
||||
/// </summary>
|
||||
private static void LoadEmbeddedLocales()
|
||||
{
|
||||
var assembly = typeof(LocalizationManager).Assembly;
|
||||
var resourceNames = assembly.GetManifestResourceNames();
|
||||
|
||||
foreach (var resourceName in resourceNames)
|
||||
{
|
||||
if (!resourceName.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (!resourceName.Contains("Localization.Locale")) continue;
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null) continue;
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
|
||||
|
||||
if (dict != null)
|
||||
{
|
||||
// Extract locale code from resource name: e.g. "ADOFAIRenderer.Localization.Locale.ko.json" -> "ko"
|
||||
var parts = resourceName.Split('.');
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
var code = parts[^2].ToLowerInvariant();
|
||||
Locales[code] = dict;
|
||||
LogMsg($"[Localization] Loaded locale '{code}' ({dict.Count} keys).");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"[Localization] Failed to load locale resource '{resourceName}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the active language and notifies subscribers if changed.
|
||||
/// </summary>
|
||||
public static void SetLanguage(SystemLanguage language, bool notify = true)
|
||||
{
|
||||
var localeCode = LanguageToLocaleCode(language);
|
||||
if (!Locales.ContainsKey(localeCode))
|
||||
{
|
||||
// Fallback to English if translation for this language does not exist
|
||||
localeCode = FallbackLocaleCode;
|
||||
}
|
||||
|
||||
var isChanged = (language != CurrentLanguage) || (localeCode != CurrentLocaleCode);
|
||||
|
||||
CurrentLanguage = language;
|
||||
CurrentLocaleCode = localeCode;
|
||||
|
||||
if (isChanged && notify)
|
||||
{
|
||||
LogMsg($"[Localization] Language changed to: {CurrentLanguage} ({CurrentLocaleCode})");
|
||||
try
|
||||
{
|
||||
OnLanguageChanged?.Invoke(CurrentLanguage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"[Localization] Error during OnLanguageChanged notification: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a localized string for the specified key.
|
||||
/// </summary>
|
||||
public static string Get(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return string.Empty;
|
||||
|
||||
// 1. Try currently active locale
|
||||
if (Locales.TryGetValue(CurrentLocaleCode, out var activeDict) && activeDict.TryGetValue(key, out var val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
// 2. Fallback to English locale
|
||||
if (CurrentLocaleCode != FallbackLocaleCode &&
|
||||
Locales.TryGetValue(FallbackLocaleCode, out var fallbackDict) &&
|
||||
fallbackDict.TryGetValue(key, out var fallbackVal))
|
||||
{
|
||||
return fallbackVal;
|
||||
}
|
||||
|
||||
// 3. Fallback to raw key if missing
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a formatted localized string.
|
||||
/// </summary>
|
||||
public static string Get(string key, params object[] args)
|
||||
{
|
||||
var template = Get(key);
|
||||
if (args == null || args.Length == 0) return template;
|
||||
|
||||
try
|
||||
{
|
||||
return string.Format(template, args);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps Unity's SystemLanguage enum to an internal locale code.
|
||||
/// </summary>
|
||||
private static string LanguageToLocaleCode(SystemLanguage language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
SystemLanguage.Korean => "ko",
|
||||
SystemLanguage.English => "en",
|
||||
_ => "en"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to read the game's current language from RDString or Persistence.
|
||||
/// </summary>
|
||||
private static SystemLanguage DetectGameLanguage()
|
||||
{
|
||||
try
|
||||
{
|
||||
// First attempt: RDString.language
|
||||
return RDString.language;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fallback attempt: Persistence.language
|
||||
return Persistence.language;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return SystemLanguage.English;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenient shorthand alias for <see cref="LocalizationManager"/>.
|
||||
/// </summary>
|
||||
internal static class I18n
|
||||
{
|
||||
/// <inheritdoc cref="LocalizationManager.Get(string)"/>
|
||||
public static string Get(string key) => LocalizationManager.Get(key);
|
||||
|
||||
/// <inheritdoc cref="LocalizationManager.Get(string, object[])"/>
|
||||
public static string Get(string key, params object[] args) => LocalizationManager.Get(key, args);
|
||||
|
||||
/// <inheritdoc cref="LocalizationManager.CurrentLanguage"/>
|
||||
public static SystemLanguage CurrentLanguage => LocalizationManager.CurrentLanguage;
|
||||
|
||||
/// <inheritdoc cref="LocalizationManager.CurrentLocaleCode"/>
|
||||
public static string CurrentLocaleCode => LocalizationManager.CurrentLocaleCode;
|
||||
|
||||
/// <inheritdoc cref="LocalizationManager.OnLanguageChanged"/>
|
||||
public static event Action<SystemLanguage>? OnLanguageChanged
|
||||
{
|
||||
add => LocalizationManager.OnLanguageChanged += value;
|
||||
remove => LocalizationManager.OnLanguageChanged -= value;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user