From b407402eed317b744b183e981f1ac3d635b287a4 Mon Sep 17 00:00:00 2001 From: bd_ Date: Thu, 21 Dec 2023 15:53:42 +0900 Subject: [PATCH] chore(error): API and docs refinements --- Editor/API/BuildContext.cs | 2 +- Editor/ErrorReporting/ErrorContainer.cs | 3 + Editor/ErrorReporting/ErrorReport.cs | 69 +++++++++++-- .../{ErrorCategory.cs => ErrorSeverity.cs} | 5 +- ...Category.cs.meta => ErrorSeverity.cs.meta} | 0 Editor/ErrorReporting/IError.cs | 21 +++- Editor/ErrorReporting/InlineError.cs | 8 +- Editor/ErrorReporting/SimpleError.cs | 48 ++++++++- Editor/ErrorReporting/StackTraceError.cs | 4 +- Editor/ErrorReporting/UI/ErrorIcon.cs | 23 +++-- Editor/ErrorReporting/UI/ErrorReportWindow.cs | 55 +++++------ Editor/ErrorReporting/UI/ObjectSelector.cs | 10 ++ Editor/ErrorReporting/UI/SimpleErrorUI.cs | 2 +- Editor/UI/LanguageSwitcher.cs | 6 ++ Editor/UI/Localization/LanguagePrefs.cs | 24 ++++- Editor/UI/Localization/Localizer.cs | 93 +++++++++--------- Editor/UI/Localization/NDMFLocales.cs | 20 ++-- Editor/UI/Localized.cs | 98 ------------------- Editor/UI/Localized.cs.meta | 3 - 19 files changed, 278 insertions(+), 216 deletions(-) rename Editor/ErrorReporting/{ErrorCategory.cs => ErrorSeverity.cs} (86%) rename Editor/ErrorReporting/{ErrorCategory.cs.meta => ErrorSeverity.cs.meta} (100%) delete mode 100644 Editor/UI/Localized.cs delete mode 100644 Editor/UI/Localized.cs.meta diff --git a/Editor/API/BuildContext.cs b/Editor/API/BuildContext.cs index 9ac028b..707a84a 100644 --- a/Editor/API/BuildContext.cs +++ b/Editor/API/BuildContext.cs @@ -73,7 +73,7 @@ public sealed partial class BuildContext /// public UnityObject AssetContainer { get; private set; } - public bool Successful => !_report.Errors.Any(e => e.TheError.Category >= ErrorCategory.Error); + public bool Successful => !_report.Errors.Any(e => e.TheError.Severity >= ErrorSeverity.Error); private Dictionary _state = new Dictionary(); private Dictionary _extensions = new Dictionary(); diff --git a/Editor/ErrorReporting/ErrorContainer.cs b/Editor/ErrorReporting/ErrorContainer.cs index aee52c0..8ffe872 100644 --- a/Editor/ErrorReporting/ErrorContainer.cs +++ b/Editor/ErrorReporting/ErrorContainer.cs @@ -2,6 +2,9 @@ namespace nadena.dev.ndmf { + /// + /// Holds a single error and information about where the error originated from. + /// public struct ErrorContext { public IError TheError; diff --git a/Editor/ErrorReporting/ErrorReport.cs b/Editor/ErrorReporting/ErrorReport.cs index 1de9142..daa3daa 100644 --- a/Editor/ErrorReporting/ErrorReport.cs +++ b/Editor/ErrorReporting/ErrorReport.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Linq; using System.Runtime.ExceptionServices; +using JetBrains.Annotations; using nadena.dev.ndmf.localization; using nadena.dev.ndmf.runtime; using UnityEngine; @@ -78,6 +79,9 @@ public void Dispose() } } + /// + /// Contains any errors or warnings issued during a single build operation. + /// public sealed class ErrorReport { internal static List Reports = new List(); @@ -93,9 +97,18 @@ private ErrorReport(string avatarName, string avatarPath) Errors = ImmutableList.Empty; } + /// + /// The name of the avatar being processed + /// public string AvatarName { get; } + /// + /// The path (from the scene root) of the avatar being processed + /// public string AvatarRootPath { get; } + /// + /// A list of reported errors. + /// public ImmutableList Errors { get; private set; } internal ErrorContext CurrentContext = new ErrorContext(); @@ -128,7 +141,7 @@ internal static ErrorReport Create(GameObject root, bool isClone) return report; } - public void AddError(IError error) + internal void AddError(IError error) { var context = CurrentContext; context.TheError = error; @@ -136,6 +149,11 @@ public void AddError(IError error) Errors = Errors.Add(context); } + /// + /// Adds an error to the currently active error report. If no error report is active, the error will simply be + /// logged to the debug log. + /// + /// public static void ReportError(IError error) { if (error is StackTraceError e) @@ -157,12 +175,24 @@ public static void ReportError(IError error) CurrentReport?.AddError(error); } - public static void ReportError(Localizer localizer, ErrorCategory errorCategory, string key, + /// + /// Helper to report a SimpleError. + /// + /// The Localizer used to look up translations + /// The severity of the error + /// The prefix used to find localization keys + /// Inline substitutions and unity objects to reference from the error + public static void ReportError(Localizer localizer, ErrorSeverity errorSeverity, string key, params object[] args) { - ReportError(new InlineError(localizer, errorCategory, key, args)); + ReportError(new InlineError(localizer, errorSeverity, key, args)); } + /// + /// Helper to report an exception. This will generate an error of InternalError severity. + /// + /// Exception to report + /// Additional information to append to the stack trace public static void ReportException(Exception e, string additionalStackTrace = null) { var report = CurrentReport; @@ -178,6 +208,11 @@ public static void ReportException(Exception e, string additionalStackTrace = nu report?.ReportedExceptions?.Add(e); } + /// + /// Attempts to find the original avatar that generated the report. + /// + /// The avatar root + /// true if the avatar was found, otherwise false public bool TryResolveAvatar(out GameObject av) { var scene = SceneManager.GetActiveScene(); @@ -206,7 +241,12 @@ public bool TryResolveAvatar(out GameObject av) return false; } - public static IDisposable WithContextObject(UnityObject obj) + /// + /// Returns a disposable scope, within which all errors will reference a specific UnityObject. + /// + /// The object to reference (can be null) + /// A disposable that will remove the object from the current scope when disposed. + public static IDisposable WithContextObject([CanBeNull] UnityObject obj) { if (obj == null || CurrentReport == null) return new NullScope(); @@ -216,7 +256,14 @@ public static IDisposable WithContextObject(UnityObject obj) return scope; } - public static T WithContextObject(UnityObject obj, Func func) + /// + /// Executes a function, within which any errors will reference a specific UnityObject. + /// Thrown exceptions will automatically be logged. + /// + /// The object to reference + /// The function to invoke + /// The return value of func() + public static T WithContextObject([CanBeNull] UnityObject obj, Func func) { using (WithContextObject(obj)) { @@ -232,7 +279,14 @@ public static T WithContextObject(UnityObject obj, Func func) } } - public static void WithContextObject(UnityObject obj, Action action) + /// + /// Executes a function, within which any errors will reference a specific UnityObject. + /// Thrown exceptions will automatically be logged. + /// + /// The object to reference + /// The function to invoke + /// The return value of func() + public static void WithContextObject([CanBeNull] UnityObject obj, Action action) { using (WithContextObject(obj)) { @@ -294,6 +348,9 @@ public static List CaptureErrors(Action action) return report.Errors.ToList(); } + /// + /// Clears all error reports. + /// public static void Clear() { Reports.Clear(); diff --git a/Editor/ErrorReporting/ErrorCategory.cs b/Editor/ErrorReporting/ErrorSeverity.cs similarity index 86% rename from Editor/ErrorReporting/ErrorCategory.cs rename to Editor/ErrorReporting/ErrorSeverity.cs index 29422c4..3c8b9fa 100644 --- a/Editor/ErrorReporting/ErrorCategory.cs +++ b/Editor/ErrorReporting/ErrorSeverity.cs @@ -1,6 +1,9 @@ namespace nadena.dev.ndmf { - public enum ErrorCategory + /// + /// Indicates the severity of a reported error. + /// + public enum ErrorSeverity { /// /// Informational messages that indicate an unusual configuration that might be an error. diff --git a/Editor/ErrorReporting/ErrorCategory.cs.meta b/Editor/ErrorReporting/ErrorSeverity.cs.meta similarity index 100% rename from Editor/ErrorReporting/ErrorCategory.cs.meta rename to Editor/ErrorReporting/ErrorSeverity.cs.meta diff --git a/Editor/ErrorReporting/IError.cs b/Editor/ErrorReporting/IError.cs index 10c4dec..f1a0a48 100644 --- a/Editor/ErrorReporting/IError.cs +++ b/Editor/ErrorReporting/IError.cs @@ -6,11 +6,30 @@ namespace nadena.dev.ndmf { + /// + /// A base interface for custom NDMF error reports. + /// public interface IError { - ErrorCategory Category { get; } + /// + /// The severity of the error + /// + ErrorSeverity Severity { get; } + /// + /// Creates a VisualElement used to display the error in the error report window. + /// + /// The report this error is in + /// A VisualElement to display VisualElement CreateVisualElement(ErrorReport report); + /// + /// Formats the error as a string, suitable for being dumped to the unity log. + /// + /// string ToMessage(); + /// + /// Adds a reference to a context object that might be helpful for tracking down the error. + /// + /// void AddReference(ObjectReference obj); } } \ No newline at end of file diff --git a/Editor/ErrorReporting/InlineError.cs b/Editor/ErrorReporting/InlineError.cs index a6c11f6..8ba6d2b 100644 --- a/Editor/ErrorReporting/InlineError.cs +++ b/Editor/ErrorReporting/InlineError.cs @@ -5,14 +5,14 @@ namespace nadena.dev.ndmf { - public class InlineError : SimpleError + internal class InlineError : SimpleError { private readonly string[] _subst; - public InlineError(Localizer localizer, ErrorCategory errorCategory, string key, params object[] args) + public InlineError(Localizer localizer, ErrorSeverity errorSeverity, string key, params object[] args) { Localizer = localizer; - Category = errorCategory; + Severity = errorSeverity; TitleKey = key; _subst = Array.ConvertAll(args, o => o?.ToString()); @@ -34,7 +34,7 @@ public InlineError(Localizer localizer, ErrorCategory errorCategory, string key, } protected override Localizer Localizer { get; } - public override ErrorCategory Category { get; } + public override ErrorSeverity Severity { get; } protected override string TitleKey { get; } protected override string[] DetailsSubst => _subst; diff --git a/Editor/ErrorReporting/SimpleError.cs b/Editor/ErrorReporting/SimpleError.cs index a8b9d9d..3b3831c 100644 --- a/Editor/ErrorReporting/SimpleError.cs +++ b/Editor/ErrorReporting/SimpleError.cs @@ -9,22 +9,56 @@ namespace nadena.dev.ndmf { + /// + /// Base class for errors that obtain their messages from the Localization system. + /// public abstract class SimpleError : IError { private static readonly Regex Pattern = new Regex("\\{([0-9])\\}"); + /// + /// The Localizer to use to look up strings. + /// protected abstract Localizer Localizer { get; } + /// + /// The key to use for the title of the error. By default, all other keys are derived from this TitleKey. + /// protected abstract string TitleKey { get; } + /// + /// The key to use for the details section of the error display. By default, this is the TitleKey + `:description`. + /// protected virtual string DetailsKey => TitleKey + ":description"; + /// + /// String substitutions to insert into the details section of the error display. You can reference these with + /// e.g. `{0}`. + /// protected virtual string[] DetailsSubst => Array.Empty(); + /// + /// The key to use for the hint section of the error display. By default, this is the TitleKey + `:hint`. + /// This section should be used to provide a hint to the user about how to resolve the error. + /// protected virtual string HintKey => TitleKey + ":hint"; + /// + /// String substitutions to insert into the hint section of the error display. You can reference these with + /// e.g. `{0}`. + /// protected virtual string[] HintSubst => Array.Empty(); + /// + /// Any ObjectReferences to display to the user; the user will be able to click to jump to these objects. + /// protected List _references = new List(); + /// + /// Any ObjectReferences to display to the user; the user will be able to click to jump to these objects. + /// By default this just returns the `_references` protected field. + /// public virtual ObjectReference[] References => _references.ToArray(); - public abstract ErrorCategory Category { get; } + /// + /// The severity of the error. + /// + public abstract ErrorSeverity Severity { get; } public virtual VisualElement CreateVisualElement(ErrorReport report) { @@ -44,16 +78,28 @@ public virtual string ToMessage() return title; } + /// + /// Returns the formatted title of the error. + /// + /// public string FormatTitle() { return Localizer.GetLocalizedString(TitleKey); } + /// + /// Returns the formatted details message for the error. + /// + /// public string FormatDetails() { return SafeSubst(DetailsKey, DetailsSubst); } + /// + /// Returns the formatted hint message for the error. + /// + /// public string FormatHint() { return SafeSubst(HintKey, HintSubst); diff --git a/Editor/ErrorReporting/StackTraceError.cs b/Editor/ErrorReporting/StackTraceError.cs index f2466f9..9851d17 100644 --- a/Editor/ErrorReporting/StackTraceError.cs +++ b/Editor/ErrorReporting/StackTraceError.cs @@ -6,7 +6,7 @@ namespace nadena.dev.ndmf { - public class StackTraceError : SimpleError + internal class StackTraceError : SimpleError { private Exception _e; private string _stackTrace; @@ -27,7 +27,7 @@ public StackTraceError(Exception e, string additionalStackTrace = null) protected override Localizer Localizer => NDMFLocales.L; protected override string TitleKey => "Errors:InternalError"; - public override ErrorCategory Category => ErrorCategory.InternalError; + public override ErrorSeverity Severity => ErrorSeverity.InternalError; protected override string[] DetailsSubst => new [] { diff --git a/Editor/ErrorReporting/UI/ErrorIcon.cs b/Editor/ErrorReporting/UI/ErrorIcon.cs index dd2f600..12d3fa8 100644 --- a/Editor/ErrorReporting/UI/ErrorIcon.cs +++ b/Editor/ErrorReporting/UI/ErrorIcon.cs @@ -10,9 +10,12 @@ namespace nadena.dev.ndmf.ui { + /// + /// Displays a severity icon for a particular ErrorLevel. + /// public sealed class ErrorIcon : VisualElement { - private ErrorCategory _category; + private ErrorSeverity _severity; private Image _image; @@ -22,12 +25,12 @@ public ErrorIcon() Add(_image); } - public ErrorCategory Category + public ErrorSeverity Severity { - get => _category; + get => _severity; set { - _category = value; + _severity = value; UpdateIcon(); } } @@ -36,12 +39,12 @@ private void UpdateIcon() { Texture2D tex; - switch (_category) + switch (_severity) { - case ErrorCategory.Information: + case ErrorSeverity.Information: tex = EditorGUIUtility.FindTexture("d_console.infoicon"); break; - case ErrorCategory.NonFatal: + case ErrorSeverity.NonFatal: tex = EditorGUIUtility.FindTexture("d_console.warnicon"); break; default: @@ -73,12 +76,12 @@ public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext var categoryStr = m_Category.GetValueFromBag(bag, cc); - if (categoryStr == null || !Enum.TryParse(categoryStr, out var categoryVal)) + if (categoryStr == null || !Enum.TryParse(categoryStr, out var categoryVal)) { - categoryVal = ErrorCategory.Error; + categoryVal = ErrorSeverity.Error; } - elem.Category = categoryVal; + elem.Severity = categoryVal; } } } diff --git a/Editor/ErrorReporting/UI/ErrorReportWindow.cs b/Editor/ErrorReporting/UI/ErrorReportWindow.cs index db710c9..ab70ff3 100644 --- a/Editor/ErrorReporting/UI/ErrorReportWindow.cs +++ b/Editor/ErrorReporting/UI/ErrorReportWindow.cs @@ -38,6 +38,11 @@ public class ErrorReportWindow : EditorWindow private ToolbarMenu _selector; #endif + /// + /// Gets or sets the error report currently being displayed. May be null if no error report has been generated + /// yet. Setting this will update CurrentAvatar, pointing to the avatar that originated the error report (or + /// null if it can't be found). + /// public ErrorReport CurrentReport { get => _report; @@ -57,6 +62,11 @@ public ErrorReport CurrentReport } } + /// + /// Gets or sets the avatar corresponding to the error report being displayed. On set, the window will search + /// for a corresponding error report; if not found, a UI offering to run a test build will be shown instead of + /// the error contents. + /// public GameObject CurrentAvatar { get => _avatarRoot; @@ -81,6 +91,7 @@ public GameObject CurrentAvatar } } + [ExcludeFromDocs] public void CreateGUI() { // Each editor window contains a root VisualElement object @@ -169,6 +180,9 @@ private void SetupSelector() #endif } + /// + /// Shows the error report window, displaying the last error report generated. + /// [MenuItem("Tools/NDM Framework/Show Error Report")] public static void ShowErrorReportWindow() { @@ -198,7 +212,7 @@ private void TestBuild() } } - void UpdateContents() + private void UpdateContents() { _errorList.Clear(); @@ -252,30 +266,10 @@ void UpdateContents() SetupSelector(); } - private GameObject FindAvatarRoot(ErrorReport report) - { - var path = report.AvatarRootPath; - var firstSegmentIndex = path.IndexOf('/'); - var firstSegment = firstSegmentIndex > 0 ? path.Substring(0, firstSegmentIndex) : path; - - foreach (var sceneRoot in SceneManager.GetActiveScene().GetRootGameObjects()) - { - if (sceneRoot.name == firstSegment) - { - if (firstSegmentIndex > 0) - { - return sceneRoot.transform.Find(path.Substring(firstSegmentIndex + 1))?.gameObject; - } - else - { - return sceneRoot; - } - } - } - - return null; - } - + /// + /// Shows the error report window, displaying a specific error report. + /// + /// public static void ShowReport(ErrorReport report) { if (Application.isBatchMode) return; // headless unit tests @@ -286,6 +280,10 @@ public static void ShowReport(ErrorReport report) wnd.Show(); } + /// + /// Shows the error report window, displaying a specific avatar and its error report (if any). + /// + /// public static void ShowReport(GameObject avatarRoot) { if (Application.isBatchMode || avatarRoot == null) return; @@ -310,11 +308,4 @@ private static bool ShowCurrentAvatarErrorReportValidation() return Selection.activeGameObject != null && RuntimeUtil.IsAvatarRoot(Selection.activeGameObject.transform); } } - - internal class TestError : SimpleError - { - public override ErrorCategory Category => ErrorCategory.NonFatal; - protected override Localizer Localizer => NDMFLocales.L; - protected override string TitleKey => "ndmf.test_error"; - } } \ No newline at end of file diff --git a/Editor/ErrorReporting/UI/ObjectSelector.cs b/Editor/ErrorReporting/UI/ObjectSelector.cs index 44f33b4..03fd7c2 100644 --- a/Editor/ErrorReporting/UI/ObjectSelector.cs +++ b/Editor/ErrorReporting/UI/ObjectSelector.cs @@ -6,8 +6,18 @@ namespace nadena.dev.ndmf.ui { using UnityObject = UnityEngine.Object; + /// + /// VisualElement used to provide a consistent UI for selecting an object referenced by error messages. + /// public sealed class ObjectSelector : VisualElement { + /// + /// Attempt to resolve an ObjectReference to a unity object and create a UI to display it. + /// + /// Error report this selector is referencing (used to find the avatar root) + /// Object reference to represent + /// The ObjectSelector to be created + /// public static bool TryCreate(ErrorReport report, ObjectReference reference, out ObjectSelector selector) { selector = null; diff --git a/Editor/ErrorReporting/UI/SimpleErrorUI.cs b/Editor/ErrorReporting/UI/SimpleErrorUI.cs index ec92d36..61442a2 100644 --- a/Editor/ErrorReporting/UI/SimpleErrorUI.cs +++ b/Editor/ErrorReporting/UI/SimpleErrorUI.cs @@ -79,7 +79,7 @@ private void RenderContent() } var icon = this.Q("icon"); - icon.Category = _error.Category; + icon.Severity = _error.Severity; var objRefs = this.Q("object-references"); if (_error.References.Length == 0) diff --git a/Editor/UI/LanguageSwitcher.cs b/Editor/UI/LanguageSwitcher.cs index db94947..5aefea7 100644 --- a/Editor/UI/LanguageSwitcher.cs +++ b/Editor/UI/LanguageSwitcher.cs @@ -7,6 +7,9 @@ namespace nadena.dev.ndmf.ui { + /// + /// VisualElement to display a language selector. + /// public sealed class LanguageSwitcher : VisualElement { public new class UxmlFactory : UxmlFactory @@ -24,6 +27,9 @@ public LanguageSwitcher() Add(imgui); } + /// + /// Draws a language selector using IMGUI. + /// public static void DrawImmediate() { var curLang = LanguagePrefs.Language; diff --git a/Editor/UI/Localization/LanguagePrefs.cs b/Editor/UI/Localization/LanguagePrefs.cs index a886aa3..aba628b 100644 --- a/Editor/UI/Localization/LanguagePrefs.cs +++ b/Editor/UI/Localization/LanguagePrefs.cs @@ -5,10 +5,16 @@ namespace nadena.dev.ndmf.localization { + /// + /// Tracks the currently selected UI language + /// public static class LanguagePrefs { private static string _curLanguage = "en-us"; + /// + /// The currently selected language ID, e.g. "en-us". + /// public static string Language { get => _curLanguage; @@ -44,6 +50,13 @@ public ElementFinalizer(Action theAction) private static ConditionalWeakTable _targetRefs = new ConditionalWeakTable(); + /// + /// Registers a callback to be invoked when the currently selected language changes. + /// This callback will be retained as long as the `handle` object is not garbage collected. + /// + /// An object which controls the lifetime of callback. + /// A callback to be invoked, passing the value of handle + /// public static void RegisterLanguageChangeCallback( T handle, Action callback @@ -76,6 +89,9 @@ private static void TriggerLanguageChangeCallbacks() } } + /// + /// Returns a list of all languages which have been registered at some point with the localization system. + /// public static ImmutableSortedSet RegisteredLanguages { get; @@ -87,9 +103,13 @@ static LanguagePrefs() RegisteredLanguages = ImmutableSortedSet.Empty; } - public static void RegisterLanguage(string defaultLanguage) + /// + /// Registers an additional language code to display in the language selectors. + /// + /// + public static void RegisterLanguage(string languageCode) { - RegisteredLanguages = RegisteredLanguages.Add(defaultLanguage); + RegisteredLanguages = RegisteredLanguages.Add(languageCode); } } } \ No newline at end of file diff --git a/Editor/UI/Localization/Localizer.cs b/Editor/UI/Localization/Localizer.cs index fe767de..a8b3d7f 100644 --- a/Editor/UI/Localization/Localizer.cs +++ b/Editor/UI/Localization/Localizer.cs @@ -10,10 +10,17 @@ namespace nadena.dev.ndmf.localization { - public class Localizer + /// + /// Provides a way to translate language lookup keys into localized strings. + /// + public sealed class Localizer { private static Action _reloadLocalizations; + /// + /// The default (fallback) language to use to look up keys when they are missing in the currently selected + /// UI language. + /// public string DefaultLanguage { get; } private ImmutableSortedDictionary> languages; @@ -22,25 +29,39 @@ public class Localizer private Func _lookupCache; private Func)>> _localizationLoader; - public Localizer(string defaultLanguage, Func)>> lookup) + /// + /// Constructs a Localizer based on a callback which loads from some external source of localizations. + /// The function is expected to return a list of (language, lookup) pairs, where lookup is a function which + /// attempts to look up a single string by its key, returning null if not found. + /// + /// This function may be called multiple times if localizations are reloaded. + /// + /// The default language code to use as a fallback when strings are missing + /// + public Localizer(string defaultLanguage, Func)>> loader) { DefaultLanguage = defaultLanguage; LanguagePrefs.RegisterLanguage(defaultLanguage); - _localizationLoader = lookup; + _localizationLoader = loader; languages = ImmutableSortedDictionary>.Empty; LoadLocalizations(); _reloadLocalizations += LoadLocalizations; } - public Localizer(string defaultLanguage, Func> lookup) + /// + /// Constructs a localizer based on a list of LocalizationAssets. + /// + /// The default language code to use as a fallback when strings are missing + /// A function which loads LocalizationAssets + public Localizer(string defaultLanguage, Func> assetLoader) { DefaultLanguage = defaultLanguage; LanguagePrefs.RegisterLanguage(defaultLanguage); _localizationLoader = () => { - return lookup().Select< + return assetLoader().Select< LocalizationAsset, (string, Func) >(asset => (asset.localeIsoCode, asset.GetLocalizedString)).ToList(); @@ -51,30 +72,6 @@ public Localizer(string defaultLanguage, Func> lookup) _reloadLocalizations += LoadLocalizations; } - public Localizer(string defaultLanguage, Func lookup) - { - DefaultLanguage = defaultLanguage; - _localizationLoader = null; - - LanguagePrefs.RegisterLanguage(defaultLanguage); - - languages = ImmutableSortedDictionary>.Empty - .Add(defaultLanguage, lookup); - _reloadLocalizations += LoadLocalizations; - } - - public Localizer(LocalizationAsset asset) - { - DefaultLanguage = asset.localeIsoCode; - _localizationLoader = null; - - LanguagePrefs.RegisterLanguage(asset.localeIsoCode); - - languages = ImmutableSortedDictionary>.Empty - .Add(asset.localeIsoCode, asset.GetLocalizedString); - _reloadLocalizations += LoadLocalizations; - } - private Localizer(string defaultLanguage, ImmutableSortedDictionary> languages) { DefaultLanguage = defaultLanguage; @@ -98,29 +95,21 @@ void LoadLocalizations() languages = newLanguages; } + /// + /// Reloads all localizations from their loader functions. + /// public static void ReloadLocalizations() { AssetDatabase.Refresh(); _reloadLocalizations?.Invoke(); } - public Localizer WithLanguage(LocalizationAsset asset) - { - return WithLanguage(asset.localeIsoCode, asset.GetLocalizedString); - } - - public Localizer WithLanguage(string lang, Func lookup) - { - if (languages.ContainsKey(lang)) - { - throw new ArgumentException($"Language {lang} already exists"); - } - - LanguagePrefs.RegisterLanguage(lang); - - return new Localizer(DefaultLanguage, languages.Add(lang, lookup)); - } - + /// + /// Attempts to look up a localized string. Returns true if the string was found, false otherwise. + /// + /// + /// + /// public bool TryGetLocalizedString(string key, out string value) { if (_lookupCache == null || _lastLanguage != LanguagePrefs.Language) @@ -159,6 +148,11 @@ public bool TryGetLocalizedString(string key, out string value) return value != null && value != key; } + /// + /// Obtains a localized string, or a placeholder if it cannot be found. + /// + /// + /// public string GetLocalizedString(string key) { if (!TryGetLocalizedString(key, out var value)) @@ -169,6 +163,13 @@ public string GetLocalizedString(string key) return value; } + /// + /// Localizes UI elements under the given root element. Any elements with the class "ndmf-tr" will be + /// localized automatically, with localization keys under their `text` or `label` properties being converted + /// into localized strings. These elements will automatically update when the currently selected language + /// changes. + /// + /// public void LocalizeUIElements(VisualElement root) { new UIElementLocalizer(this).Localize(root); diff --git a/Editor/UI/Localization/NDMFLocales.cs b/Editor/UI/Localization/NDMFLocales.cs index f7a23f0..28ec010 100644 --- a/Editor/UI/Localization/NDMFLocales.cs +++ b/Editor/UI/Localization/NDMFLocales.cs @@ -1,4 +1,5 @@ -using UnityEditor; +using System.Collections.Generic; +using UnityEditor; using UnityEngine; namespace nadena.dev.ndmf.localization @@ -6,13 +7,16 @@ namespace nadena.dev.ndmf.localization internal static class NDMFLocales { public static Localizer L = new Localizer( - AssetDatabase.LoadAssetAtPath( - AssetDatabase.GUIDToAssetPath("5cb11a9adc5d7404d8c01d558a5c0af6") - ) - ).WithLanguage( - AssetDatabase.LoadAssetAtPath( - AssetDatabase.GUIDToAssetPath("87c99a0330751d842a030f1385973541") - ) + "en-us", + () => new List() + { + AssetDatabase.LoadAssetAtPath( + AssetDatabase.GUIDToAssetPath("5cb11a9adc5d7404d8c01d558a5c0af6") + ), + AssetDatabase.LoadAssetAtPath( + AssetDatabase.GUIDToAssetPath("87c99a0330751d842a030f1385973541") + ) + } ); } } \ No newline at end of file diff --git a/Editor/UI/Localized.cs b/Editor/UI/Localized.cs deleted file mode 100644 index 346fb1c..0000000 --- a/Editor/UI/Localized.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using UnityEngine.UIElements; - -namespace nadena.dev.ndmf.ui -{ - public class Localized : VisualElement - { - private static Dictionary> _localizers = - new Dictionary>(); - - public new class UxmlFactory : UxmlFactory - { - } - - public new class UxmlTraits : VisualElement.UxmlTraits - { - UxmlStringAttributeDescription m_Folder = new UxmlStringAttributeDescription {name = "folder"}; - - public override IEnumerable uxmlChildElementsDescription - { - get - { - yield return new UxmlChildElementDescription(typeof (VisualElement)); - } - } - - public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) - { - base.Init(ve, bag, cc); - - var elem = ve as Localized; - - elem.folder = m_Folder.GetValueFromBag(bag, cc); - } - } - - public string folder { get; set; } - - public Localized() - { - RegisterCallback(Init); - } - - private void Init(GeometryChangedEvent evt) - { - WalkTree(this); - UnregisterCallback(Init); - } - - private static void WalkTree(VisualElement elem) - { - var ty = elem.GetType(); - - GetLocalizationOperation(ty)(elem); - - foreach (var child in elem.Children()) - { - WalkTree(child); - } - } - - private static Action GetLocalizationOperation(Type ty) - { - if (!_localizers.TryGetValue(ty, out var action)) - { - PropertyInfo m_label = ty.GetProperty("text") ?? ty.GetProperty("label"); - - if (m_label == null) - { - action = _elem => { }; - } - else - { - action = elem => - { - var cur_label = m_label.GetValue(elem) as string; - if (cur_label != null && cur_label.StartsWith("##")) - { - var key = cur_label.Substring(2); - - var new_label = "label: " + key; - var new_tooltip = "tooltip: " + key; - - m_label.SetValue(elem, new_label); - elem.tooltip = new_tooltip; - } - }; - } - - _localizers[ty] = action; - } - - return action; - } - } -} \ No newline at end of file diff --git a/Editor/UI/Localized.cs.meta b/Editor/UI/Localized.cs.meta deleted file mode 100644 index 06d4aca..0000000 --- a/Editor/UI/Localized.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: fed7813f40c148818422ee5a96e22b47 -timeCreated: 1697879525 \ No newline at end of file