Files
zeling_v2/Assets/_Game/Scripts/Editor/Localization/LocalizationFileIO.cs
2026-06-06 09:00:11 +08:00

161 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 文件不写 BOMAddressables/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));
}
}
}