using System.Collections; using System; using System.Threading; using System.Threading.Tasks; using AibisDream.Utility; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; #if UNITY_EDITOR using UnityEditor; #endif namespace AibisDream.Framework { public static class LocalizationKit { private const string LocalizationPrefix = "l10n."; private static Task currentLanguageSwitchTask; private static int languageSwitchVersion; private static bool managedLocaleChange; private static bool typographyLoaderConfigured; private static bool localeChangedSubscribed; public static string CurrentLanguageCode { get; private set; } = "zh-Hans"; public static Locale CurrentLocale => LocalizationSettings.SelectedLocale; /// /// 把 LocaleCode 映射到 csv 列类别。集中在此一处,便于扩展(加新语言时修改这里 + LocaleKind 枚举 + 数据类字段)。 /// public static LocaleKind GetLocaleKind(string localeCode) { if (string.IsNullOrEmpty(localeCode)) return LocaleKind.Cn; if (localeCode.StartsWith("en", StringComparison.OrdinalIgnoreCase)) return LocaleKind.En; if (localeCode.StartsWith("ja", StringComparison.OrdinalIgnoreCase)) return LocaleKind.Ja; if (localeCode.StartsWith("ru", StringComparison.OrdinalIgnoreCase)) return LocaleKind.Ru; if (localeCode.StartsWith("es", StringComparison.OrdinalIgnoreCase)) return LocaleKind.Es; if (localeCode.StartsWith("pt", StringComparison.OrdinalIgnoreCase)) return LocaleKind.PtBr; return LocaleKind.Cn; } public static LocaleKind CurrentLocaleKind => GetLocaleKind(CurrentLanguageCode); /// /// 解析 setting.json 中的 Language:空值表示跟随系统语言,否则使用用户保存的 code。 /// public static string ResolveLanguage(string savedLanguage) { if (!string.IsNullOrWhiteSpace(savedLanguage)) { return savedLanguage.Trim(); } return MapSystemLanguage(Application.systemLanguage); } /// /// 将 Unity 系统语言映射到项目支持的 Locale code。 /// public static string MapSystemLanguage(SystemLanguage lang) => lang switch { SystemLanguage.Chinese or SystemLanguage.ChineseSimplified or SystemLanguage.ChineseTraditional => "zh-Hans", SystemLanguage.Japanese => "ja-JP", SystemLanguage.English => "en", SystemLanguage.Russian => "ru", SystemLanguage.Spanish => "es", SystemLanguage.Portuguese => "pt-BR", _ => "en", }; static LocalizationKit() { #if UNITY_EDITOR EditorApplication.playModeStateChanged += OnPlayModeChanged; if (EditorApplication.isPlayingOrWillChangePlaymode) { EnsureTypographyLoaderConfigured(); SubscribeLocaleChanged(); RefreshCurrentLanguageCodeAsync(); } #else SubscribeLocaleChanged(); RefreshCurrentLanguageCodeAsync(); #endif } private static void OnLocaleChanged(Locale locale) { UpdateCurrentLanguageCode(locale); EnumEventSystem.Global.Send(SettingChangeEvent.LocaleChanged, locale); if (!managedLocaleChange && locale != null) { EnsureTypographyLoaderConfigured(); LocaleKind localeKind = GetLocaleKind(locale.Identifier.Code); if (!TypographyService.IsActiveFor(localeKind)) currentLanguageSwitchTask = TypographyService.LoadAndActivateAsync(localeKind); } } private static void UpdateCurrentLanguageCode(Locale locale) { if (locale != null) { CurrentLanguageCode = locale.Identifier.Code; } } private static async void RefreshCurrentLanguageCodeAsync() { await LocalizationSettings.InitializationOperation.Task; UpdateCurrentLanguageCode(LocalizationSettings.SelectedLocale); } #if UNITY_EDITOR private static void OnPlayModeChanged(PlayModeStateChange playMode) { if (playMode == PlayModeStateChange.EnteredPlayMode) { EnsureTypographyLoaderConfigured(); SubscribeLocaleChanged(); RefreshCurrentLanguageCodeAsync(); } else if (playMode == PlayModeStateChange.ExitingPlayMode) { UnsubscribeLocaleChanged(); TypographyService.ResetForTests(); typographyLoaderConfigured = false; currentLanguageSwitchTask = null; } } #endif public static void SwitchLanguage(string localeCode) { _ = SwitchLanguageAsync(localeCode); } public static Task SwitchLanguageAsync(string localeCode) { int requestVersion = Interlocked.Increment(ref languageSwitchVersion); currentLanguageSwitchTask = SwitchLanguageCoreAsync(localeCode, requestVersion); return currentLanguageSwitchTask; } private static async Task SwitchLanguageCoreAsync(string localeCode, int requestVersion) { await LocalizationSettings.InitializationOperation.Task; var selectedLocale = LocalizationSettings.AvailableLocales.Locales.Find(locale => locale.Identifier.Code == localeCode); if (!selectedLocale) { Debug.LogWarning($"Locale with code {localeCode} not found."); return false; } EnsureTypographyLoaderConfigured(); bool typographyReady = await TypographyService.LoadAndActivateAsync(GetLocaleKind(localeCode)); if (requestVersion != languageSwitchVersion) return false; managedLocaleChange = true; try { LocalizationSettings.SelectedLocale = selectedLocale; } finally { managedLocaleChange = false; } await Task.Yield(); Canvas.ForceUpdateCanvases(); if (!typographyReady) { Debug.LogWarning( $"[LocalizationKit] Locale 已切换为 {localeCode},但字体 Profile 加载失败,保留上一套字体。"); } return true; } private static void EnsureTypographyLoaderConfigured() { if (typographyLoaderConfigured) return; TypographyService.ConfigureLoader(new ResourceSystemTypographyProfileLoader()); typographyLoaderConfigured = true; } private static void SubscribeLocaleChanged() { if (localeChangedSubscribed) return; LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged; localeChangedSubscribed = true; } private static void UnsubscribeLocaleChanged() { if (!localeChangedSubscribed) return; LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged; localeChangedSubscribed = false; } /// /// 判断字符串是否为 l10n 参数引用。 /// public static bool IsLocalizedParam(string value) { return !string.IsNullOrEmpty(value) && value.StartsWith(LocalizationPrefix, StringComparison.Ordinal); } /// /// 从 Params String Table 异步解析 l10n 参数。非 l10n 参数原样返回。 /// 当前 Locale 条目为空时保留空值,不使用任何 Locale 回退。 /// public static async Task LocalizeParamAsync(string param, Locale locale = null) { if (!IsLocalizedParam(param)) return param; string localizationKey = GetL10NParamKey(param); if (string.IsNullOrEmpty(localizationKey)) { Debug.LogError("[LocalizationKit] l10n 参数缺少 Entry Key。"); return "⟦empty⟧"; } try { await LocalizationSettings.InitializationOperation.Task; Locale resolvedLocale = locale != null ? locale : LocalizationSettings.SelectedLocale; var operation = LocalizationSettings.StringDatabase.GetTableEntryAsync( ConstRef.ParamsTable, localizationKey, resolvedLocale, FallbackBehavior.DontUseFallback); var result = await operation.Task; if (result.Entry != null) return result.Entry.LocalizedValue ?? string.Empty; string localeCode = resolvedLocale != null ? resolvedLocale.Identifier.Code : ""; Debug.LogError( $"[LocalizationKit] Params 缺少条目:Table={ConstRef.ParamsTable}, " + $"Key={localizationKey}, Locale={localeCode}。"); return $"⟦{localizationKey}⟧"; } catch (Exception exception) { string localeCode = locale != null ? locale.Identifier.Code : LocalizationSettings.SelectedLocale?.Identifier.Code ?? ""; Debug.LogError( $"[LocalizationKit] Params 查询失败:Table={ConstRef.ParamsTable}, " + $"Key={localizationKey}, Locale={localeCode}。\n{exception}"); return $"⟦{localizationKey}⟧"; } } /// /// 协程版 l10n 参数解析,供 Yarn Command 和 MonoBehaviour 使用。 /// public static IEnumerator LocalizeParam( string param, Locale locale, Action completed) { Task task = LocalizeParamAsync(param, locale); while (!task.IsCompleted) yield return null; string result = task.Status == TaskStatus.RanToCompletion ? task.Result : $"⟦{GetL10NParamKey(param)}⟧"; completed?.Invoke(result); } public static string GetL10NParamKey(string key) { // 本地化参数则去除前缀,非本地化参数直接返回 return IsLocalizedParam(key) ? key[LocalizationPrefix.Length..] : key; } /// /// 判断解析结果是否是当前 l10n. 引用对应的缺失标记。 /// 非本地化原始文本即使使用相同括号格式也不会被误判。 /// public static bool IsMissingParamResult(string source, string localizedValue) { if (!IsLocalizedParam(source)) return false; string key = GetL10NParamKey(source); string marker = string.IsNullOrEmpty(key) ? "⟦empty⟧" : $"⟦{key}⟧"; return string.Equals(localizedValue, marker, StringComparison.Ordinal); } /// /// 确保本地化系统已初始化 /// /// 初始化操作 public static async void EnsureInitialized() { await LocalizationSettings.InitializationOperation.Task; } /// /// 等待本地化系统初始化完成(含 SettingLoader 触发的语言切换)。 /// public static IEnumerator WaitUntilReady() { yield return LocalizationSettings.InitializationOperation; Task languageTask; do { languageTask = currentLanguageSwitchTask; while (languageTask != null && !languageTask.IsCompleted) yield return null; } while (languageTask != currentLanguageSwitchTask); EnsureTypographyLoaderConfigured(); if (!TypographyService.IsReady) { Task typographyTask = TypographyService.LoadAndActivateAsync( GetLocaleKind(LocalizationSettings.SelectedLocale?.Identifier.Code)); while (!typographyTask.IsCompleted) yield return null; } yield return null; } /// /// 等待 LocalizeStringEvent 等 UI 组件刷新文案。 /// public static IEnumerator WaitForUIRefresh() { yield return null; Canvas.ForceUpdateCanvases(); } } }