using System; using System.Collections.Generic; using System.Globalization; using System.IO; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; public static class LocalizationService { public const string LanguagePrefKey = "bansonic_language_code"; public const string DefaultLanguageCode = "zh-CN"; public const string GameTextTableCollectionName = "GameText"; public const string LiteralTextTableCollectionName = "GameLiteralText"; private static readonly object SyncRoot = new object(); private static bool _initialized; private static string _currentLanguageCode = DefaultLanguageCode; private static readonly Dictionary> EntryFallbacks = new Dictionary>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary> LiteralFallbacks = new Dictionary>(StringComparer.OrdinalIgnoreCase); public static event Action LanguageChanged; public static string CurrentLanguageCode { get { EnsureInitialized(); return _currentLanguageCode; } } public static bool IsEnglish => string.Equals(CurrentLanguageCode, "en-US", StringComparison.OrdinalIgnoreCase); public static IReadOnlyList GetAvailableLanguageCodes() { EnsureInitialized(); if (!LocalizationSettings.HasSettings || LocalizationSettings.AvailableLocales == null) { return new[] { DefaultLanguageCode, "en-US" }; } IList locales = LocalizationSettings.AvailableLocales.Locales; if (locales == null || locales.Count == 0) { return new[] { DefaultLanguageCode, "en-US" }; } List results = new List(locales.Count); for (int i = 0; i < locales.Count; i++) { Locale locale = locales[i]; if (locale == null) { continue; } string normalized = NormalizeLanguageCode(locale.Identifier.Code); if (!results.Contains(normalized)) { results.Add(normalized); } } if (results.Count == 0) { results.Add(DefaultLanguageCode); results.Add("en-US"); } else { if (!results.Contains(DefaultLanguageCode)) { results.Insert(0, DefaultLanguageCode); } if (!results.Contains("en-US")) { results.Add("en-US"); } } return results; } // Addressables/LocalizationSettings 是否已完成初始化(与基础初始化分离)。 private static bool _settingsInitialized; public static void EnsureInitialized() { if (_initialized) { return; } lock (SyncRoot) { if (_initialized) { return; } // 只做"安全的同步基础初始化":语言偏好 + 运行时回退包(从 StreamingAssets 读)。 // Get()/TryGet() 会先查表、查不到再走这些回退包,所以此时本地化已可用。 _currentLanguageCode = NormalizeLanguageCode(PlayerPrefs.GetString(LanguagePrefKey, ResolveStartupLanguageCode())); PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode); PlayerPrefs.Save(); LoadRuntimeFallbackPack(DefaultLanguageCode); LoadRuntimeFallbackPack("en-US"); _initialized = true; } // 关键(修复启动死锁):绝不在此处同步 WaitForCompletion 等 Addressables 初始化。 // 该方法可能在 [RuntimeInitializeOnLoadMethod(BeforeSceneLoad)] 阶段被调用, // 此时引擎主循环尚未运行,WaitForCompletion 会自旋泵送 Addressables 异步链, // 而其 continuation 需要主线程/尚未就绪的子系统 → WaitOnAddress 永远等不到信号而死锁。 // 改为把 LocalizationSettings 初始化推迟到引擎就绪后(EnsureLocalizationSettingsInitialized)。 } /// /// 初始化 Unity Localization(Addressables 表数据库)。必须在引擎主循环运行后调用 /// (如首帧协程 / 场景加载后),不能在 BeforeSceneLoad 同步阶段调用,否则死锁。 /// 未完成前,本地化走运行时回退包,不影响 Get() 可用性。 /// public static void EnsureLocalizationSettingsInitialized() { if (_settingsInitialized) return; lock (SyncRoot) { if (_settingsInitialized) return; if (!_initialized) EnsureInitialized(); if (LocalizationSettings.HasSettings) { // 引擎已就绪,此时 WaitForCompletion 安全(异步链能被正常泵送完成)。 LocalizationSettings.InitializeSynchronously = true; var init = LocalizationSettings.InitializationOperation; if (!init.IsDone) { init.WaitForCompletion(); } if (LocalizationSettings.StringDatabase != null) { LocalizationSettings.StringDatabase.MissingTranslationState = (MissingTranslationBehavior)0; } LocalizationSettings.SelectedLocaleChanged -= HandleSelectedLocaleChanged; LocalizationSettings.SelectedLocaleChanged += HandleSelectedLocaleChanged; ApplySelectedLocale(_currentLanguageCode, savePreference: true, notifyListeners: false); } _settingsInitialized = true; } } public static void SetLanguage(string languageCode) { EnsureInitialized(); // 运行时切换语言由 UI 触发(引擎已就绪),此处确保 Localization 设置已初始化,安全。 EnsureLocalizationSettingsInitialized(); string normalized = NormalizeLanguageCode(languageCode); if (string.Equals(_currentLanguageCode, normalized, StringComparison.OrdinalIgnoreCase)) { return; } if (LocalizationSettings.HasSettings) { ApplySelectedLocale(normalized, savePreference: true, notifyListeners: false); return; } _currentLanguageCode = normalized; PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode); PlayerPrefs.Save(); LanguageChanged?.Invoke(_currentLanguageCode); } public static string Get(string key, string fallback = null) { if (TryGet(key, out string localized)) { return localized; } if (string.IsNullOrWhiteSpace(key)) { return fallback ?? string.Empty; } return fallback ?? key; } public static string GetFormat(string key, params object[] args) { string format = Get(key, key); try { return string.Format(CultureInfo.InvariantCulture, format, args); } catch { return format; } } public static string LocalizeLiteral(string source) { if (TryGetLiteral(source, out string localized)) { return localized; } if (string.IsNullOrEmpty(source)) { return source; } return source; } public static bool TryGet(string key, out string localized) { localized = null; EnsureInitialized(); if (string.IsNullOrWhiteSpace(key)) { return false; } localized = ResolveFromTable(GameTextTableCollectionName, key); if (!string.IsNullOrEmpty(localized)) { return true; } localized = ResolveFromRuntimeFallback(EntryFallbacks, key); return !string.IsNullOrEmpty(localized); } public static bool TryGetLiteral(string source, out string localized) { localized = null; EnsureInitialized(); if (string.IsNullOrWhiteSpace(source)) { return false; } localized = ResolveFromTable(LiteralTextTableCollectionName, source); if (!string.IsNullOrEmpty(localized)) { return true; } localized = ResolveFromRuntimeFallback(LiteralFallbacks, source); return !string.IsNullOrEmpty(localized); } private static void HandleSelectedLocaleChanged(Locale locale) { _currentLanguageCode = NormalizeLanguageCode(locale?.Identifier.Code ?? DefaultLanguageCode); PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode); PlayerPrefs.Save(); LanguageChanged?.Invoke(_currentLanguageCode); } private static void ApplySelectedLocale(string languageCode, bool savePreference, bool notifyListeners) { string normalized = NormalizeLanguageCode(languageCode); Locale targetLocale = FindLocale(normalized) ?? FindLocale(DefaultLanguageCode); if (targetLocale == null) { _currentLanguageCode = normalized; if (savePreference) { PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode); PlayerPrefs.Save(); } if (notifyListeners) { LanguageChanged?.Invoke(_currentLanguageCode); } return; } bool localeChanged = LocalizationSettings.SelectedLocale == null || !string.Equals(LocalizationSettings.SelectedLocale.Identifier.Code, targetLocale.Identifier.Code, StringComparison.OrdinalIgnoreCase); _currentLanguageCode = NormalizeLanguageCode(targetLocale.Identifier.Code); if (savePreference) { PlayerPrefs.SetString(LanguagePrefKey, _currentLanguageCode); PlayerPrefs.Save(); } if (localeChanged) { LocalizationSettings.SelectedLocale = targetLocale; } else if (notifyListeners) { LanguageChanged?.Invoke(_currentLanguageCode); } } private static Locale FindLocale(string languageCode) { if (!LocalizationSettings.HasSettings || LocalizationSettings.AvailableLocales == null) { return null; } string normalized = NormalizeLanguageCode(languageCode); IList locales = LocalizationSettings.AvailableLocales.Locales; if (locales == null) { return null; } for (int i = 0; i < locales.Count; i++) { Locale locale = locales[i]; if (locale == null) { continue; } if (string.Equals(NormalizeLanguageCode(locale.Identifier.Code), normalized, StringComparison.OrdinalIgnoreCase)) { return locale; } } if (normalized.StartsWith("en", StringComparison.OrdinalIgnoreCase)) { for (int i = 0; i < locales.Count; i++) { Locale locale = locales[i]; if (locale != null && locale.Identifier.Code.StartsWith("en", StringComparison.OrdinalIgnoreCase)) { return locale; } } } if (normalized.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) { for (int i = 0; i < locales.Count; i++) { Locale locale = locales[i]; if (locale != null && locale.Identifier.Code.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) { return locale; } } } return null; } private static string ResolveFromTable(string tableName, string entryKey) { if (!LocalizationSettings.HasSettings || LocalizationSettings.StringDatabase == null) { return null; } try { string value = LocalizationSettings.StringDatabase.GetLocalizedString(tableName, entryKey, LocalizationSettings.SelectedLocale); return string.IsNullOrEmpty(value) ? null : value; } catch (Exception ex) { Debug.LogWarning($"[LocalizationService] Failed to resolve '{entryKey}' from '{tableName}': {ex.Message}"); return null; } } private static void LoadRuntimeFallbackPack(string languageCode) { string normalized = NormalizeLanguageCode(languageCode); if (EntryFallbacks.ContainsKey(normalized) && LiteralFallbacks.ContainsKey(normalized)) { return; } string path = Path.Combine(Application.streamingAssetsPath, "localization", normalized + ".json"); if (!File.Exists(path)) { EntryFallbacks[normalized] = new Dictionary(StringComparer.Ordinal); LiteralFallbacks[normalized] = new Dictionary(StringComparer.Ordinal); return; } try { string json = File.ReadAllText(path); LocalizationLanguagePack pack = Newtonsoft.Json.JsonConvert.DeserializeObject(json) ?? new LocalizationLanguagePack(); EntryFallbacks[normalized] = pack.entries ?? new Dictionary(StringComparer.Ordinal); LiteralFallbacks[normalized] = pack.literals ?? new Dictionary(StringComparer.Ordinal); } catch (Exception ex) { Debug.LogWarning($"[LocalizationService] Failed to load runtime pack '{normalized}': {ex.Message}"); EntryFallbacks[normalized] = new Dictionary(StringComparer.Ordinal); LiteralFallbacks[normalized] = new Dictionary(StringComparer.Ordinal); } } private static string ResolveFromRuntimeFallback(Dictionary> source, string key) { string normalized = NormalizeLanguageCode(_currentLanguageCode); LoadRuntimeFallbackPack(normalized); if (source.TryGetValue(normalized, out Dictionary primary) && primary != null && primary.TryGetValue(key, out string localized) && !string.IsNullOrEmpty(localized)) { return localized; } if (!string.Equals(normalized, DefaultLanguageCode, StringComparison.OrdinalIgnoreCase)) { LoadRuntimeFallbackPack(DefaultLanguageCode); if (source.TryGetValue(DefaultLanguageCode, out Dictionary fallback) && fallback != null && fallback.TryGetValue(key, out localized) && !string.IsNullOrEmpty(localized)) { return localized; } } return null; } private static string ResolveStartupLanguageCode() { return Application.systemLanguage switch { SystemLanguage.English => "en-US", _ => DefaultLanguageCode }; } private static string NormalizeLanguageCode(string languageCode) { if (string.IsNullOrWhiteSpace(languageCode)) { return DefaultLanguageCode; } string trimmed = languageCode.Trim(); if (string.Equals(trimmed, "en", StringComparison.OrdinalIgnoreCase)) { return "en-US"; } if (string.Equals(trimmed, "zh", StringComparison.OrdinalIgnoreCase) || string.Equals(trimmed, "zh-Hans", StringComparison.OrdinalIgnoreCase)) { return "zh-CN"; } return trimmed; } } [Serializable] public sealed class LocalizationLanguagePack { public string languageCode; public Dictionary entries = new Dictionary(StringComparer.Ordinal); public Dictionary literals = new Dictionary(StringComparer.Ordinal); } [Serializable] internal sealed class LocalizationLanguagePackJsonSurrogate { public string languageCode; public SerializableDictionaryEntry[] entries; public SerializableDictionaryEntry[] literals; public LocalizationLanguagePack ToPack() { var pack = new LocalizationLanguagePack { languageCode = languageCode, entries = new Dictionary(StringComparer.Ordinal), literals = new Dictionary(StringComparer.Ordinal) }; if (entries != null) { for (int i = 0; i < entries.Length; i++) { if (!string.IsNullOrWhiteSpace(entries[i].key)) { pack.entries[entries[i].key] = entries[i].value ?? string.Empty; } } } if (literals != null) { for (int i = 0; i < literals.Length; i++) { if (!string.IsNullOrWhiteSpace(literals[i].key)) { pack.literals[literals[i].key] = literals[i].value ?? string.Empty; } } } return pack; } } [Serializable] internal struct SerializableDictionaryEntry { public string key; public string value; }