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,
});
}
}
}