UI系统优化

This commit is contained in:
2026-05-25 11:54:37 +08:00
parent c7057db27d
commit 3c812cfb41
130 changed files with 4738 additions and 477 deletions

View File

@@ -1,17 +1,19 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Localization",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Localization",
"references": [
"BaseGames.Core.Events",
"BaseGames.Core.Save"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}
"name": "BaseGames.Localization",
"rootNamespace": "BaseGames.Localization",
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"BaseGames.Core.Save",
"Unity.TextMeshPro"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace BaseGames.Localization
{
/// <summary>
/// 标记一个 ScriptableObject 持有可本地化字段。
/// 实现此接口后,<see cref="BaseGames.Editor.Modules.LocalizationAuditModule"/>
/// 将自动发现并检查该 SO 的所有 Key无需在审计模块中硬编码扫描逻辑。
///
/// 新增 SO 类型时:实现此接口即可自动纳入本地化审计,不需要修改审计模块。
/// </summary>
public interface ILocalizableAsset
{
/// <summary>
/// 返回该资产中所有本地化 Key 的引用列表。
/// 实现时跳过空 key<c>string.IsNullOrEmpty</c> 检查)。
/// </summary>
IEnumerable<LocalizationKeyRef> GetLocalizationKeys();
}
/// <summary>
/// 对一个本地化 Key 引用的描述,供审计工具使用。
/// </summary>
public readonly struct LocalizationKeyRef
{
/// <summary>本地化 Key 字符串。</summary>
public readonly string Key;
/// <summary>所属表名(使用 <see cref="LocalizationTable"/> 常量)。</summary>
public readonly string Table;
/// <summary>该 Key 来自的字段名称,用于审计报告中精确定位。</summary>
public readonly string FieldName;
public LocalizationKeyRef(string key, string table, string fieldName)
{
Key = key;
Table = table;
FieldName = fieldName;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4ea4cfc00373e14aadc6750c579aae7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -12,12 +12,49 @@ namespace BaseGames.Localization
/// <summary>
/// 获取本地化字符串。查找顺序:当前语言 → 回退语言English→ 直接返回 key。
/// 表名建议使用 <see cref="LocalizationTable"/> 中的常量。
/// </summary>
string Get(string key, string table = "UI");
string Get(string key, string table = LocalizationTable.UI);
/// <summary>
/// 尝试获取本地化字符串。返回 false 表示 key 在所有语言(含回退)中均不存在。
/// 与 <see cref="Get"/> 不同key 不存在时不会将 key 本身作为值返回。
/// 适用于需要区分"key 存在但值为空"和"key 完全不存在"的场景。
/// </summary>
bool TryGet(string key, out string value, string table = LocalizationTable.UI);
/// <summary>
/// 获取带格式化参数的本地化字符串(<c>string.Format</c> 风格)。
/// 例:<c>GetFormat("REWARD_GOLD", LocalizationTable.UI, amount)</c> → "获得 100 灵珠"。
/// 格式化失败时静默返回原始模板字符串,不抛出异常。
/// </summary>
string GetFormat(string key, string table, params object[] args);
/// <summary>切换游戏语言并通知所有订阅者刷新文本。</summary>
void SetLanguage(Language language);
/// <summary>
/// 同步预热指定语言的所有已知本地化表,避免首次访问时产生帧卡顿。
/// 在主线程阻塞执行,建议改用 <see cref="PreloadTablesAsync"/> 分帧加载。
/// </summary>
void PreloadTables(Language language);
/// <summary>
/// 异步分帧预热指定语言的所有本地化表(每帧加载一个表,不阻塞主线程)。
/// 建议在 Loading Screen 的协程中调用。
/// </summary>
/// <param name="language">要预热的语言。</param>
/// <param name="onComplete">全部表加载完成后的回调(可为 null。</param>
void PreloadTablesAsync(Language language, Action onComplete = null);
/// <summary>
/// 获取带数量的复数形式本地化字符串。
/// 规则:先查找 "{key}_one"count==1或 "{key}_other"count≠1找不到则回退到基础 key。
/// 查找到的模板以 <c>string.Format(template, count)</c> 展开,{0} 代入 count。
/// 示例key="ITEM_COUNT",表中配置 "ITEM_COUNT_other"="获得 {0} 个物品" → "获得 5 个物品"。
/// </summary>
string GetPlural(string key, int count, string table = LocalizationTable.UI);
/// <summary>语言切换时触发。</summary>
event Action<Language> OnLanguageChanged;
}

View File

@@ -0,0 +1,59 @@
using System;
using TMPro;
using UnityEngine;
namespace BaseGames.Localization
{
/// <summary>
/// 每种语言对应的 TMP 字体配置。
/// 创建资产Assets/Data/Localization/FontConfig.asset
///
/// 用法:将此资产拖入 <see cref="BaseGames.Localization.LocalizedText"/> 的
/// <c>Font Config</c> 字段,切换语言时 LocalizedText 将自动替换字体和材质。
///
/// CJK 语言通常需要单独的字体资产,无需为每个文本节点逐一指定,
/// 只需在此 SO 中统一配置一次即可全局生效。
/// </summary>
[CreateAssetMenu(
menuName = "BaseGames/Localization/Language Font Config",
fileName = "FontConfig")]
public class LanguageFontConfigSO : ScriptableObject
{
[Serializable]
public class FontEntry
{
[Tooltip("对应的语言。")]
public Language language;
[Tooltip("该语言使用的 TMP 字体资产(留空表示沿用默认字体)。")]
public TMP_FontAsset fontAsset;
[Tooltip("该语言字体的材质预设(留空表示使用字体默认材质)。")]
public Material fontMaterial;
}
[Tooltip("每种语言的字体映射。未列出的语言保持默认字体不变。")]
public FontEntry[] entries = Array.Empty<FontEntry>();
/// <summary>
/// 尝试获取指定语言的字体配置。
/// 返回 false 表示该语言未配置,调用方应保持现有字体不变。
/// </summary>
public bool TryGetFont(Language language, out TMP_FontAsset font, out Material material)
{
if (entries != null)
{
foreach (var entry in entries)
{
if (entry.language != language) continue;
font = entry.fontAsset;
material = entry.fontMaterial;
return true;
}
}
font = null;
material = null;
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b31f31378d72a8e42b454d4af01e51ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
// Assets/Scripts/Localization/LocalizationManager.cs
// Assets/Scripts/Localization/LocalizationManager.cs
// 本地化管理器(运行时 JSON 文件驱动)。
//
// 数据格式(放在 Resources/Localization/{Language}/{TableName}.json
@@ -15,9 +15,11 @@
//
// 便捷静态方法(内部仍走 ServiceLocator推荐在热路径之外使用
// LocalizationManager.Get("ui_start")
// LocalizationManager.Get("dlg_hero", "Dialogue")
// LocalizationManager.Get("dlg_hero", LocalizationTable.Dialogue)
// LocalizationManager.GetFormat("REWARD_GOLD", LocalizationTable.UI, 100)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
@@ -36,10 +38,17 @@ namespace BaseGames.Localization
private Language _currentLanguage = Language.ChineseSimplified;
private readonly Language _fallbackLanguage = Language.English;
// 双层缓存:languageKey("ChineseSimplified/UI") → (key → value)
private readonly Dictionary<string, Dictionary<string, string>> _cache = new();
// 双层缓存:(Language, tableName) 结构体键 → (key → value)
// 使用值类型 CacheKey 代替字符串插值,消除每次 Get 调用的 string 堆分配。
private readonly Dictionary<CacheKey, Dictionary<string, string>> _cache = new();
// ILocalizationService 实例事件
// LanguageEventChannelSO语言切换时向 SO 驱动的 UI 组件广播。
// 在 Persistent 场景预制体的 Inspector 中拖入 EVT_LanguageChanged.asset。
[SerializeField]
[Tooltip("语言切换事件频道EVT_LanguageChanged.asset。切换语言时广播订阅此频道的 UI 组件自动刷新文本。")]
private LanguageEventChannelSO _languageEventChannel;
// ILocalizationService 实例事件C# 订阅,供不方便引用 SO 的组件使用)
private event Action<Language> _onLanguageChanged;
event Action<Language> ILocalizationService.OnLanguageChanged
{
@@ -78,8 +87,22 @@ namespace BaseGames.Localization
if (_currentLanguage == language) return;
_currentLanguage = language;
_onLanguageChanged?.Invoke(language);
_languageEventChannel?.Raise(language);
// 同步到设置文件ISettingsService 存储 locale code 字符串)
ServiceLocator.GetOrDefault<ISettingsService>()?.SetLanguage(LanguageToLocaleCode(language));
}
/// <summary>将 Language 枚举转换为标准 locale code 字符串。</summary>
public static string LanguageToLocaleCode(Language language) => language switch
{
Language.ChineseSimplified => "zh-CN",
Language.Japanese => "ja-JP",
Language.Korean => "ko-KR",
Language.English => "en-US",
_ => "en-US",
};
/// <summary>
/// 获取本地化字符串(显式接口实现)。
/// 查找顺序:当前语言 → 回退语言English→ 直接返回 key。
@@ -101,6 +124,118 @@ namespace BaseGames.Localization
return key;
}
/// <summary>
/// 尝试获取本地化字符串(显式接口实现)。
/// 返回 false 时 value 为 nullkey 不会作为 value 返回。
/// </summary>
bool ILocalizationService.TryGet(string key, out string value, string table)
{
if (string.IsNullOrEmpty(key)) { value = null; return false; }
if (TryGetFromTable(_currentLanguage, table, key, out value)) return true;
if (_currentLanguage != _fallbackLanguage &&
TryGetFromTable(_fallbackLanguage, table, key, out value)) return true;
value = null;
return false;
}
/// <summary>
/// 获取带格式化参数的本地化字符串(显式接口实现)。
/// 格式化失败时静默返回原始模板字符串。
/// </summary>
string ILocalizationService.GetFormat(string key, string table, params object[] args)
{
string template = ((ILocalizationService)this).Get(key, table);
if (args == null || args.Length == 0) return template;
try { return string.Format(template, args); }
catch (Exception e)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning($"[LocalizationManager] GetFormat '{key}' 格式化失败: {e.Message}");
#else
_ = e;
#endif
return template;
}
}
/// <summary>
/// 获取带数量的复数形式本地化字符串(显式接口实现)。
/// 先查找 "{key}_one"count==1或 "{key}_other"count≠1找不到则回退到基础 key。
/// 模板以 string.Format(template, count) 展开。
/// </summary>
string ILocalizationService.GetPlural(string key, int count, string table)
{
string pluralKey = count == 1 ? $"{key}_one" : $"{key}_other";
if (TryGetFromTable(_currentLanguage, table, pluralKey, out string text) ||
(_currentLanguage != _fallbackLanguage &&
TryGetFromTable(_fallbackLanguage, table, pluralKey, out text)))
{
try { return string.Format(text, count); }
catch { return text; }
}
// 回退到基础 key
string baseText = ((ILocalizationService)this).Get(key, table);
try { return string.Format(baseText, count); }
catch { return baseText; }
}
/// <summary>
/// 同步预热指定语言的所有已知本地化表,不阻塞检测已缓存表(显式接口实现)。
/// </summary>
void ILocalizationService.PreloadTables(Language language)
{
foreach (var table in LocalizationTable.All)
{
var ck = new CacheKey(language, table);
if (_cache.ContainsKey(ck)) continue;
var dict = LoadTable(language, table);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (dict == null)
Debug.LogWarning(
$"[LocalizationManager] PreloadTables{language}/{table} 未找到," +
$"请确认 Resources/Localization/{language}/{table}.json 存在。");
#endif
_cache[ck] = dict;
}
}
/// <summary>
/// 异步分帧预热:每帧加载一个表,不阻塞主线程(显式接口实现)。
/// 建议在 Loading Screen 的协程中调用。
/// </summary>
void ILocalizationService.PreloadTablesAsync(Language language, Action onComplete)
=> StartCoroutine(PreloadTablesRoutine(language, onComplete));
private IEnumerator PreloadTablesRoutine(Language language, Action onComplete)
{
foreach (var table in LocalizationTable.All)
{
var ck = new CacheKey(language, table);
if (_cache.ContainsKey(ck)) { yield return null; continue; }
string path = $"Localization/{language}/{table}";
var request = Resources.LoadAsync<TextAsset>(path);
yield return request;
var asset = request.asset as TextAsset;
var dict = asset == null ? null : ParseTableText(asset.text);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (dict == null)
Debug.LogWarning(
$"[LocalizationManager] PreloadTablesAsync{language}/{table} 未找到," +
$"请确认 Resources/Localization/{language}/{table}.json 存在。");
#endif
_cache[ck] = dict;
}
onComplete?.Invoke();
}
// ── ISaveable ─────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
@@ -118,75 +253,91 @@ namespace BaseGames.Localization
// ── 静态便捷方法 ─────────────────────────────────────────────────────────
/// <summary>
/// 静态快捷获取本地化字符串。委托给 ILocalizationService 实例;服务未注册时直接返回 key。
/// 表名建议使用 <see cref="LocalizationTable"/> 中的常量。
/// </summary>
public static string Get(string key, string table = "UI")
public static string Get(string key, string table = LocalizationTable.UI)
=> ServiceLocator.GetOrDefault<ILocalizationService>()?.Get(key, table) ?? key;
/// <summary>
/// 静态快捷尝试获取本地化字符串。服务未注册时返回 false。
/// </summary>
public static bool TryGet(string key, out string value, string table = LocalizationTable.UI)
{
var svc = ServiceLocator.GetOrDefault<ILocalizationService>();
if (svc != null) return svc.TryGet(key, out value, table);
value = null;
return false;
}
/// <summary>
/// 静态快捷获取带格式化参数的本地化字符串。
/// 服务未注册时以 key 作为模板直接格式化后返回。
/// </summary>
public static string GetFormat(string key, string table, params object[] args)
{
var svc = ServiceLocator.GetOrDefault<ILocalizationService>();
if (svc != null) return svc.GetFormat(key, table, args);
if (args == null || args.Length == 0) return key;
try { return string.Format(key, args); }
catch { return key; }
}
/// <summary>
/// 静态快捷获取带数量的复数形式本地化字符串。
/// 服务未注册时直接返回 key。
/// </summary>
public static string GetPlural(string key, int count, string table = LocalizationTable.UI)
=> ServiceLocator.GetOrDefault<ILocalizationService>()?.GetPlural(key, count, table) ?? key;
// ── 编辑器预览(不依赖 ServiceLocator 实例)────────────────────────────
#if UNITY_EDITOR
// 编辑器预览缓存:"{language}/{table}" → (key → value)
// 生命周期与编辑器进程相同域重载时自动清空static 字段随域重载重置)。
private static readonly System.Collections.Generic.Dictionary<
string,
System.Collections.Generic.Dictionary<string, string>> s_editorPreviewCache = new();
private static readonly Dictionary<string, Dictionary<string, string>> s_editorPreviewCache = new();
/// <summary>
/// 编辑器工具专用:不依赖运行时服务实例,直接从 Resources 读取本地化文本。
/// 结果缓存在静态字典中,同一编辑器会话内同一表只加载一次。
/// 找不到时返回 null区别于运行时的 key 回退,便于调用方判断是否显示 key
/// </summary>
public static string GetEditorPreview(string key, string table = "UI",
public static string GetEditorPreview(string key, string table = LocalizationTable.UI,
Language language = Language.ChineseSimplified)
{
if (string.IsNullOrEmpty(key)) return null;
var dict = GetEditorTable(language, table)
?? GetEditorTable(Language.English, table); // 中文缺失时英文回退
?? GetEditorTable(Language.English, table);
if (dict == null) return null;
dict.TryGetValue(key, out var value);
return value; // 找不到 key 时返回 null
return value;
}
private static System.Collections.Generic.Dictionary<string, string> GetEditorTable(
Language language, string table)
public static Dictionary<string, string> GetEditorTable(Language language, string table)
{
string cacheKey = $"{language}/{table}";
if (s_editorPreviewCache.TryGetValue(cacheKey, out var cached))
return cached; // 已缓存(可能是 null 占位,表示文件不存在)
return cached;
string path = $"Localization/{language}/{table}";
var asset = Resources.Load<TextAsset>(path);
if (asset == null)
{
s_editorPreviewCache[cacheKey] = null; // 记录"不存在",避免重复尝试
return null;
}
var parsed = JsonUtility.FromJson<StringTableJson>(asset.text);
if (parsed?.entries == null)
{
s_editorPreviewCache[cacheKey] = null;
return null;
}
var dict = new System.Collections.Generic.Dictionary<string, string>(
parsed.entries.Count, System.StringComparer.Ordinal);
foreach (var entry in parsed.entries)
if (!string.IsNullOrEmpty(entry.key))
dict[entry.key] = entry.value ?? string.Empty;
var dict = asset == null ? null : ParseTableText(asset.text);
s_editorPreviewCache[cacheKey] = dict;
return dict;
}
/// <summary>编辑器工具:清除编辑器预览缓存(修改 JSON 文件后手动刷新时调用)。</summary>
public static void ClearEditorPreviewCache() => s_editorPreviewCache.Clear();
#endif
// ── 内部缓存查找 ──────────────────────────────────────────────────────
private bool TryGetFromTable(Language language, string table, string key, out string value)
{
var cacheKey = $"{language}/{table}";
if (!_cache.TryGetValue(cacheKey, out var dict))
var ck = new CacheKey(language, table);
if (!_cache.TryGetValue(ck, out var dict))
{
dict = LoadTable(language, table);
_cache[cacheKey] = dict; // 即使加载失败也存入空字典,避免每帧重试
_cache[ck] = dict; // 即使加载失败也存入空字典,避免每帧重试
}
if (dict != null && dict.TryGetValue(key, out value)) return true;
value = null;
@@ -201,9 +352,16 @@ namespace BaseGames.Localization
{
string path = $"Localization/{language}/{table}";
var asset = Resources.Load<TextAsset>(path);
if (asset == null) return null;
return asset == null ? null : ParseTableText(asset.text);
}
var parsed = JsonUtility.FromJson<StringTableJson>(asset.text);
/// <summary>
/// 将 JSON 文本解析为 key→value 字典(内部共享解析逻辑)。
/// 返回 null 表示格式无效。
/// </summary>
private static Dictionary<string, string> ParseTableText(string jsonText)
{
var parsed = JsonUtility.FromJson<StringTableJson>(jsonText);
if (parsed?.entries == null) return null;
var dict = new Dictionary<string, string>(parsed.entries.Count, StringComparer.Ordinal);
@@ -214,6 +372,29 @@ namespace BaseGames.Localization
return dict;
}
// ── 缓存键(值类型,消除字符串插值 GC──────────────────────────────
private readonly struct CacheKey : IEquatable<CacheKey>
{
private readonly Language _language;
private readonly string _table;
public CacheKey(Language language, string table)
{
_language = language;
_table = table;
}
public bool Equals(CacheKey other)
=> _language == other._language &&
string.Equals(_table, other._table, StringComparison.Ordinal);
public override bool Equals(object obj)
=> obj is CacheKey other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)_language, _table);
}
// ── 序列化辅助类型 ────────────────────────────────────────────────────
[Serializable]
@@ -230,4 +411,3 @@ namespace BaseGames.Localization
}
}
}

