diff --git a/Directory.Build.props b/Directory.Build.props index 5176381..372c6fa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,6 +11,6 @@ It should follow the format major.minor.patch (semantic versioning). If you publish your mod as a library to NuGet, this version will also be used as the package version. --> - 0.3.0 + 0.4.0 diff --git a/Elements/ChoiceElement.cs b/Elements/ChoiceElement.cs index 05ebc5c..150043b 100644 --- a/Elements/ChoiceElement.cs +++ b/Elements/ChoiceElement.cs @@ -17,7 +17,7 @@ public class ChoiceElement : SelectableValueElement /// /// Construct a ChoiceElement with the given label text, model, and description text. /// - public ChoiceElement(string label, IChoiceModel model, string description = "") + public ChoiceElement(LocalizedText label, IChoiceModel model, LocalizedText description) : base( MenuPrefabs.Get().NewTextChoiceContainer(out var menuOptionHorizontal), menuOptionHorizontal, @@ -36,17 +36,29 @@ public ChoiceElement(string label, IChoiceModel model, string description = " OnValueChanged += _ => custom.UpdateText(); - LabelText.text = label; - DescriptionText.text = description; + LabelText.LocalizedText = label; + DescriptionText.LocalizedText = description; custom.UpdateText(); } + /// + /// Construct a ChoiceElement with the given label and model. + /// + public ChoiceElement(LocalizedText label, IChoiceModel model) + : this(label, model, "") { } + /// /// Shortcut for building a ChoiceElement from a finite list of values. /// - public ChoiceElement(string label, List items, string description = "") + public ChoiceElement(LocalizedText label, List items, LocalizedText description) : this(label, ChoiceModels.ForValues(items), description) { } + /// + /// Shortcut for building a ChoiceElement from a finite list of values. + /// + public ChoiceElement(LocalizedText label, List items) + : this(label, ChoiceModels.ForValues(items), "") { } + /// /// The value holder and model underlying this choice element. /// diff --git a/Elements/DynamicDescriptionChoiceElement.cs b/Elements/DynamicDescriptionChoiceElement.cs index b1c2864..9283bc3 100644 --- a/Elements/DynamicDescriptionChoiceElement.cs +++ b/Elements/DynamicDescriptionChoiceElement.cs @@ -19,17 +19,17 @@ public class DynamicDescriptionChoiceElement : ChoiceElement { internal const string RIGHT_DESCRIPTION_NAME = "ModMenu-Right Description"; - /// + /// public DynamicDescriptionChoiceElement( - string label, + LocalizedText label, IChoiceModel model, - string description, - string rightDescription + LocalizedText description, + LocalizedText rightDescription ) : base(label, model, description) { RightText = SetupRightDescription(DescriptionText, ChoiceText); - RightText.text = rightDescription; + RightText.LocalizedText = rightDescription; } /// @@ -40,14 +40,14 @@ string rightDescription /// /// Function used to determine the description below the choice. public DynamicDescriptionChoiceElement( - string label, + LocalizedText label, IChoiceModel model, - string description, - Func getRightDescription + LocalizedText description, + Func getRightDescription ) : this(label, model, description, getRightDescription(model.Value)) { - model.OnValueChanged += value => RightText.text = getRightDescription(value); + model.OnValueChanged += value => RightText.LocalizedText = getRightDescription(value); } /// diff --git a/Elements/KeyBindElement.cs b/Elements/KeyBindElement.cs index 6c83fe8..a15d1e7 100644 --- a/Elements/KeyBindElement.cs +++ b/Elements/KeyBindElement.cs @@ -14,7 +14,7 @@ public class KeyBindElement : SelectableValueElement /// /// Construct a KeyBindElement with a custom model. /// - public KeyBindElement(string label, IValueModel model) + public KeyBindElement(LocalizedText label, IValueModel model) : base( MenuPrefabs.Get().NewKeyBindContainer(out var customMappableKey), customMappableKey, @@ -27,13 +27,13 @@ public KeyBindElement(string label, IValueModel model) KeyBindText = customMappableKey.KeymapText!; KeyBindImage = customMappableKey.KeymapImage!; - LabelText.text = label; + LabelText.LocalizedText = label; } /// /// Construct a KeyBindElement with a default model that accepts any KeyCode. /// - public KeyBindElement(string label) + public KeyBindElement(LocalizedText label) : this(label, new ValueModel(KeyCode.A)) { } /// diff --git a/Elements/LocalizedText.cs b/Elements/LocalizedText.cs new file mode 100644 index 0000000..1dae199 --- /dev/null +++ b/Elements/LocalizedText.cs @@ -0,0 +1,62 @@ +using TeamCherry.Localization; + +namespace Silksong.ModMenu.Elements; + +/// +/// Wrapper class for either text that supports localization, or raw text which does not. +/// +public class LocalizedText +{ + private readonly TeamCherry.Localization.LocalisedString localisedString; + private readonly string rawText; + + private LocalizedText(LocalisedString localisedString, string rawText) + { + this.localisedString = localisedString; + this.rawText = rawText; + } + + /// + /// Get the readable text represented by this object. + /// + public string Text => + localisedString.IsEmpty + ? rawText + : Language.Get(localisedString.Key, localisedString.Sheet); + + /// + /// Returns true if this object has localization support. + /// + public bool IsLocalized => !localisedString.IsEmpty; + + /// + /// Returns the localized identifier for this object, which may be empty. + /// + public LocalisedString Localized => localisedString; + + /// + /// Represents localized text with the given key and an empty sheet title. + /// + public static LocalizedText Key(string languageKey) => new(new("", languageKey), ""); + + /// + /// Represents localized text with the given key. + /// + public static LocalizedText Key(LocalisedString localisedString) => new(localisedString, ""); + + /// + /// Represents text with no localization that always renders to the given value. + /// + public static LocalizedText Raw(string rawText) => new(new(), rawText); + + /// + /// Implicit conversion for raw text to un-localized LocalizedText. + /// + public static implicit operator LocalizedText(string rawText) => Raw(rawText); + + /// + /// Implicit conversion from a LocalisedString. + /// + public static implicit operator LocalizedText(LocalisedString localisedString) => + Key(localisedString); +} diff --git a/Elements/LocalizedTextExtensions.cs b/Elements/LocalizedTextExtensions.cs new file mode 100644 index 0000000..59974f6 --- /dev/null +++ b/Elements/LocalizedTextExtensions.cs @@ -0,0 +1,46 @@ +using UnityEngine.UI; + +namespace Silksong.ModMenu.Elements; + +/// +/// Helper methods for working with LocalizedText. +/// +public static class LocalizedTextExtensions +{ + extension(Text self) + { + /// + /// Helper field to set localized text on a Text component. + /// + public LocalizedText LocalizedText + { + get + { + if (self.TryGetComponent(out var auto) && !auto.text.IsEmpty) + return auto.text; + else + return self.text; + } + set + { + if (value.IsLocalized) + { + if (!self.TryGetComponent(out var auto)) + { + auto = self.gameObject.AddComponent(); + auto.textField = self; + } + + auto.text = value.Localized; + auto.RefreshTextFromLocalization(); + } + else + { + if (self.TryGetComponent(out var auto)) + UnityEngine.Object.Destroy(auto); + self.text = value.Text; + } + } + } + } +} diff --git a/Elements/SliderElement.cs b/Elements/SliderElement.cs index 16c6632..513ee74 100644 --- a/Elements/SliderElement.cs +++ b/Elements/SliderElement.cs @@ -19,10 +19,10 @@ public class SliderElement : SelectableValueElement /// /// The label text for the slider. /// The model for the domain range and underlying value. - public SliderElement(string label, SliderModel model) + public SliderElement(LocalizedText label, SliderModel model) : base(MenuPrefabs.Get().NewSliderContainer(out var slider), slider, model) { - Container.name = label; + Container.name = label.Text; Slider = slider; var sliderObj = Slider.gameObject; @@ -57,7 +57,7 @@ public SliderElement(string label, SliderModel model) } }); - LabelText.text = label; + LabelText.LocalizedText = label; UpdateValueText(); } @@ -95,5 +95,5 @@ public override void SetFontSizes(FontSizes fontSizes) ValueText.fontSize = fontSizes.SliderSize(); } - private void UpdateValueText() => ValueText.text = SliderModel.DisplayString(); + private void UpdateValueText() => ValueText.LocalizedText = SliderModel.DisplayString(); } diff --git a/Elements/TextButton.cs b/Elements/TextButton.cs index 92eeba7..553f3ce 100644 --- a/Elements/TextButton.cs +++ b/Elements/TextButton.cs @@ -15,10 +15,10 @@ public class TextButton : SelectableElement /// /// Construct a text button with the given text. /// - public TextButton(string text) + public TextButton(LocalizedText text) : base(MenuPrefabs.Get().NewTextButtonContainer(out var menuButton), menuButton) { - Container.name = text; + Container.name = text.Text; MenuButton = menuButton; MenuButton @@ -35,7 +35,7 @@ public TextButton(string text) MenuButton.buttonType = MenuButton.MenuButtonType.Activate; ButtonText = menuButton.gameObject.FindChild("Menu Button Text")!.GetComponent(); - ButtonText.text = text; + ButtonText.LocalizedText = text; } /// diff --git a/Elements/TextInput.cs b/Elements/TextInput.cs index 9ff4def..0a0af3d 100644 --- a/Elements/TextInput.cs +++ b/Elements/TextInput.cs @@ -28,7 +28,7 @@ public class TextInput : SelectableValueElement /// /// Construct a basic text input. /// - public TextInput(string label, ITextModel model, string description = "") + public TextInput(LocalizedText label, ITextModel model, LocalizedText description) : base(MenuPrefabs.Get().NewTextInputContainer(out var inputField), inputField, model) { TextModel = model; @@ -40,8 +40,8 @@ public TextInput(string label, ITextModel model, string description = "") OnTextValueChanged += value => InputField.text = value; - LabelText.text = label; - DescriptionText.text = description; + LabelText.LocalizedText = label; + DescriptionText.LocalizedText = description; if (intTypes.Contains(typeof(T))) InputField.contentType = InputField.ContentType.IntegerNumber; @@ -49,6 +49,12 @@ public TextInput(string label, ITextModel model, string description = "") InputField.contentType = InputField.ContentType.DecimalNumber; } + /// + /// Construct a basic text input with no description. + /// + public TextInput(LocalizedText label, ITextModel model) + : this(label, model, "") { } + /// /// The value holder and model underlying this choice element. /// diff --git a/Elements/TextLabel.cs b/Elements/TextLabel.cs index 88bd189..1271d9a 100644 --- a/Elements/TextLabel.cs +++ b/Elements/TextLabel.cs @@ -12,12 +12,12 @@ public class TextLabel : MenuElement /// /// Construct a label with the given text contents. /// - public TextLabel(string text) + public TextLabel(LocalizedText text) : base(MenuPrefabs.Get().NewTextLabel()) { - Container.name = text; + Container.name = text.Text; Text = Container.GetComponent(); - Text.text = text; + Text.LocalizedText = text; } /// diff --git a/Internal/CustomMenuOptionHorizontal.cs b/Internal/CustomMenuOptionHorizontal.cs index f4a36d9..450c2fa 100644 --- a/Internal/CustomMenuOptionHorizontal.cs +++ b/Internal/CustomMenuOptionHorizontal.cs @@ -1,6 +1,7 @@ using MonoDetour; using MonoDetour.DetourTypes; using MonoDetour.HookGen; +using Silksong.ModMenu.Elements; using Silksong.ModMenu.Models; using UnityEngine; using UnityEngine.EventSystems; @@ -27,7 +28,7 @@ internal void UpdateText() if (orig == null) return; - orig.optionText.text = Model?.DisplayString() ?? "???"; + orig.optionText.LocalizedText = Model?.DisplayString() ?? "???"; if (orig.optionText.TryGetComponent(out var align)) align.AlignText(); } diff --git a/Models/AbstractValueModel.cs b/Models/AbstractValueModel.cs index 1dc7736..9f69cf8 100644 --- a/Models/AbstractValueModel.cs +++ b/Models/AbstractValueModel.cs @@ -1,4 +1,5 @@ using System; +using Silksong.ModMenu.Elements; namespace Silksong.ModMenu.Models; @@ -57,5 +58,5 @@ public T Value } /// - public virtual string DisplayString() => $"{GetValue()}"; + public virtual LocalizedText DisplayString() => $"{GetValue()}"; } diff --git a/Models/Delegates.cs b/Models/Delegates.cs index e923489..0eb0705 100644 --- a/Models/Delegates.cs +++ b/Models/Delegates.cs @@ -1,6 +1,8 @@ -namespace Silksong.ModMenu.Models; +using Silksong.ModMenu.Elements; + +namespace Silksong.ModMenu.Models; /// /// Converts 'item' to string with the additional context of an associated index, most likely within a list. /// -public delegate string IndexedToString(int index, T item); +public delegate LocalizedText IndexedToString(int index, T item); diff --git a/Models/IDisplayable.cs b/Models/IDisplayable.cs index ebe2231..b66ff8e 100644 --- a/Models/IDisplayable.cs +++ b/Models/IDisplayable.cs @@ -1,4 +1,6 @@ -namespace Silksong.ModMenu.Models; +using Silksong.ModMenu.Elements; + +namespace Silksong.ModMenu.Models; /// /// Base interface for all models which ultimately display a string to represent the chosen value. @@ -8,5 +10,5 @@ public interface IDisplayable /// /// The UI string to display for this entity. /// - string DisplayString(); + LocalizedText DisplayString(); } diff --git a/Models/IntRangeChoiceModel.cs b/Models/IntRangeChoiceModel.cs index 2ac12de..686d873 100644 --- a/Models/IntRangeChoiceModel.cs +++ b/Models/IntRangeChoiceModel.cs @@ -1,4 +1,5 @@ using System; +using Silksong.ModMenu.Elements; namespace Silksong.ModMenu.Models; @@ -90,10 +91,11 @@ public override bool SetValue(int value) /// /// A custom display function to use for the selected integer value. /// - public Func? DisplayFn; + public Func? DisplayFn; /// - public override string DisplayString() => DisplayFn?.Invoke(GetValue()) ?? $"{GetValue()}"; + public override LocalizedText DisplayString() => + DisplayFn?.Invoke(GetValue()) ?? $"{GetValue()}"; private void ResetParamsInternal(int min, int max, int value) { diff --git a/Models/LinearFloatSliderModel.cs b/Models/LinearFloatSliderModel.cs index 179e564..d07a606 100644 --- a/Models/LinearFloatSliderModel.cs +++ b/Models/LinearFloatSliderModel.cs @@ -1,4 +1,5 @@ using System; +using Silksong.ModMenu.Elements; using UnityEngine; namespace Silksong.ModMenu.Models; @@ -67,5 +68,5 @@ protected override bool GetIndex(float value, out int index) } /// - protected override string DefaultDisplayString(int index, float item) => $"{item:0.###}"; + protected override LocalizedText DefaultDisplayString(int index, float item) => $"{item:0.###}"; } diff --git a/Models/ListChoiceModel.cs b/Models/ListChoiceModel.cs index e45b29e..f7e9409 100644 --- a/Models/ListChoiceModel.cs +++ b/Models/ListChoiceModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Silksong.ModMenu.Elements; namespace Silksong.ModMenu.Models; @@ -102,13 +103,13 @@ public override bool SetValue(T value) public IndexedToString? DisplayFn; /// - public override string DisplayString() => + public override LocalizedText DisplayString() => (DisplayFn ?? DefaultDisplayString).Invoke(Index, GetValue()); /// /// Default string to display, in the absence of a DisplayFn. /// - protected virtual string DefaultDisplayString(int index, T item) => $"{item}"; + protected virtual LocalizedText DefaultDisplayString(int index, T item) => $"{item}"; private bool Move(int delta) { diff --git a/Models/ParserTextModel.cs b/Models/ParserTextModel.cs index 398e188..5627ea9 100644 --- a/Models/ParserTextModel.cs +++ b/Models/ParserTextModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Silksong.ModMenu.Elements; namespace Silksong.ModMenu.Models; @@ -109,7 +110,7 @@ public bool SetValue(T value) } /// - public string DisplayString() => text; + public LocalizedText DisplayString() => text; /// public T Value diff --git a/Models/SliderModel.cs b/Models/SliderModel.cs index 3503912..3008955 100644 --- a/Models/SliderModel.cs +++ b/Models/SliderModel.cs @@ -1,4 +1,5 @@ using System; +using Silksong.ModMenu.Elements; namespace Silksong.ModMenu.Models; @@ -89,11 +90,11 @@ public override bool SetValue(T value) public IndexedToString? DisplayFn; /// - public override string DisplayString() => + public override LocalizedText DisplayString() => (DisplayFn ?? DefaultDisplayString).Invoke(Index, GetValue()); /// /// The default display string for this model, in the absence of a DisplayFn. /// - protected virtual string DefaultDisplayString(int index, T item) => $"{item}"; + protected virtual LocalizedText DefaultDisplayString(int index, T item) => $"{item}"; } diff --git a/README.md b/README.md index 0110cc7..7128dfe 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,7 @@ It is recommended, though not required, that you create new Models every time yo ## Future work -The ModMenu mod should be considered _unstable_ version 1.0 is released. Breaking API changes may occur in the pursuit of implementing additional features to reach 1.0. - -Required for 1.0: -* Localization +The ModMenu mod should be considered _unstable_ until version 1.0 is released. Breaking API changes may occur in the pursuit of implementing additional features to reach 1.0. Optional future work: * Extending the 'new game' menu with new game modes.