using System; using System.Collections.Generic; using System.Globalization; using UnityEngine; namespace Ironspore { public static class GameLocalization { public static string PreferenceKey => GameDataPaths.PreferenceKey("settings.locale"); public const string FallbackCode = "en"; public static event Action Changed; public static string CurrentCode { get; private set; } = FallbackCode; public static LocalizationCatalog Catalog { get; private set; } public static CultureInfo Culture => CultureInfo.GetCultureInfo(CurrentCode); static readonly Dictionary> tables = new Dictionary>(); static readonly HashSet missing = new HashSet(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { Changed = null; Catalog = null; CurrentCode = FallbackCode; tables.Clear(); missing.Clear(); } public static void Initialize(LocalizationCatalog catalog) { if (!catalog || catalog.locales == null) throw new InvalidOperationException("Localization catalog is missing."); Catalog = catalog; tables.Clear(); missing.Clear(); foreach (var locale in catalog.locales) tables.Add(locale.code, ReadTable(locale.table)); if (!tables.ContainsKey(FallbackCode)) throw new InvalidOperationException("English fallback table is missing."); string system = CultureInfo.CurrentUICulture.Name; if (Application.systemLanguage == SystemLanguage.ChineseSimplified || Application.systemLanguage == SystemLanguage.ChineseTraditional || Application.systemLanguage == SystemLanguage.Chinese) system = "zh-Hans"; CurrentCode = ResolveInitialLocale(PlayerPrefs.GetString(PreferenceKey, ""), system, tables.Keys); if (!tables.ContainsKey(CurrentCode)) CurrentCode = FallbackCode; } public static Dictionary ReadTable(TextAsset source) { if (!source) throw new ArgumentNullException(nameof(source)); var result = new Dictionary(StringComparer.Ordinal); var table = JsonUtility.FromJson(source.text); if (table?.entries == null) throw new FormatException("Invalid translation table: " + source.name); foreach (var entry in table.entries) { if (string.IsNullOrWhiteSpace(entry.key) || string.IsNullOrWhiteSpace(entry.value)) throw new FormatException("Empty translation in " + source.name); result.Add(entry.key, entry.value); } return result; } public static string ResolveInitialLocale(string saved, string system, IEnumerable availableCodes = null) { var supported = new HashSet(availableCodes ?? new[] { "en", "zh-Hans" }, StringComparer.OrdinalIgnoreCase); if (!string.IsNullOrEmpty(saved) && supported.Contains(saved)) return saved; if (!string.IsNullOrEmpty(system)) { if (system.StartsWith("zh", StringComparison.OrdinalIgnoreCase) && supported.Contains("zh-Hans")) return "zh-Hans"; if (supported.Contains(system)) return system; string language = system.Split('-')[0]; if (supported.Contains(language)) return language; } return FallbackCode; } public static bool SelectLocale(string code, bool persist = true) { if (!tables.ContainsKey(code)) return false; bool changed = CurrentCode != code; CurrentCode = code; if (persist) { PlayerPrefs.SetString(PreferenceKey, code); PlayerPrefs.Save(); } if (changed) Changed?.Invoke(); return true; } public static string Text(string key, params object[] args) { string template = null; if (tables.TryGetValue(CurrentCode, out var selected)) selected.TryGetValue(key, out template); if (string.IsNullOrEmpty(template) && tables.TryGetValue(FallbackCode, out var fallback)) fallback.TryGetValue(key, out template); if (string.IsNullOrEmpty(template)) { if (Debug.isDebugBuild && missing.Add(key)) Debug.LogWarning("Missing localization key: " + key); return "[" + key + "]"; } return args.Length == 0 ? template : string.Format(Culture, template, args); } public static string ItemName(string id) => Text("item." + id); public static LocalizedMessage Message(string key, params object[] args) => new LocalizedMessage(key, args); } // Keep keys and parameters so existing notifications and reports follow locale changes. public sealed class LocalizedMessage { readonly string key; readonly object[] args; public LocalizedMessage(string key, params object[] args) { this.key = key; this.args = args; } public override string ToString() => GameLocalization.Text(key, args); } }