354 lines
13 KiB
C#
354 lines
13 KiB
C#
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<bool> 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;
|
|
|
|
/// <summary>
|
|
/// 把 LocaleCode 映射到 csv 列类别。集中在此一处,便于扩展(加新语言时修改这里 + LocaleKind 枚举 + 数据类字段)。
|
|
/// </summary>
|
|
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);
|
|
|
|
/// <summary>
|
|
/// 解析 setting.json 中的 Language:空值表示跟随系统语言,否则使用用户保存的 code。
|
|
/// </summary>
|
|
public static string ResolveLanguage(string savedLanguage)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(savedLanguage))
|
|
{
|
|
return savedLanguage.Trim();
|
|
}
|
|
|
|
return MapSystemLanguage(Application.systemLanguage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将 Unity 系统语言映射到项目支持的 Locale code。
|
|
/// </summary>
|
|
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<bool> SwitchLanguageAsync(string localeCode)
|
|
{
|
|
int requestVersion = Interlocked.Increment(ref languageSwitchVersion);
|
|
currentLanguageSwitchTask = SwitchLanguageCoreAsync(localeCode, requestVersion);
|
|
return currentLanguageSwitchTask;
|
|
}
|
|
|
|
private static async Task<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断字符串是否为 l10n 参数引用。
|
|
/// </summary>
|
|
public static bool IsLocalizedParam(string value)
|
|
{
|
|
return !string.IsNullOrEmpty(value) &&
|
|
value.StartsWith(LocalizationPrefix, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 Params String Table 异步解析 l10n 参数。非 l10n 参数原样返回。
|
|
/// 当前 Locale 条目为空时保留空值,不使用任何 Locale 回退。
|
|
/// </summary>
|
|
public static async Task<string> 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
|
|
: "<null>";
|
|
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 ?? "<null>";
|
|
Debug.LogError(
|
|
$"[LocalizationKit] Params 查询失败:Table={ConstRef.ParamsTable}, " +
|
|
$"Key={localizationKey}, Locale={localeCode}。\n{exception}");
|
|
return $"⟦{localizationKey}⟧";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 协程版 l10n 参数解析,供 Yarn Command 和 MonoBehaviour 使用。
|
|
/// </summary>
|
|
public static IEnumerator LocalizeParam(
|
|
string param,
|
|
Locale locale,
|
|
Action<string> completed)
|
|
{
|
|
Task<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断解析结果是否是当前 <c>l10n.</c> 引用对应的缺失标记。
|
|
/// 非本地化原始文本即使使用相同括号格式也不会被误判。
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 确保本地化系统已初始化
|
|
/// </summary>
|
|
/// <returns>初始化操作</returns>
|
|
public static async void EnsureInitialized()
|
|
{
|
|
await LocalizationSettings.InitializationOperation.Task;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 等待本地化系统初始化完成(含 SettingLoader 触发的语言切换)。
|
|
/// </summary>
|
|
public static IEnumerator WaitUntilReady()
|
|
{
|
|
yield return LocalizationSettings.InitializationOperation;
|
|
|
|
Task<bool> languageTask;
|
|
do
|
|
{
|
|
languageTask = currentLanguageSwitchTask;
|
|
while (languageTask != null && !languageTask.IsCompleted)
|
|
yield return null;
|
|
} while (languageTask != currentLanguageSwitchTask);
|
|
|
|
EnsureTypographyLoaderConfigured();
|
|
if (!TypographyService.IsReady)
|
|
{
|
|
Task<bool> typographyTask = TypographyService.LoadAndActivateAsync(
|
|
GetLocaleKind(LocalizationSettings.SelectedLocale?.Identifier.Code));
|
|
while (!typographyTask.IsCompleted)
|
|
yield return null;
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 等待 LocalizeStringEvent 等 UI 组件刷新文案。
|
|
/// </summary>
|
|
public static IEnumerator WaitForUIRefresh()
|
|
{
|
|
yield return null;
|
|
Canvas.ForceUpdateCanvases();
|
|
}
|
|
}
|
|
}
|