PackRepository.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using LocalizationTool.Models;
  7. using Newtonsoft.Json;
  8. using Newtonsoft.Json.Linq;
  9. namespace LocalizationTool.Services
  10. {
  11. /// <summary>
  12. /// 语言包仓库(JSON 格式,人类可读,与主程序 TeamAAS 共用同一份 .json 文件)。
  13. /// </summary>
  14. public static class PackRepository
  15. {
  16. /// <summary>加载目录下所有语言包;zh-CN 识别为源(IsSource)。</summary>
  17. public static List<PackFile> Load(string dir)
  18. {
  19. var packs = new List<PackFile>();
  20. foreach (var file in Directory.GetFiles(dir, "*.json"))
  21. {
  22. try
  23. {
  24. var pack = LanguagePackIO.LoadJson(file);
  25. var code = pack.LanguageCode;
  26. packs.Add(new PackFile
  27. {
  28. FilePath = file,
  29. LanguageCode = code,
  30. LanguageName = pack.LanguageName ?? code,
  31. Version = pack.Version,
  32. Author = pack.Author,
  33. IsSource = code == "zh-CN",
  34. Translations = pack.Translations ?? new Dictionary<string, string>(),
  35. });
  36. }
  37. catch { /* 解析失败的文件跳过 */ }
  38. }
  39. return packs.OrderBy(p => p.LanguageCode).ToList();
  40. }
  41. /// <summary>
  42. /// 构建全部行:Key = 应翻译的中文源文集合。
  43. /// 来源= 所有语言 translations 键的并集 ∪ zh-CN 分节结构的叶子中文值(捕获新属性)。
  44. /// </summary>
  45. public static List<RowVm> BuildRows(List<PackFile> packs, out List<PackFile> targetLangs)
  46. {
  47. var source = packs.FirstOrDefault(p => p.IsSource);
  48. targetLangs = packs.Where(p => !p.IsSource).ToList();
  49. var keys = new HashSet<string>(StringComparer.Ordinal);
  50. foreach (var p in packs)
  51. foreach (var k in p.Translations.Keys)
  52. keys.Add(k);
  53. if (source != null)
  54. foreach (var cv in EnumerateChineseValues(source))
  55. keys.Add(cv);
  56. // 排序:优先按 zh-CN 源的枚举顺序——新增词条自然排在末尾,不再"随机"插入
  57. List<string> orderedKeys;
  58. if (source != null)
  59. {
  60. orderedKeys = new List<string>();
  61. var seen = new HashSet<string>(StringComparer.Ordinal);
  62. foreach (var k in source.Translations.Keys)
  63. if (keys.Contains(k) && seen.Add(k))
  64. orderedKeys.Add(k);
  65. // 仅存在于其他语言包的 key 追加在后(字母序)
  66. foreach (var k in keys.OrderBy(k => k, StringComparer.Ordinal))
  67. if (seen.Add(k))
  68. orderedKeys.Add(k);
  69. }
  70. else
  71. {
  72. orderedKeys = keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
  73. }
  74. var rows = new List<RowVm>();
  75. int index = 1;
  76. foreach (var key in orderedKeys)
  77. {
  78. var row = new RowVm { Index = index++, Key = key };
  79. foreach (var lang in targetLangs)
  80. {
  81. lang.Translations.TryGetValue(key, out var val);
  82. row.Cells[lang.LanguageCode] = new CellVm { LangCode = lang.LanguageCode, Value = val ?? "" };
  83. }
  84. rows.Add(row);
  85. }
  86. return rows;
  87. }
  88. /// <summary>保存语言包为 .json(人类可读)。</summary>
  89. public static void Save(PackFile pack)
  90. {
  91. LanguagePackIO.SaveJson(new LanguagePack
  92. {
  93. LanguageCode = pack.LanguageCode,
  94. LanguageName = pack.LanguageName,
  95. Version = string.IsNullOrWhiteSpace(pack.Version) ? "1.0.0" : pack.Version,
  96. Author = pack.Author ?? "TeamAAS",
  97. Translations = pack.Translations
  98. }, pack.FilePath);
  99. }
  100. /// <summary>新增语言:以 zh-CN 中文源为 Key 集生成空模板(值留空)。</summary>
  101. public static bool CreateFromTemplate(string langCode, string langName, List<PackFile> packs, string dir, out PackFile result)
  102. {
  103. result = null;
  104. if (string.IsNullOrWhiteSpace(langCode)) return false;
  105. var path = Path.Combine(dir, langCode + ".json");
  106. if (File.Exists(path)) return false;
  107. var source = packs.FirstOrDefault(p => p.IsSource);
  108. var pack = new PackFile
  109. {
  110. FilePath = path,
  111. LanguageCode = langCode,
  112. LanguageName = langName,
  113. Version = "1.0.0",
  114. Author = "TeamAAS",
  115. IsSource = false,
  116. };
  117. foreach (var kv in EnumerateKeysFromSource(packs, source))
  118. pack.Translations[kv] = "";
  119. Save(pack);
  120. result = pack;
  121. return true;
  122. }
  123. /// <summary>由各语言 translations 键并集 + zh 源值生成"应翻译 Key 集"(供模板/新增语言用)。</summary>
  124. private static IEnumerable<string> EnumerateKeysFromSource(List<PackFile> packs, PackFile source)
  125. {
  126. var keys = new HashSet<string>(StringComparer.Ordinal);
  127. foreach (var p in packs)
  128. foreach (var k in p.Translations.Keys)
  129. keys.Add(k);
  130. if (source != null)
  131. foreach (var cv in EnumerateChineseValues(source))
  132. keys.Add(cv);
  133. return keys;
  134. }
  135. /// <summary>zh-CN 分节结构的所有叶子中文值(properties/plugins/pluginCategories/enums/uiTexts)。</summary>
  136. private static IEnumerable<string> EnumerateChineseValues(PackFile source)
  137. {
  138. var result = new List<string>();
  139. try
  140. {
  141. var root = JObject.Parse(File.ReadAllText(source.FilePath, Encoding.UTF8));
  142. foreach (var leaf in EnumerateLeaves(root))
  143. if (leaf.Value is JValue jv && jv.Value is string s && !string.IsNullOrWhiteSpace(s))
  144. result.Add(s);
  145. }
  146. catch { }
  147. return result;
  148. }
  149. /// <summary>校验:JSON 可解析、UTF-8、含 languageCode。</summary>
  150. public static List<string> Validate(string dir)
  151. {
  152. var errors = new List<string>();
  153. foreach (var file in Directory.GetFiles(dir, "*.json"))
  154. {
  155. var bytes = HandleBom(File.ReadAllBytes(file));
  156. try { new UTF8Encoding(false, true).GetString(bytes); }
  157. catch (Exception) { errors.Add($"{Path.GetFileName(file)}: 不是合法 UTF-8"); }
  158. try
  159. {
  160. var root = JObject.Parse(Encoding.UTF8.GetString(bytes));
  161. if (root["languageCode"] == null)
  162. errors.Add($"{Path.GetFileName(file)}: 缺少 languageCode");
  163. }
  164. catch (Exception) { errors.Add($"{Path.GetFileName(file)}: JSON 解析失败"); }
  165. }
  166. return errors;
  167. }
  168. private static byte[] HandleBom(byte[] bytes)
  169. {
  170. if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
  171. return bytes.Skip(3).ToArray();
  172. return bytes;
  173. }
  174. // ---------- 通用叶子枚举(供 zh-CN 分节结构取值) ----------
  175. public class Leaf { public string Section; public string Path; public JToken Value; public List<string> ValuePath; }
  176. public static IEnumerable<Leaf> EnumerateLeaves(JObject root)
  177. {
  178. var list = new List<Leaf>();
  179. WalkSection(root, "properties", list, 4);
  180. WalkSection(root, "plugins", list, 3);
  181. WalkSection(root, "pluginCategories", list, 2);
  182. WalkSection(root, "enums", list, 3);
  183. WalkSection(root, "uiTexts", list, 2);
  184. return list;
  185. }
  186. private static void WalkSection(JObject root, string section, List<Leaf> list, int depth)
  187. {
  188. var seg = root[section];
  189. if (seg == null) return;
  190. Collect(seg, new List<string> { section }, section, depth, list);
  191. }
  192. private static void Collect(JToken token, List<string> prefix, string section, int depth, List<Leaf> list)
  193. {
  194. if (token is JObject obj)
  195. {
  196. foreach (var prop in obj.Properties())
  197. {
  198. var next = new List<string>(prefix) { prop.Name };
  199. if (next.Count == depth && prop.Value is JValue v)
  200. list.Add(new Leaf { Section = section, Path = string.Join(".", next), Value = v, ValuePath = next });
  201. else
  202. Collect(prop.Value, next, section, depth, list);
  203. }
  204. }
  205. }
  206. }
  207. }