using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEngine; using BaseGames.Localization; namespace BaseGames.Editor.Localization { /// /// 本地化 JSON 表的编辑器侧磁盘读写门面(唯一入口)。 /// /// 所有编辑器工具(表格编辑器、CSV 工具、审计模块、各 Inspector)读写本地化文件 /// 都必须经此类,统一:路径()、格式 /// ()、Addressable 注册、编辑器缓存刷新。 /// /// 这样运行时加载与编辑器写盘永不脱节——杜绝"编辑器看着正常、Play 加载不到"的隐藏 bug。 /// public static class LocalizationFileIO { /// 所有受支持语言(枚举顺序)。 public static readonly Language[] AllLanguages = (Language[])Enum.GetValues(typeof(Language)); // ── 读 ─────────────────────────────────────────────────────────────── /// /// 读取指定语言 + 表的 key→value 字典(返回可安全修改的副本,不存在时返回空字典)。 /// 内部走 复用其静态缓存。 /// public static Dictionary Read(Language language, string table) { var src = LocalizationManager.GetEditorTable(language, table); return src == null ? new Dictionary(StringComparer.Ordinal) : new Dictionary(src, StringComparer.Ordinal); } /// 该表的 JSON 文件是否已存在于磁盘。 public static bool TableExists(Language language, string table) => AssetDatabase.LoadAssetAtPath(LocalizationPaths.AssetPath(language, table)) != null; /// 扫描数据根目录,返回磁盘上实际存在子目录的语言。 public static List DiscoverLanguages() { var result = new List(); if (!AssetDatabase.IsValidFolder(LocalizationPaths.DataRoot)) return result; foreach (var langFolder in AssetDatabase.GetSubFolders(LocalizationPaths.DataRoot)) { string langName = Path.GetFileName(langFolder); if (Enum.TryParse(langName, out var lang)) result.Add(lang); } return result; } // ── 写 ─────────────────────────────────────────────────────────────── /// /// 将字典写回指定语言 + 表的 JSON 文件(正确的 {entries:[…]} 格式 + 正确目录)。 /// 新文件会自动注册到 Addressables(地址 ), /// 写盘后清除编辑器预览缓存。 /// /// 目标语言。 /// 目标表名( 常量)。 /// key→value 字典。 /// 是否确保 Addressable 注册(默认 true)。 public static void Write(Language language, string table, IReadOnlyDictionary dict, bool registerAddressable = true) { string assetPath = LocalizationPaths.AssetPath(language, table); EditorScaffoldUtils.EnsureFolder(LocalizationPaths.LanguageFolder(language)); // JSON 文件不写 BOM(Addressables/TextAsset 解析期望纯 UTF-8)。 string json = LocalizationSerializer.Serialize(dict, sortKeys: true); string fullPath = ToFullPath(assetPath); File.WriteAllText(fullPath, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate); if (registerAddressable) EnsureAddressable(language, table, assetPath); LocalizationManager.ClearEditorPreviewCache(); } // ── Ping ─────────────────────────────────────────────────────────────── /// 在 Project 窗口中定位指定语言 + 表的 JSON 文件。 public static TextAsset Ping(Language language, string table) { var asset = AssetDatabase.LoadAssetAtPath(LocalizationPaths.AssetPath(language, table)); if (asset != null) EditorScaffoldUtils.PingAndSelect(asset); return asset; } /// 在 Project 窗口中定位指定表的任一存在语言文件(优先简体中文 → 英文 → 其余)。 public static TextAsset PingAny(string table) { foreach (var lang in OrderedForPing()) { var asset = AssetDatabase.LoadAssetAtPath(LocalizationPaths.AssetPath(lang, table)); if (asset != null) { EditorScaffoldUtils.PingAndSelect(asset); return asset; } } Debug.LogWarning($"[LocalizationFileIO] 未找到本地化表文件:{LocalizationPaths.DataRoot}/…/{table}.json"); return null; } // ── 内部 ─────────────────────────────────────────────────────────────── private static IEnumerable OrderedForPing() { yield return Language.ChineseSimplified; yield return Language.English; foreach (var lang in AllLanguages) if (lang != Language.ChineseSimplified && lang != Language.English) yield return lang; } /// /// 确保该 JSON 文件已注册为对应 Addressable 地址。 /// 复用 的 FindEntry-or-Create 幂等范式。 /// private static void EnsureAddressable(Language language, string table, string assetPath) { var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) { Debug.LogWarning( $"[LocalizationFileIO] Addressable Settings 未初始化,未能注册 " + $"{LocalizationPaths.Address(language, table)}。运行时可能加载不到该表。"); return; } string guid = AssetDatabase.AssetPathToGUID(assetPath); if (string.IsNullOrEmpty(guid)) return; string address = LocalizationPaths.Address(language, table); var entry = settings.FindAssetEntry(guid) ?? settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false, false); // 已注册且地址正确则无需改动(避免无意义的 SetDirty)。 if (entry.address == address) return; entry.address = address; EditorUtility.SetDirty(settings); AssetDatabase.SaveAssets(); } private static string ToFullPath(string assetPath) { string projectRoot = Path.GetDirectoryName(Application.dataPath)!; return Path.Combine(projectRoot, assetPath.Replace('/', Path.DirectorySeparatorChar)); } } }