using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using LocalizationTool.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace LocalizationTool.Services { /// /// 语言包仓库(JSON 格式,人类可读,与主程序 TeamAAS 共用同一份 .json 文件)。 /// public static class PackRepository { /// 加载目录下所有语言包;zh-CN 识别为源(IsSource)。 public static List Load(string dir) { var packs = new List(); foreach (var file in Directory.GetFiles(dir, "*.json")) { try { var pack = LanguagePackIO.LoadJson(file); var code = pack.LanguageCode; packs.Add(new PackFile { FilePath = file, LanguageCode = code, LanguageName = pack.LanguageName ?? code, Version = pack.Version, Author = pack.Author, IsSource = code == "zh-CN", Translations = pack.Translations ?? new Dictionary(), }); } catch { /* 解析失败的文件跳过 */ } } return packs.OrderBy(p => p.LanguageCode).ToList(); } /// /// 构建全部行:Key = 应翻译的中文源文集合。 /// 来源= 所有语言 translations 键的并集 ∪ zh-CN 分节结构的叶子中文值(捕获新属性)。 /// public static List BuildRows(List packs, out List targetLangs) { var source = packs.FirstOrDefault(p => p.IsSource); targetLangs = packs.Where(p => !p.IsSource).ToList(); var keys = new HashSet(StringComparer.Ordinal); foreach (var p in packs) foreach (var k in p.Translations.Keys) keys.Add(k); if (source != null) foreach (var cv in EnumerateChineseValues(source)) keys.Add(cv); // 排序:优先按 zh-CN 源的枚举顺序——新增词条自然排在末尾,不再"随机"插入 List orderedKeys; if (source != null) { orderedKeys = new List(); var seen = new HashSet(StringComparer.Ordinal); foreach (var k in source.Translations.Keys) if (keys.Contains(k) && seen.Add(k)) orderedKeys.Add(k); // 仅存在于其他语言包的 key 追加在后(字母序) foreach (var k in keys.OrderBy(k => k, StringComparer.Ordinal)) if (seen.Add(k)) orderedKeys.Add(k); } else { orderedKeys = keys.OrderBy(k => k, StringComparer.Ordinal).ToList(); } var rows = new List(); int index = 1; foreach (var key in orderedKeys) { var row = new RowVm { Index = index++, Key = key }; foreach (var lang in targetLangs) { lang.Translations.TryGetValue(key, out var val); row.Cells[lang.LanguageCode] = new CellVm { LangCode = lang.LanguageCode, Value = val ?? "" }; } rows.Add(row); } return rows; } /// 保存语言包为 .json(人类可读)。 public static void Save(PackFile pack) { LanguagePackIO.SaveJson(new LanguagePack { LanguageCode = pack.LanguageCode, LanguageName = pack.LanguageName, Version = string.IsNullOrWhiteSpace(pack.Version) ? "1.0.0" : pack.Version, Author = pack.Author ?? "TeamAAS", Translations = pack.Translations }, pack.FilePath); } /// 新增语言:以 zh-CN 中文源为 Key 集生成空模板(值留空)。 public static bool CreateFromTemplate(string langCode, string langName, List packs, string dir, out PackFile result) { result = null; if (string.IsNullOrWhiteSpace(langCode)) return false; var path = Path.Combine(dir, langCode + ".json"); if (File.Exists(path)) return false; var source = packs.FirstOrDefault(p => p.IsSource); var pack = new PackFile { FilePath = path, LanguageCode = langCode, LanguageName = langName, Version = "1.0.0", Author = "TeamAAS", IsSource = false, }; foreach (var kv in EnumerateKeysFromSource(packs, source)) pack.Translations[kv] = ""; Save(pack); result = pack; return true; } /// 由各语言 translations 键并集 + zh 源值生成"应翻译 Key 集"(供模板/新增语言用)。 private static IEnumerable EnumerateKeysFromSource(List packs, PackFile source) { var keys = new HashSet(StringComparer.Ordinal); foreach (var p in packs) foreach (var k in p.Translations.Keys) keys.Add(k); if (source != null) foreach (var cv in EnumerateChineseValues(source)) keys.Add(cv); return keys; } /// zh-CN 分节结构的所有叶子中文值(properties/plugins/pluginCategories/enums/uiTexts)。 private static IEnumerable EnumerateChineseValues(PackFile source) { var result = new List(); try { var root = JObject.Parse(File.ReadAllText(source.FilePath, Encoding.UTF8)); foreach (var leaf in EnumerateLeaves(root)) if (leaf.Value is JValue jv && jv.Value is string s && !string.IsNullOrWhiteSpace(s)) result.Add(s); } catch { } return result; } /// 校验:JSON 可解析、UTF-8、含 languageCode。 public static List Validate(string dir) { var errors = new List(); foreach (var file in Directory.GetFiles(dir, "*.json")) { var bytes = HandleBom(File.ReadAllBytes(file)); try { new UTF8Encoding(false, true).GetString(bytes); } catch (Exception) { errors.Add($"{Path.GetFileName(file)}: 不是合法 UTF-8"); } try { var root = JObject.Parse(Encoding.UTF8.GetString(bytes)); if (root["languageCode"] == null) errors.Add($"{Path.GetFileName(file)}: 缺少 languageCode"); } catch (Exception) { errors.Add($"{Path.GetFileName(file)}: JSON 解析失败"); } } return errors; } private static byte[] HandleBom(byte[] bytes) { if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) return bytes.Skip(3).ToArray(); return bytes; } // ---------- 通用叶子枚举(供 zh-CN 分节结构取值) ---------- public class Leaf { public string Section; public string Path; public JToken Value; public List ValuePath; } public static IEnumerable EnumerateLeaves(JObject root) { var list = new List(); WalkSection(root, "properties", list, 4); WalkSection(root, "plugins", list, 3); WalkSection(root, "pluginCategories", list, 2); WalkSection(root, "enums", list, 3); WalkSection(root, "uiTexts", list, 2); return list; } private static void WalkSection(JObject root, string section, List list, int depth) { var seg = root[section]; if (seg == null) return; Collect(seg, new List { section }, section, depth, list); } private static void Collect(JToken token, List prefix, string section, int depth, List list) { if (token is JObject obj) { foreach (var prop in obj.Properties()) { var next = new List(prefix) { prop.Name }; if (next.Count == depth && prop.Value is JValue v) list.Add(new Leaf { Section = section, Path = string.Join(".", next), Value = v, ValuePath = next }); else Collect(prop.Value, next, section, depth, list); } } } } }