using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditorInternal; using UnityEngine; namespace AibisDream.YarnLocalizationValidation.Editor { public class YarnL10nValidationWindow : EditorWindow { private const string MenuPath = "AIBIS/Yarn 本地化校验"; private const string WindowTitle = "Yarn Localization Validator"; private const string FolderPathPrefKey = "AibisDream.YarnL10nValidation.FolderPath"; private const string ScanSubdirsPrefKey = "AibisDream.YarnL10nValidation.ScanSubdirs"; private string _folderPath = "Assets/Yarn/FP/FP_Day1_mid"; private DefaultAsset _folderAsset; private bool _scanSubdirectories; private Vector2 _scrollPosition; private YarnL10nValidationResult _lastResult; private string _filterType = "全部"; private string _filterLanguage = "全部"; private string _searchText = string.Empty; private string[] _typeOptions = { "全部" }; private string[] _languageOptions = { "全部" }; private int _filterTypeIndex; private int _filterLanguageIndex; [MenuItem(MenuPath)] public static void Open() { var window = GetWindow(WindowTitle); window.minSize = new Vector2(640, 480); } private void OnEnable() { _folderPath = EditorPrefs.GetString(FolderPathPrefKey, _folderPath); _scanSubdirectories = EditorPrefs.GetBool(ScanSubdirsPrefKey, false); SyncFolderAssetFromPath(); } private void OnGUI() { EditorGUILayout.LabelField("Yarn 本地化校验", EditorStyles.boldLabel); EditorGUILayout.Space(4); DrawFolderSelector(); EditorGUILayout.Space(4); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("校验", GUILayout.Height(28))) { RunValidation(); } using (new EditorGUI.DisabledScope(_lastResult == null || _lastResult.Issues.Count == 0)) { if (GUILayout.Button("导出 CSV", GUILayout.Height(28))) { ExportReport(); } } if (GUILayout.Button("定位文件夹", GUILayout.Height(28))) { PingFolder(); } EditorGUILayout.EndHorizontal(); _scanSubdirectories = EditorGUILayout.ToggleLeft("扫描子目录(批量)", _scanSubdirectories); if (GUI.changed) { EditorPrefs.SetBool(ScanSubdirsPrefKey, _scanSubdirectories); } EditorGUILayout.Space(6); DrawSummary(); DrawFilters(); DrawIssueList(); } private void DrawFolderSelector() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("章节文件夹", GUILayout.Width(80)); var newAsset = (DefaultAsset)EditorGUILayout.ObjectField(_folderAsset, typeof(DefaultAsset), false); if (newAsset != _folderAsset) { _folderAsset = newAsset; if (_folderAsset != null) { _folderPath = AssetDatabase.GetAssetPath(_folderAsset); SaveFolderPreferences(); } } var newPath = EditorGUILayout.TextField(_folderPath); if (!string.Equals(newPath, _folderPath, StringComparison.Ordinal)) { _folderPath = newPath; SyncFolderAssetFromPath(); SaveFolderPreferences(); } if (GUILayout.Button("浏览", GUILayout.Width(60))) { var startDirectory = GetBrowseStartDirectory(); var selected = EditorUtility.OpenFolderPanel("选择 Yarn 章节文件夹", startDirectory, string.Empty); if (!string.IsNullOrEmpty(selected)) { _folderPath = YarnL10nValidator.NormalizeProjectRelativePath(selected); SyncFolderAssetFromPath(); SaveFolderPreferences(); } } EditorGUILayout.EndHorizontal(); } private void SaveFolderPreferences() { EditorPrefs.SetString(FolderPathPrefKey, _folderPath); } private string GetBrowseStartDirectory() { var absolutePath = YarnL10nValidator.ToAbsolutePath(_folderPath); if (!string.IsNullOrEmpty(absolutePath) && !IsFilesystemRoot(absolutePath)) { var parent = Directory.GetParent(absolutePath); if (parent != null) { return parent.FullName; } } if (!string.IsNullOrEmpty(absolutePath) && Directory.Exists(absolutePath)) { return absolutePath; } var assetsPath = Application.dataPath; return Directory.Exists(assetsPath) ? assetsPath : Environment.CurrentDirectory; } private static bool IsFilesystemRoot(string absolutePath) { var normalized = Path.GetFullPath(absolutePath) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var root = Path.GetPathRoot(normalized)? .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); return !string.IsNullOrEmpty(root) && string.Equals(normalized, root, StringComparison.OrdinalIgnoreCase); } private void SyncFolderAssetFromPath() { _folderAsset = AssetDatabase.LoadAssetAtPath(_folderPath); } private void RunValidation() { SaveFolderPreferences(); _lastResult = _scanSubdirectories ? YarnL10nValidator.ValidateScanAll(_folderPath) : YarnL10nValidator.ValidateFolder(_folderPath); RefreshFilterOptions(); Repaint(); } private void RefreshFilterOptions() { if (_lastResult == null || _lastResult.Issues.Count == 0) { _typeOptions = new[] { "全部" }; _languageOptions = new[] { "全部" }; } else { var types = _lastResult.Issues .Select(issue => issue.IssueTypeLabel) .Where(label => !string.IsNullOrEmpty(label)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(label => label, StringComparer.OrdinalIgnoreCase) .ToList(); types.Insert(0, "全部"); _typeOptions = types.ToArray(); var languages = _lastResult.Issues .Select(issue => issue.Language) .Where(language => !string.IsNullOrEmpty(language)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(language => language, StringComparer.OrdinalIgnoreCase) .ToList(); languages.Insert(0, "全部"); _languageOptions = languages.ToArray(); } _filterTypeIndex = ClampFilterIndex(_filterType, _typeOptions, ref _filterType); _filterLanguageIndex = ClampFilterIndex(_filterLanguage, _languageOptions, ref _filterLanguage); } private static int ClampFilterIndex(string currentValue, string[] options, ref string selectedValue) { if (options.Length == 0) { selectedValue = "全部"; return 0; } for (var i = 0; i < options.Length; i++) { if (string.Equals(options[i], currentValue, StringComparison.OrdinalIgnoreCase)) { selectedValue = options[i]; return i; } } selectedValue = options[0]; return 0; } private void ExportReport() { var defaultName = YarnL10nReportExporter.BuildDefaultExportFileName(_folderPath); var savePath = EditorUtility.SaveFilePanel("导出校验报告", Application.dataPath, defaultName, "csv"); if (string.IsNullOrEmpty(savePath)) { return; } YarnL10nReportExporter.ExportToCsv(_lastResult, savePath); EditorUtility.RevealInFinder(savePath); } private void PingFolder() { SyncFolderAssetFromPath(); if (_folderAsset == null) { EditorUtility.DisplayDialog("Yarn 本地化校验", "无法定位文件夹,请检查路径。", "确定"); return; } EditorGUIUtility.PingObject(_folderAsset); Selection.activeObject = _folderAsset; } private void DrawSummary() { EditorGUILayout.LabelField("摘要", EditorStyles.boldLabel); using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { if (_lastResult == null) { EditorGUILayout.LabelField("尚未执行校验。"); return; } EditorGUILayout.LabelField($"Issue 总数: {_lastResult.TotalIssueCount}"); EditorGUILayout.LabelField($"章节数: {_lastResult.Chapters.Count}"); foreach (var chapter in _lastResult.Chapters) { var localeParts = chapter.Locales.Values .Select(locale => $"{locale.Language}: 缺 {locale.MissingCount} / 多余 {locale.OrphanCount} / 变更 {locale.SourceChangedCount}") .ToArray(); var localeSummary = localeParts.Length > 0 ? string.Join(" | ", localeParts) : "无翻译语言"; EditorGUILayout.LabelField($"{chapter.ChapterFolder} — 原文 {chapter.SourceLineCount} 行 | {localeSummary}"); } } EditorGUILayout.Space(6); } private void DrawFilters() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("类型", GUILayout.Width(40)); _filterTypeIndex = EditorGUILayout.Popup(_filterTypeIndex, _typeOptions, GUILayout.Width(180)); _filterType = _typeOptions[_filterTypeIndex]; EditorGUILayout.LabelField("语言", GUILayout.Width(40)); _filterLanguageIndex = EditorGUILayout.Popup(_filterLanguageIndex, _languageOptions, GUILayout.Width(100)); _filterLanguage = _languageOptions[_filterLanguageIndex]; EditorGUILayout.LabelField("搜索", GUILayout.Width(40)); _searchText = EditorGUILayout.TextField(_searchText); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); } private void DrawIssueList() { EditorGUILayout.LabelField("结果列表", EditorStyles.boldLabel); _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.ExpandHeight(true)); if (_lastResult == null || _lastResult.Issues.Count == 0) { EditorGUILayout.LabelField(_lastResult == null ? "无结果。" : "未发现 issue。"); } else { var index = 0; foreach (var issue in FilterIssues(_lastResult.Issues)) { DrawIssueRow(issue, index); index++; } } EditorGUILayout.EndScrollView(); } private IEnumerable FilterIssues(IEnumerable issues) { foreach (var issue in issues) { if (!MatchesFilter(issue)) { continue; } yield return issue; } } private bool MatchesFilter(YarnL10nIssue issue) { if (!string.IsNullOrWhiteSpace(_filterType) && !string.Equals(_filterType, "全部", StringComparison.OrdinalIgnoreCase) && !string.Equals(issue.IssueTypeLabel, _filterType, StringComparison.OrdinalIgnoreCase)) { return false; } if (!string.IsNullOrWhiteSpace(_filterLanguage) && !string.Equals(_filterLanguage, "全部", StringComparison.OrdinalIgnoreCase) && !string.Equals(issue.Language, _filterLanguage, StringComparison.OrdinalIgnoreCase)) { return false; } if (string.IsNullOrWhiteSpace(_searchText)) { return true; } var haystack = string.Join(" ", issue.GetDisplayType(), issue.Language, issue.LineId, issue.SourceText, issue.LocText, issue.YarnFile, issue.ChapterFolder, issue.Message); return haystack.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0; } private void DrawIssueRow(YarnL10nIssue issue, int index) { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField($"{issue.GetDisplayType()} | {issue.Language} | {issue.LineId}", EditorStyles.boldLabel); if (!string.IsNullOrEmpty(issue.ChapterFolder)) { EditorGUILayout.LabelField("章节", issue.ChapterFolder); } if (!string.IsNullOrEmpty(issue.YarnFile)) { EditorGUILayout.LabelField("源文件", $"{issue.YarnFile}:{issue.YarnLineNumber}"); } if (!string.IsNullOrEmpty(issue.CsvFile)) { EditorGUILayout.LabelField("CSV", issue.CsvFile); } if (!string.IsNullOrEmpty(issue.SourceText)) { EditorGUILayout.LabelField("原文", issue.SourceText); } if (!string.IsNullOrEmpty(issue.LocText)) { EditorGUILayout.LabelField("翻译", issue.LocText); } if (!string.IsNullOrEmpty(issue.Message)) { EditorGUILayout.LabelField("说明", issue.Message); } EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("定位", GUILayout.Width(80))) { PingIssue(issue); } EditorGUILayout.EndHorizontal(); } } private void PingIssue(YarnL10nIssue issue) { if (issue.Type == YarnL10nIssueType.OrphanLocalization && !string.IsNullOrEmpty(issue.CsvFile)) { var csvAsset = AssetDatabase.LoadAssetAtPath(issue.CsvFile); if (csvAsset != null) { EditorGUIUtility.PingObject(csvAsset); Selection.activeObject = csvAsset; return; } } if (!string.IsNullOrEmpty(issue.YarnFile)) { var yarnPath = ResolveYarnAssetPath(issue); var yarnAsset = AssetDatabase.LoadAssetAtPath(yarnPath); if (yarnAsset != null) { EditorGUIUtility.PingObject(yarnAsset); Selection.activeObject = yarnAsset; if (int.TryParse(issue.YarnLineNumber, out var lineNumber) && lineNumber > 0) { var absolutePath = YarnL10nValidator.ToAbsolutePath(yarnPath); InternalEditorUtility.OpenFileAtLineExternal(absolutePath, lineNumber); } return; } } if (!string.IsNullOrEmpty(issue.ChapterFolder)) { var folderAsset = AssetDatabase.LoadAssetAtPath(issue.ChapterFolder); if (folderAsset != null) { EditorGUIUtility.PingObject(folderAsset); Selection.activeObject = folderAsset; } } } private static string ResolveYarnAssetPath(YarnL10nIssue issue) { if (issue.YarnFile.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) { return issue.YarnFile; } if (!string.IsNullOrEmpty(issue.ChapterFolder)) { return $"{issue.ChapterFolder}/{issue.YarnFile}"; } return issue.YarnFile; } } }