using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Editor.Localization
{
///
/// 本地化 CSV(Excel 往返)读写共享工具。
///
/// CSV 约定:
/// - 列顺序:key, ChineseSimplified, English, Japanese, Korean(表头用 枚举名)。
/// - 以 UTF-8 BOM 写出,使 Excel 双击打开时中日韩文不乱码。
/// - 解析遵循 RFC 4180(带引号字段可含逗号 / 换行 / 双引号转义),并自动剥离前导 BOM。
/// - 导入语义:合并(覆盖已有 key、追加新 key、不删除多余 key)。
///
public static class LocalizationCsv
{
/// 导出列顺序(与导入表头解析共用)。
public static readonly Language[] Columns = LocalizationFileIO.AllLanguages;
// 带 BOM 的 UTF-8(Excel 友好)
private static readonly Encoding Utf8Bom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
// ── 高层:表 ⇄ CSV 文件 ─────────────────────────────────────────────────
///
/// 将给定的"语言→字典"导出为该表的 CSV 文件(含 BOM)。返回写出的资产路径。
///
public static string ExportToFile(string table,
IReadOnlyDictionary> langDicts)
{
// 汇总所有语言出现过的 key(排序,保证稳定 diff)
var allKeys = new SortedSet(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;
}
///
/// 读取该表的 CSV 文件并解析为"语言→字典"(仅含表头出现的语言列)。
/// CSV 文件不存在时返回 null。
///
public static Dictionary> 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));
}
///
/// 将该表的 CSV 合并写回磁盘 JSON(供批处理工具用):
/// 对 CSV 表头出现的每个语言,读现有 JSON → 合并 CSV → 。
/// 返回受影响的语言数;CSV 不存在时返回 0。
///
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;
}
// ── 低层:文本 ⇄ 行 ─────────────────────────────────────────────────────
/// 构造 CSV 文本(表头 + 数据行,按 allKeys 顺序)。
public static string BuildCsvText(IEnumerable orderedKeys,
IReadOnlyDictionary> 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();
}
/// 解析 CSV 文本为"语言→字典"(仅表头识别出的 Language 列)。
public static Dictionary> ParseText(string text)
{
var rows = ParseRows(text);
var result = new Dictionary>();
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(header[col].Trim(), out var lang))
{
langCols.Add((col, lang));
result[lang] = new Dictionary(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;
}
/// 按 RFC 4180 规范转义单元格(含逗号 / 引号 / 换行时用双引号包裹)。
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("\"", "\"\"") + "\"";
}
/// RFC 4180 CSV 解析器,支持带引号字段(含换行),并剥离前导 UTF-8 BOM。
public static List> ParseRows(string text)
{
var rows = new List>();
if (string.IsNullOrEmpty(text)) return rows;
// 剥离前导 BOM(File.ReadAllText 以 UTF8 读时若文件带 BOM 会保留为 U+FEFF)
if (text[0] == '') text = text.Substring(1);
var row = new List();
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();
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(); 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));
}
}
}