| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using LocalizationTool.Models;
- namespace LocalizationTool.Services
- {
- /// <summary>一条译文质量问题。</summary>
- public class TranslationIssue
- {
- public RowVm Row { get; set; }
- public CellVm Cell { get; set; }
- public string LangCode { get; set; }
- /// <summary>true = 错误(如正文丢失/乱码);false = 警告(如首尾空格/疑似未翻译)。</summary>
- public bool IsError { get; set; }
- public string Message { get; set; }
- }
- /// <summary>
- /// 译文质量校验引擎:对照中文原文检查各语言译文。
- /// 错误:正文丢失(如“⏱ 延时等待”→“⏱”)、乱码字符、格式化占位符丢失。
- /// 警告:译文与原文完全相同(疑似未翻译)、首尾空白。
- /// </summary>
- public static class TranslationValidator
- {
- /// <summary>
- /// “疑似未翻译”豁免语言:这些语言的文字与中文写法相同属正常情况。
- /// 日语大量汉字词与中文写法一致(如“管理”“可能”“時間”“保存”),译文相同并非漏翻。
- /// </summary>
- public static readonly HashSet<string> NotTranslatedIgnoredLangs =
- new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "ja" };
- /// <summary>校验一组行 × 一组语言,返回问题列表(每个单元格最多一条主问题)。</summary>
- public static List<TranslationIssue> Validate(IEnumerable<RowVm> rows, IEnumerable<PackFile> langs)
- {
- var issues = new List<TranslationIssue>();
- 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;
- }
- /// <summary>
- /// 提取“实质文本”:仅保留字母与数字(汉字/假名/谚文/拉丁字母均算,emoji/符号/空白/标点全部忽略)。
- /// 用于判断译文是否只剩表情符号——“⏱ 延时等待”→“延时等待”,“⏱”→""。
- /// </summary>
- 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();
- }
- /// <summary>提取格式化占位符:{0} {name} %s %d。</summary>
- public static HashSet<string> Placeholders(string s)
- {
- var set = new HashSet<string>();
- 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;
- }
- /// <summary>是否包含汉字(CJK 统一表意文字区)。</summary>
- 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<TranslationIssue> 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,
- });
- }
- }
- }
|