| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- 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
- {
- /// <summary>
- /// 语言包仓库(JSON 格式,人类可读,与主程序 TeamAAS 共用同一份 .json 文件)。
- /// </summary>
- public static class PackRepository
- {
- /// <summary>加载目录下所有语言包;zh-CN 识别为源(IsSource)。</summary>
- public static List<PackFile> Load(string dir)
- {
- var packs = new List<PackFile>();
- 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<string, string>(),
- });
- }
- catch { /* 解析失败的文件跳过 */ }
- }
- return packs.OrderBy(p => p.LanguageCode).ToList();
- }
- /// <summary>
- /// 构建全部行:Key = 应翻译的中文源文集合。
- /// 来源= 所有语言 translations 键的并集 ∪ zh-CN 分节结构的叶子中文值(捕获新属性)。
- /// </summary>
- public static List<RowVm> BuildRows(List<PackFile> packs, out List<PackFile> targetLangs)
- {
- var source = packs.FirstOrDefault(p => p.IsSource);
- targetLangs = packs.Where(p => !p.IsSource).ToList();
- var keys = new HashSet<string>(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<string> orderedKeys;
- if (source != null)
- {
- orderedKeys = new List<string>();
- var seen = new HashSet<string>(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<RowVm>();
- 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;
- }
- /// <summary>保存语言包为 .json(人类可读)。</summary>
- 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);
- }
- /// <summary>新增语言:以 zh-CN 中文源为 Key 集生成空模板(值留空)。</summary>
- public static bool CreateFromTemplate(string langCode, string langName, List<PackFile> 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;
- }
- /// <summary>由各语言 translations 键并集 + zh 源值生成"应翻译 Key 集"(供模板/新增语言用)。</summary>
- private static IEnumerable<string> EnumerateKeysFromSource(List<PackFile> packs, PackFile source)
- {
- var keys = new HashSet<string>(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;
- }
- /// <summary>zh-CN 分节结构的所有叶子中文值(properties/plugins/pluginCategories/enums/uiTexts)。</summary>
- private static IEnumerable<string> EnumerateChineseValues(PackFile source)
- {
- var result = new List<string>();
- 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;
- }
- /// <summary>校验:JSON 可解析、UTF-8、含 languageCode。</summary>
- public static List<string> Validate(string dir)
- {
- var errors = new List<string>();
- 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<string> ValuePath; }
- public static IEnumerable<Leaf> EnumerateLeaves(JObject root)
- {
- var list = new List<Leaf>();
- 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<Leaf> list, int depth)
- {
- var seg = root[section];
- if (seg == null) return;
- Collect(seg, new List<string> { section }, section, depth, list);
- }
- private static void Collect(JToken token, List<string> prefix, string section, int depth, List<Leaf> list)
- {
- if (token is JObject obj)
- {
- foreach (var prop in obj.Properties())
- {
- var next = new List<string>(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);
- }
- }
- }
- }
- }
|