UI系统组件

This commit is contained in:
2026-06-06 09:00:11 +08:00
parent fe4fd60083
commit d794b83ebe
107 changed files with 25690 additions and 476 deletions

View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Editor.Localization
{
/// <summary>
/// 本地化 CSVExcel 往返)读写共享工具。
///
/// CSV 约定:
/// - 列顺序:<c>key, ChineseSimplified, English, Japanese, Korean</c>(表头用 <see cref="Language"/> 枚举名)。
/// - 以 <b>UTF-8 BOM</b> 写出,使 Excel 双击打开时中日韩文不乱码。
/// - 解析遵循 RFC 4180带引号字段可含逗号 / 换行 / 双引号转义),并自动剥离前导 BOM。
/// - 导入语义:合并(覆盖已有 key、追加新 key、不删除多余 key
/// </summary>
public static class LocalizationCsv
{
/// <summary>导出列顺序(与导入表头解析共用)。</summary>
public static readonly Language[] Columns = LocalizationFileIO.AllLanguages;
// 带 BOM 的 UTF-8Excel 友好)
private static readonly Encoding Utf8Bom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
// ── 高层:表 ⇄ CSV 文件 ─────────────────────────────────────────────────
/// <summary>
/// 将给定的"语言→字典"导出为该表的 CSV 文件(含 BOM。返回写出的资产路径。
/// </summary>
public static string ExportToFile(string table,
IReadOnlyDictionary<Language, Dictionary<string, string>> langDicts)
{
// 汇总所有语言出现过的 key排序保证稳定 diff
var allKeys = new SortedSet<string>(StringComparer.Ordinal);
foreach (var lang in Columns)
if (langDicts.TryGetValue(lang, out var d) && d != null)
foreach (var k in d.Keys) allKeys.Add(k);
string csvText = BuildCsvText(allKeys, langDicts);
EditorScaffoldUtils.EnsureFolder(LocalizationPaths.ExportRoot);
string assetPath = LocalizationPaths.CsvPath(table);
File.WriteAllText(ToFullPath(assetPath), csvText, Utf8Bom);
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
return assetPath;
}
/// <summary>
/// 读取该表的 CSV 文件并解析为"语言→字典"(仅含表头出现的语言列)。
/// CSV 文件不存在时返回 null。
/// </summary>
public static Dictionary<Language, Dictionary<string, string>> ParseFile(string table)
{
string assetPath = LocalizationPaths.CsvPath(table);
string fullPath = ToFullPath(assetPath);
if (!File.Exists(fullPath)) return null;
return ParseText(File.ReadAllText(fullPath, Encoding.UTF8));
}
/// <summary>
/// 将该表的 CSV 合并写回磁盘 JSON供批处理工具用
/// 对 CSV 表头出现的每个语言,读现有 JSON → 合并 CSV → <see cref="LocalizationFileIO.Write"/>。
/// 返回受影响的语言数CSV 不存在时返回 0。
/// </summary>
public static int ImportFileToJson(string table)
{
var parsed = ParseFile(table);
if (parsed == null) return 0;
foreach (var (lang, csvDict) in parsed)
{
var merged = LocalizationFileIO.Read(lang, table); // 现有 JSON 副本
foreach (var kv in csvDict) merged[kv.Key] = kv.Value;
LocalizationFileIO.Write(lang, table, merged);
}
return parsed.Count;
}
// ── 低层:文本 ⇄ 行 ─────────────────────────────────────────────────────
/// <summary>构造 CSV 文本(表头 + 数据行,按 allKeys 顺序)。</summary>
public static string BuildCsvText(IEnumerable<string> orderedKeys,
IReadOnlyDictionary<Language, Dictionary<string, string>> langDicts)
{
var sb = new StringBuilder();
sb.Append("key");
foreach (var lang in Columns) sb.Append(',').Append(lang);
sb.Append('\n');
foreach (var key in orderedKeys)
{
sb.Append(Escape(key));
foreach (var lang in Columns)
{
string val = "";
if (langDicts.TryGetValue(lang, out var d) && d != null)
d.TryGetValue(key, out val);
sb.Append(',').Append(Escape(val ?? ""));
}
sb.Append('\n');
}
return sb.ToString();
}
/// <summary>解析 CSV 文本为"语言→字典"(仅表头识别出的 Language 列)。</summary>
public static Dictionary<Language, Dictionary<string, string>> ParseText(string text)
{
var rows = ParseRows(text);
var result = new Dictionary<Language, Dictionary<string, string>>();
if (rows.Count < 2) return result;
// 表头:列号 → Language
var header = rows[0];
var langCols = new List<(int col, Language lang)>();
for (int col = 1; col < header.Count; col++)
if (Enum.TryParse<Language>(header[col].Trim(), out var lang))
{
langCols.Add((col, lang));
result[lang] = new Dictionary<string, string>(StringComparer.Ordinal);
}
for (int row = 1; row < rows.Count; row++)
{
var cells = rows[row];
if (cells.Count == 0 || string.IsNullOrWhiteSpace(cells[0])) continue;
string key = cells[0];
foreach (var (col, lang) in langCols)
result[lang][key] = col < cells.Count ? cells[col] : "";
}
return result;
}
/// <summary>按 RFC 4180 规范转义单元格(含逗号 / 引号 / 换行时用双引号包裹)。</summary>
public static string Escape(string value)
{
if (value == null) return "";
bool needsQuote = value.IndexOf(',') >= 0 || value.IndexOf('"') >= 0
|| value.IndexOf('\n') >= 0 || value.IndexOf('\r') >= 0;
if (!needsQuote) return value;
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
/// <summary>RFC 4180 CSV 解析器,支持带引号字段(含换行),并剥离前导 UTF-8 BOM。</summary>
public static List<List<string>> ParseRows(string text)
{
var rows = new List<List<string>>();
if (string.IsNullOrEmpty(text)) return rows;
// 剥离前导 BOMFile.ReadAllText 以 UTF8 读时若文件带 BOM 会保留为 U+FEFF
if (text[0] == '') text = text.Substring(1);
var row = new List<string>();
var cell = new StringBuilder();
bool inQuote = false;
int i = 0;
while (i < text.Length)
{
char c = text[i];
if (inQuote)
{
if (c == '"')
{
if (i + 1 < text.Length && text[i + 1] == '"') { cell.Append('"'); i += 2; }
else { inQuote = false; i++; }
}
else { cell.Append(c); i++; }
}
else
{
if (c == '"') { inQuote = true; i++; }
else if (c == ',') { row.Add(cell.ToString()); cell.Clear(); i++; }
else if (c == '\r')
{
row.Add(cell.ToString()); cell.Clear();
rows.Add(row); row = new List<string>();
if (i + 1 < text.Length && text[i + 1] == '\n') i++;
i++;
}
else if (c == '\n')
{ row.Add(cell.ToString()); cell.Clear(); rows.Add(row); row = new List<string>(); i++; }
else { cell.Append(c); i++; }
}
}
if (cell.Length > 0 || row.Count > 0)
{
row.Add(cell.ToString());
rows.Add(row);
}
return rows;
}
private static string ToFullPath(string assetPath)
{
string projectRoot = Path.GetDirectoryName(Application.dataPath)!;
return Path.Combine(projectRoot, assetPath.Replace('/', Path.DirectorySeparatorChar));
}
}
}