472 lines
17 KiB
C#
472 lines
17 KiB
C#
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 = AibisEditorMenus.YarnLocalizationValidator;
|
|
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<YarnL10nValidationWindow>(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<DefaultAsset>(_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<YarnL10nIssue> FilterIssues(IEnumerable<YarnL10nIssue> 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<UnityEngine.Object>(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<UnityEngine.Object>(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<DefaultAsset>(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;
|
|
}
|
|
}
|
|
}
|