Files
bansonic_beta_main/Assets/scripts/localization/LocalizationService.cs
T
2026-07-22 05:35:53 +08:00

535 lines
17 KiB
C#

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<string, Dictionary<string, string>> EntryFallbacks = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, Dictionary<string, string>> LiteralFallbacks = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
public static event Action<string> LanguageChanged;
public static string CurrentLanguageCode
{
get
{
EnsureInitialized();
return _currentLanguageCode;
}
}
public static bool IsEnglish => string.Equals(CurrentLanguageCode, "en-US", StringComparison.OrdinalIgnoreCase);
public static IReadOnlyList<string> GetAvailableLanguageCodes()
{
EnsureInitialized();
if (!LocalizationSettings.HasSettings || LocalizationSettings.AvailableLocales == null)
{
return new[] { DefaultLanguageCode, "en-US" };
}
IList<Locale> locales = LocalizationSettings.AvailableLocales.Locales;
if (locales == null || locales.Count == 0)
{
return new[] { DefaultLanguageCode, "en-US" };
}
List<string> results = new List<string>(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)。
}
/// <summary>
/// 初始化 Unity Localization(Addressables 表数据库)。必须在引擎主循环运行后调用
/// (如首帧协程 / 场景加载后),不能在 BeforeSceneLoad 同步阶段调用,否则死锁。
/// 未完成前,本地化走运行时回退包,不影响 Get() 可用性。
/// </summary>
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<Locale> 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<string, string>(StringComparer.Ordinal);
LiteralFallbacks[normalized] = new Dictionary<string, string>(StringComparer.Ordinal);
return;
}
try
{
string json = File.ReadAllText(path);
LocalizationLanguagePack pack = Newtonsoft.Json.JsonConvert.DeserializeObject<LocalizationLanguagePack>(json)
?? new LocalizationLanguagePack();
EntryFallbacks[normalized] = pack.entries ?? new Dictionary<string, string>(StringComparer.Ordinal);
LiteralFallbacks[normalized] = pack.literals ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalizationService] Failed to load runtime pack '{normalized}': {ex.Message}");
EntryFallbacks[normalized] = new Dictionary<string, string>(StringComparer.Ordinal);
LiteralFallbacks[normalized] = new Dictionary<string, string>(StringComparer.Ordinal);
}
}
private static string ResolveFromRuntimeFallback(Dictionary<string, Dictionary<string, string>> source, string key)
{
string normalized = NormalizeLanguageCode(_currentLanguageCode);
LoadRuntimeFallbackPack(normalized);
if (source.TryGetValue(normalized, out Dictionary<string, string> 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<string, string> 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<string, string> entries = new Dictionary<string, string>(StringComparer.Ordinal);
public Dictionary<string, string> literals = new Dictionary<string, string>(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<string, string>(StringComparer.Ordinal),
literals = new Dictionary<string, string>(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;
}