View File

@@ -0,0 +1,39 @@
namespace BaseGames.Localization
{
/// <summary>
/// 本地化表名常量。
/// 所有调用 <see cref="ILocalizationService.Get"/> 或 <see cref="LocalizationManager.Get"/> 时
/// 必须引用此类的常量,禁止直接硬编码表名字符串。
///
/// 新增表时:在此追加常量,并在 Resources/Localization/{Language}/ 下创建同名 JSON 文件。
/// </summary>
public static class LocalizationTable
{
/// <summary>通用 UI 文本按钮、标题、菜单、HUD、提示等。</summary>
public const string UI = "UI";
/// <summary>NPC 对话行与对话选项文本。</summary>
public const string Dialogue = "Dialogue";
/// <summary>任务名称与描述文本。</summary>
public const string Quest = "Quest";
/// <summary>法术名称与描述文本。</summary>
public const string Spells = "Spells";
/// <summary>角色名称NPC、玩家角色等。</summary>
public const string Character = "Character";
/// <summary>物品名称与描述(护符、收集品等)。</summary>
public const string Items = "Items";
/// <summary>技能名称与描述。</summary>
public const string Skills = "Skills";
/// <summary>教程与上下文提示文本。</summary>
public const string Tutorial = "Tutorial";
/// <summary>所有已定义的表名数组,供预热和审计遍历使用。</summary>
public static readonly string[] All = { UI, Dialogue, Quest, Spells, Character, Items, Skills, Tutorial };
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 231300f4bcb6aa34991ea6b1b145204d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,144 @@
using UnityEngine;
using TMPro;
using BaseGames.Core;
namespace BaseGames.Localization
{
/// <summary>
/// UI 文本本地化自动绑定组件。
/// 挂载在含 <see cref="TMP_Text"/> 的 GameObject 上,语言切换时自动刷新文本内容。
///
/// 用法:
/// 1. 挂上此组件,填写 <see cref="key"/> 和 <see cref="table"/>(默认 UI 表)。
/// 2. 运行时 <see cref="ILocalizationService.OnLanguageChanged"/> 触发时自动刷新。
/// 3. 格式化参数在运行时通过 <see cref="SetFormatArgs"/> 传入后即时更新显示。
/// 4. (可选)绑定 <see cref="LanguageFontConfigSO"/>,语言切换时自动替换 TMP 字体。
///
/// 编辑器预览Inspector 内实时显示当前 key 对应的本地化文本(使用简体中文表)。
/// </summary>
[RequireComponent(typeof(TMP_Text))]
[AddComponentMenu("BaseGames/Localization/Localized Text")]
public class LocalizedText : MonoBehaviour
{
[Tooltip("本地化 Key如 \"BTN_START\"、\"HUD_HP\"。")]
[SerializeField] private string _key;
[Tooltip("所属本地化表。使用 LocalizationTable 中的常量,默认 \"UI\"。")]
[SerializeField] private string _table = LocalizationTable.UI;
[Tooltip("(可选)语言→字体映射表。填写后语言切换时自动替换 TMP 字体,用于 CJK 等需要独立字体的语言。")]
[SerializeField] private LanguageFontConfigSO _fontConfig;
// 格式化参数(运行时由 SetFormatArgs 设置,空数组 = 无格式化)
private object[] _formatArgs;
private TMP_Text _label;
private ILocalizationService _svc;
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
{
_label = GetComponent<TMP_Text>();
}
private void OnEnable()
{
_svc = ServiceLocator.GetOrDefault<ILocalizationService>();
if (_svc != null)
_svc.OnLanguageChanged += OnLanguageChanged;
#if UNITY_EDITOR || DEVELOPMENT_BUILD
else
Debug.LogWarning(
$"[LocalizedText] '{name}' OnEnableILocalizationService 尚未注册," +
$"文本将不会随语言切换自动刷新。请确认 LocalizationManager 在此对象激活前已完成 Awake。", this);
#endif
Refresh();
}
private void OnDisable()
{
if (_svc != null)
_svc.OnLanguageChanged -= OnLanguageChanged;
_svc = null;
}
// ── 公开 API ──────────────────────────────────────────────────────────
/// <summary>
/// 动态更改本地化 Key 并立即刷新文本。
/// 适用于同一 UI 控件在不同状态下显示不同字段的情况。
/// </summary>
public void SetKey(string key, string table = null)
{
_key = key;
if (table != null) _table = table;
Refresh();
}
/// <summary>
/// 设置格式化参数并立即刷新文本。
/// 本地化字符串中使用标准 {0}、{1}…占位符,例如:
/// "获得 {0} 灵珠" → <c>SetFormatArgs(amount)</c>
/// </summary>
public void SetFormatArgs(params object[] args)
{
_formatArgs = args;
Refresh();
}
/// <summary>强制立即刷新文本(语言切换后由组件自动调用,通常无需手动调用)。</summary>
public void Refresh()
{
if (_label == null || string.IsNullOrEmpty(_key)) return;
ApplyFont();
_label.text = ResolveText();
}
// ── 内部 ──────────────────────────────────────────────────────────────
private void OnLanguageChanged(Language _) => Refresh();
private string ResolveText()
{
// 直接使用缓存的 _svc 实例,避免每次调用 ServiceLocator 字典查找(热路径优化)
if (_svc != null)
{
return (_formatArgs != null && _formatArgs.Length > 0)
? _svc.GetFormat(_key, _table, _formatArgs)
: _svc.Get(_key, _table);
}
// 服务未注册时使用静态方法兜底(保证不崩溃)
return (_formatArgs != null && _formatArgs.Length > 0)
? LocalizationManager.GetFormat(_key, _table, _formatArgs)
: LocalizationManager.Get(_key, _table);
}
private void ApplyFont()
{
if (_fontConfig == null || _label == null) return;
var lang = _svc?.CurrentLanguage ?? Language.ChineseSimplified;
if (!_fontConfig.TryGetFont(lang, out var font, out var mat)) return;
if (font != null) _label.font = font;
if (mat != null) _label.fontSharedMaterial = mat;
}
#if UNITY_EDITOR
// 编辑器下 key / table 变化时立即预览(无需进入 Play Mode
private void OnValidate()
{
if (!Application.isPlaying)
UpdateEditorPreview();
}
public void UpdateEditorPreview()
{
if (_label == null) _label = GetComponent<TMP_Text>();
if (_label == null || string.IsNullOrEmpty(_key)) return;
string preview = LocalizationManager.GetEditorPreview(_key, _table);
// 未找到时显示 key 本身,方便策划确认是否拼写正确
_label.text = preview ?? $"[{_key}]";
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 964e3a3ab86805244bbde47d5f54950b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: