161 lines
7.9 KiB
C#
161 lines
7.9 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 本地化 JSON 表的编辑器侧磁盘读写门面(唯一入口)。
|
||
///
|
||
/// 所有编辑器工具(表格编辑器、CSV 工具、审计模块、各 Inspector)读写本地化文件
|
||
/// 都必须经此类,统一:路径(<see cref="LocalizationPaths"/>)、格式
|
||
/// (<see cref="LocalizationSerializer"/>)、Addressable 注册、编辑器缓存刷新。
|
||
///
|
||
/// 这样运行时加载与编辑器写盘永不脱节——杜绝"编辑器看着正常、Play 加载不到"的隐藏 bug。
|
||
/// </summary>
|
||
public static class LocalizationFileIO
|
||
{
|
||
/// <summary>所有受支持语言(枚举顺序)。</summary>
|
||
public static readonly Language[] AllLanguages =
|
||
(Language[])Enum.GetValues(typeof(Language));
|
||
|
||
// ── 读 ───────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 读取指定语言 + 表的 key→value 字典(返回可安全修改的副本,不存在时返回空字典)。
|
||
/// 内部走 <see cref="LocalizationManager.GetEditorTable"/> 复用其静态缓存。
|
||
/// </summary>
|
||
public static Dictionary<string, string> Read(Language language, string table)
|
||
{
|
||
var src = LocalizationManager.GetEditorTable(language, table);
|
||
return src == null
|
||
? new Dictionary<string, string>(StringComparer.Ordinal)
|
||
: new Dictionary<string, string>(src, StringComparer.Ordinal);
|
||
}
|
||
|
||
/// <summary>该表的 JSON 文件是否已存在于磁盘。</summary>
|
||
public static bool TableExists(Language language, string table)
|
||
=> AssetDatabase.LoadAssetAtPath<TextAsset>(LocalizationPaths.AssetPath(language, table)) != null;
|
||
|
||
/// <summary>扫描数据根目录,返回磁盘上实际存在子目录的语言。</summary>
|
||
public static List<Language> DiscoverLanguages()
|
||
{
|
||
var result = new List<Language>();
|
||
if (!AssetDatabase.IsValidFolder(LocalizationPaths.DataRoot)) return result;
|
||
|
||
foreach (var langFolder in AssetDatabase.GetSubFolders(LocalizationPaths.DataRoot))
|
||
{
|
||
string langName = Path.GetFileName(langFolder);
|
||
if (Enum.TryParse<Language>(langName, out var lang))
|
||
result.Add(lang);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ── 写 ───────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 将字典写回指定语言 + 表的 JSON 文件(正确的 <c>{entries:[…]}</c> 格式 + 正确目录)。
|
||
/// 新文件会自动注册到 Addressables(地址 <see cref="LocalizationPaths.Address"/>),
|
||
/// 写盘后清除编辑器预览缓存。
|
||
/// </summary>
|
||
/// <param name="language">目标语言。</param>
|
||
/// <param name="table">目标表名(<see cref="LocalizationTable"/> 常量)。</param>
|
||
/// <param name="dict">key→value 字典。</param>
|
||
/// <param name="registerAddressable">是否确保 Addressable 注册(默认 true)。</param>
|
||
public static void Write(Language language, string table,
|
||
IReadOnlyDictionary<string, string> 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 ───────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>在 Project 窗口中定位指定语言 + 表的 JSON 文件。</summary>
|
||
public static TextAsset Ping(Language language, string table)
|
||
{
|
||
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(LocalizationPaths.AssetPath(language, table));
|
||
if (asset != null) EditorScaffoldUtils.PingAndSelect(asset);
|
||
return asset;
|
||
}
|
||
|
||
/// <summary>在 Project 窗口中定位指定表的任一存在语言文件(优先简体中文 → 英文 → 其余)。</summary>
|
||
public static TextAsset PingAny(string table)
|
||
{
|
||
foreach (var lang in OrderedForPing())
|
||
{
|
||
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(LocalizationPaths.AssetPath(lang, table));
|
||
if (asset != null) { EditorScaffoldUtils.PingAndSelect(asset); return asset; }
|
||
}
|
||
Debug.LogWarning($"[LocalizationFileIO] 未找到本地化表文件:{LocalizationPaths.DataRoot}/…/{table}.json");
|
||
return null;
|
||
}
|
||
|
||
// ── 内部 ───────────────────────────────────────────────────────────────
|
||
|
||
private static IEnumerable<Language> OrderedForPing()
|
||
{
|
||
yield return Language.ChineseSimplified;
|
||
yield return Language.English;
|
||
foreach (var lang in AllLanguages)
|
||
if (lang != Language.ChineseSimplified && lang != Language.English)
|
||
yield return lang;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保该 JSON 文件已注册为对应 Addressable 地址。
|
||
/// 复用 <see cref="BaseGames.Editor.Addressables.CoreSceneRegistrar"/> 的 FindEntry-or-Create 幂等范式。
|
||
/// </summary>
|
||
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));
|
||
}
|
||
}
|
||
}
|