A game framework written with osu! in mind.

Merge pull request #4518 from smoogipoo/add-localisable-description

Add LocalisableEnumAttribute and GetLocalisableDescription() extension method

authored by

Dean Herbert and committed by
GitHub
0b4a9c68 3c860dae

+215 -1
+96
osu.Framework.Tests/Localisation/LocalisableEnumAttributeTest.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + using NUnit.Framework; 6 + using osu.Framework.Extensions; 7 + using osu.Framework.Localisation; 8 + 9 + namespace osu.Framework.Tests.Localisation 10 + { 11 + [TestFixture] 12 + public class LocalisableEnumAttributeTest 13 + { 14 + [TestCase(EnumA.Item1, "Item1")] 15 + [TestCase(EnumA.Item2, "B")] 16 + public void TestNonLocalisableEnumReturnsDescriptionOrToString(EnumA value, string expected) 17 + { 18 + Assert.That(value.GetLocalisableDescription().ToString(), Is.EqualTo(expected)); 19 + } 20 + 21 + [TestCase(EnumB.Item1, "A")] 22 + [TestCase(EnumB.Item2, "B")] 23 + public void TestLocalisableEnumReturnsMappedValue(EnumB value, string expected) 24 + { 25 + Assert.That(value.GetLocalisableDescription().ToString(), Is.EqualTo(expected)); 26 + } 27 + 28 + [Test] 29 + public void TestLocalisableEnumWithInvalidBaseTypeThrows() 30 + { 31 + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed 32 + Assert.Throws<ArgumentException>(() => EnumC.Item1.GetLocalisableDescription().ToString()); 33 + } 34 + 35 + [Test] 36 + public void TestLocalisableEnumWithInvalidGenericTypeThrows() 37 + { 38 + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed 39 + Assert.Throws<InvalidOperationException>(() => EnumD.Item1.GetLocalisableDescription().ToString()); 40 + } 41 + 42 + public enum EnumA 43 + { 44 + Item1, 45 + 46 + [System.ComponentModel.Description("B")] 47 + Item2 48 + } 49 + 50 + [LocalisableEnum(typeof(EnumBEnumLocalisationMapper))] 51 + public enum EnumB 52 + { 53 + Item1, 54 + Item2 55 + } 56 + 57 + private class EnumBEnumLocalisationMapper : EnumLocalisationMapper<EnumB> 58 + { 59 + public override LocalisableString Map(EnumB value) 60 + { 61 + switch (value) 62 + { 63 + case EnumB.Item1: 64 + return "A"; 65 + 66 + case EnumB.Item2: 67 + return "B"; 68 + 69 + default: 70 + throw new ArgumentOutOfRangeException(nameof(value), value, null); 71 + } 72 + } 73 + } 74 + 75 + [LocalisableEnum(typeof(EnumCEnumLocalisationMapper))] 76 + public enum EnumC 77 + { 78 + Item1, 79 + } 80 + 81 + private class EnumCEnumLocalisationMapper 82 + { 83 + } 84 + 85 + [LocalisableEnum(typeof(EnumDEnumLocalisationMapper))] 86 + public enum EnumD 87 + { 88 + Item1, 89 + } 90 + 91 + private class EnumDEnumLocalisationMapper : EnumLocalisationMapper<EnumA> 92 + { 93 + public override LocalisableString Map(EnumA value) => "A"; 94 + } 95 + } 96 + }
+55
osu.Framework/Extensions/ExtensionMethods.cs
··· 4 4 using System; 5 5 using System.Collections.Generic; 6 6 using System.ComponentModel; 7 + using System.Diagnostics; 7 8 using System.Drawing; 8 9 using System.IO; 9 10 using System.Linq; 10 11 using System.Reflection; 11 12 using System.Security.Cryptography; 12 13 using System.Text; 14 + using osu.Framework.Extensions.TypeExtensions; 15 + using osu.Framework.Localisation; 13 16 using osu.Framework.Platform; 14 17 using osuTK; 15 18 ··· 173 176 } 174 177 } 175 178 179 + /// <summary> 180 + /// Returns the description of a given enum value, via (in order): 181 + /// <list type="number"> 182 + /// <item> 183 + /// <description>Any <see cref="LocalisableEnumAttribute"/> attached to the enum type.</description> 184 + /// </item> 185 + /// <item> 186 + /// <description><see cref="GetDescription"/></description> 187 + /// </item> 188 + /// </list> 189 + /// </summary> 190 + /// <exception cref="InvalidOperationException">When the enum type has an attached <see cref="LocalisableEnumAttribute"/> 191 + /// and the <see cref="EnumLocalisationMapper{T}"/> could not be instantiated.</exception> 192 + /// <exception cref="InvalidOperationException">When the enum type has an attached <see cref="LocalisableEnumAttribute"/> 193 + /// and the type handled by the <see cref="EnumLocalisationMapper{T}"/> is not <typeparamref name="T"/>.</exception> 194 + public static LocalisableString GetLocalisableDescription<T>(this T value) 195 + where T : Enum 196 + { 197 + var enumType = value.GetType(); 198 + 199 + var mapperType = enumType.GetCustomAttribute<LocalisableEnumAttribute>()?.MapperType; 200 + if (mapperType == null) 201 + return GetDescription(value); 202 + 203 + var mapperInstance = Activator.CreateInstance(mapperType); 204 + if (mapperInstance == null) 205 + throw new InvalidOperationException($"Could not create the {nameof(EnumLocalisationMapper<T>)} for enum type {enumType.ReadableName()}"); 206 + 207 + var mapMethod = mapperType.GetMethod(nameof(EnumLocalisationMapper<T>.Map), BindingFlags.Instance | BindingFlags.Public); 208 + Debug.Assert(mapMethod != null); 209 + 210 + var expectedMappingType = mapMethod.GetParameters()[0].ParameterType; 211 + if (expectedMappingType != enumType) 212 + throw new InvalidOperationException($"Cannot use {mapperType.ReadableName()} (maps {expectedMappingType.ReadableName()} enum values) to map {enumType.ReadableName()} enum values."); 213 + 214 + var mappedValue = mapMethod.Invoke(mapperInstance, new object[] { value }); 215 + Debug.Assert(mappedValue != null); 216 + 217 + return (LocalisableString)mappedValue; 218 + } 219 + 220 + /// <summary> 221 + /// Returns the description of a given object, via (in order): 222 + /// <list type="number"> 223 + /// <item> 224 + /// <description>Any attached <see cref="DescriptionAttribute"/>.</description> 225 + /// </item> 226 + /// <item> 227 + /// <description>The object's <see cref="object.ToString()"/>.</description> 228 + /// </item> 229 + /// </list> 230 + /// </summary> 176 231 public static string GetDescription(this object value) 177 232 => value.GetType() 178 233 .GetField(value.ToString())?
+1 -1
osu.Framework/Graphics/UserInterface/Dropdown.cs
··· 162 162 return t.Text; 163 163 164 164 case Enum e: 165 - return e.GetDescription(); 165 + return e.GetLocalisableDescription(); 166 166 167 167 default: 168 168 return item?.ToString() ?? "null";
+29
osu.Framework/Localisation/EnumLocalisationMapper.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + 6 + namespace osu.Framework.Localisation 7 + { 8 + /// <summary> 9 + /// Describes the values of an <see cref="Enum"/> type by <see cref="LocalisableString"/>s. 10 + /// </summary> 11 + /// <typeparam name="T">The <see cref="Enum"/> type.</typeparam> 12 + public abstract class EnumLocalisationMapper<T> : IEnumLocalisationMapper 13 + where T : Enum 14 + { 15 + /// <summary> 16 + /// Describes a <typeparamref name="T"/> value by a <see cref="LocalisableString"/>. 17 + /// </summary> 18 + /// <param name="value">The value to map.</param> 19 + /// <returns>The <see cref="LocalisableString"/> describing <paramref name="value"/>.</returns> 20 + public abstract LocalisableString Map(T value); 21 + } 22 + 23 + /// <summary> 24 + /// Marker class for <see cref="EnumLocalisationMapper{T}"/>. Do not use. 25 + /// </summary> 26 + internal interface IEnumLocalisationMapper 27 + { 28 + } 29 + }
+34
osu.Framework/Localisation/LocalisableEnumAttribute.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + using osu.Framework.Extensions; 6 + using osu.Framework.Extensions.TypeExtensions; 7 + 8 + namespace osu.Framework.Localisation 9 + { 10 + /// <summary> 11 + /// Indicates that the values of an enum have <see cref="LocalisableString"/> descriptions. 12 + /// The descriptions can be retrieved through <see cref="ExtensionMethods.GetLocalisableDescription{T}"/>. 13 + /// </summary> 14 + [AttributeUsage(AttributeTargets.Enum)] 15 + public sealed class LocalisableEnumAttribute : Attribute 16 + { 17 + /// <summary> 18 + /// The <see cref="EnumLocalisationMapper{T}"/> type that maps enum values to <see cref="LocalisableString"/>s. 19 + /// </summary> 20 + public readonly Type MapperType; 21 + 22 + /// <summary> 23 + /// Creates a new <see cref="LocalisableEnumAttribute"/>. 24 + /// </summary> 25 + /// <param name="mapperType">The <see cref="EnumLocalisationMapper{T}"/> type that maps enum values to <see cref="LocalisableString"/>s.</param> 26 + public LocalisableEnumAttribute(Type mapperType) 27 + { 28 + MapperType = mapperType; 29 + 30 + if (!typeof(IEnumLocalisationMapper).IsAssignableFrom(mapperType)) 31 + throw new ArgumentException($"Type \"{mapperType.ReadableName()}\" must inherit from {nameof(EnumLocalisationMapper<Enum>)}.", nameof(mapperType)); 32 + } 33 + } 34 + }