using System; using System.Collections.Generic; using System.Linq; using System.Text; using LocalizationTool.Models; namespace LocalizationTool.Services { /// 一条译文质量问题。 public class TranslationIssue { public RowVm Row { get; set; } public CellVm Cell { get; set; } public string LangCode { get; set; } /// true = 错误(如正文丢失/乱码);false = 警告(如首尾空格/疑似未翻译)。 public bool IsError { get; set; } public string Message { get; set; } } /// /// 译文质量校验引擎:对照中文原文检查各语言译文。 /// 错误:正文丢失(如“⏱ 延时等待”→“⏱”)、乱码字符、格式化占位符丢失。 /// 警告:译文与原文完全相同(疑似未翻译)、首尾空白。 /// public static class TranslationValidator { /// /// “疑似未翻译”豁免语言:这些语言的文字与中文写法相同属正常情况。 /// 日语大量汉字词与中文写法一致(如“管理”“可能”“時間”“保存”),译文相同并非漏翻。 /// public static readonly HashSet NotTranslatedIgnoredLangs = new HashSet(StringComparer.OrdinalIgnoreCase) { "ja" }; /// 校验一组行 × 一组语言,返回问题列表(每个单元格最多一条主问题)。 public static List Validate(IEnumerable rows, IEnumerable langs) { var issues = new List(); foreach (var lang in langs) { var isZhTarget = lang.LanguageCode.StartsWith("zh", StringComparison.OrdinalIgnoreCase); foreach (var row in rows) { if (!row.Cells.TryGetValue(lang.LanguageCode, out var cell) || cell == null) continue; var v = cell.Value; if (string.IsNullOrWhiteSpace(v)) continue; // 缺译由“缺译”列负责,这里只校验已填的译文 var key = row.Key ?? ""; // 乱码字符(错误) if (v.Contains('\uFFFD')) { Add(issues, row, cell, lang, true, "译文包含乱码字符(U+FFFD),编码可能已损坏"); continue; } if (v.Any(ch => char.IsControl(ch) && ch != '\n' && ch != '\t')) { Add(issues, row, cell, lang, true, "译文包含不可见控制字符"); continue; } // 正文丢失(错误):原文有实质文字,译文只剩 emoji/符号 var keyCore = CoreText(key); var vCore = CoreText(v); if (keyCore.Length > 0 && vCore.Length == 0) { Add(issues, row, cell, lang, true, $"译文疑似丢失正文:原文实质内容为“{keyCore}”,译文只剩符号/表情"); continue; } // 占位符丢失(错误):{0} {name} %s %d 等 var keyPh = Placeholders(key); if (keyPh.Count > 0) { var missing = keyPh.Except(Placeholders(v)).ToList(); if (missing.Count > 0) { Add(issues, row, cell, lang, true, $"译文丢失格式化占位符:{string.Join("、", missing)}"); continue; } } // 疑似未翻译(警告):译文与原文完全一致。 // 豁免语言(如日语)跳过:汉字词写法相同属正常,不标问题、不给 AI 重译 var langMain = lang.LanguageCode.Split('-')[0].ToLowerInvariant(); if (!isZhTarget && v == key && ContainsHan(key) && !NotTranslatedIgnoredLangs.Contains(langMain)) { Add(issues, row, cell, lang, false, "译文与原文完全相同,疑似未翻译"); continue; } // 首尾空白(警告) if (v != v.Trim()) { Add(issues, row, cell, lang, false, "译文首尾存在空白字符"); continue; } } } return issues; } /// /// 提取“实质文本”:仅保留字母与数字(汉字/假名/谚文/拉丁字母均算,emoji/符号/空白/标点全部忽略)。 /// 用于判断译文是否只剩表情符号——“⏱ 延时等待”→“延时等待”,“⏱”→""。 /// public static string CoreText(string s) { if (string.IsNullOrEmpty(s)) return ""; var sb = new StringBuilder(s.Length); foreach (var ch in s) if (char.IsLetterOrDigit(ch)) sb.Append(ch); return sb.ToString(); } /// 提取格式化占位符:{0} {name} %s %d。 public static HashSet Placeholders(string s) { var set = new HashSet(); if (string.IsNullOrEmpty(s)) return set; foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(s, @"\{\w+\}|%[sd]")) set.Add(m.Value); return set; } /// 是否包含汉字(CJK 统一表意文字区)。 private static bool ContainsHan(string s) { foreach (var ch in s) { var c = (int)ch; if ((c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3400 && c <= 0x4DBF)) return true; } return false; } private static void Add(List issues, RowVm row, CellVm cell, PackFile lang, bool isError, string message) { issues.Add(new TranslationIssue { Row = row, Cell = cell, LangCode = lang.LanguageCode, IsError = isError, Message = message, }); } } }