diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/StardewModdingAPI.Tests/ModResolverTests.cs | 1 | ||||
-rw-r--r-- | src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj | 2 | ||||
-rw-r--r-- | src/StardewModdingAPI.Tests/TranslationTests.cs | 333 | ||||
-rw-r--r-- | src/StardewModdingAPI/Translation.cs | 7 | ||||
-rw-r--r-- | src/crossplatform.targets | 1 |
5 files changed, 341 insertions, 3 deletions
diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index c0545cd6..46c1c0cf 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -13,6 +13,7 @@ using StardewModdingAPI.Tests.Framework; namespace StardewModdingAPI.Tests { + /// <summary>Unit tests for <see cref="ModResolver"/>.</summary> [TestFixture] public class ModResolverTests { diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj index c84adbd7..3818ec9c 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj @@ -48,6 +48,7 @@ <Compile Include="..\GlobalAssemblyInfo.cs"> <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> + <Compile Include="TranslationTests.cs" /> <Compile Include="ModResolverTests.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Framework\Sample.cs" /> @@ -61,5 +62,6 @@ <Name>StardewModdingAPI</Name> </ProjectReference> </ItemGroup> + <Import Project="$(SolutionDir)\crossplatform.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs new file mode 100644 index 00000000..3e6e6499 --- /dev/null +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using StardewModdingAPI.Framework; +using StardewValley; + +namespace StardewModdingAPI.Tests +{ + /// <summary>Unit tests for <see cref="TranslationHelper"/> and <see cref="Translation"/>.</summary> + [TestFixture] + public class TranslationTests + { + /********* + ** Data + *********/ + /// <summary>Sample translation text for unit tests.</summary> + public static string[] Samples = { null, "", " ", "boop", " boop " }; + + /// <summary>A token structure type.</summary> + public enum TokenType + { + /// <summary>The tokens are passed in a dictionary.</summary> + Dictionary, + + /// <summary>The tokens are passed in an anonymous object.</summary> + AnonymousObject + } + + + /********* + ** Unit tests + *********/ + /**** + ** Translation helper + ****/ + [Test(Description = "Assert that the translation helper correctly handles no translations.")] + public void Helper_HandlesNoTranslations() + { + // arrange + var data = new Dictionary<string, IDictionary<string, string>>(); + + // act + ITranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + Translation translation = helper.Translate("key"); + + // assert + Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value."); + Assert.IsNotNull(helper.GetTranslations(), "The full list of translations is unexpectedly null."); + Assert.AreEqual(0, helper.GetTranslations().Count, "The full list of translations is unexpectedly not empty."); + + Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation."); + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value."); + } + + [Test(Description = "Assert that the translation helper returns the expected translations correctly.")] + public void Helper_GetTranslations_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary<string, IDictionary<string, string>>(); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + actual[locale] = helper.GetTranslations(); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]), $"The translations for {locale} don't match the expected values."); + } + } + + [Test(Description = "Assert that the translations returned by the helper has the expected text.")] + public void Helper_Translate_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary<string, IDictionary<string, string>>(); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + actual[locale] = new Dictionary<string, string>(); + foreach (string key in expected[locale].Keys) + actual[locale][key] = helper.Translate(key); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]), $"The translations for {locale} don't match the expected values."); + } + } + + /**** + ** Translation + ****/ + [Test(Description = "Assert that HasValue returns the expected result for various inputs.")] + [TestCase(null, ExpectedResult = false)] + [TestCase("", ExpectedResult = false)] + [TestCase(" ", ExpectedResult = true)] + [TestCase("boop", ExpectedResult = true)] + [TestCase(" boop ", ExpectedResult = true)] + public bool Translation_HasValue(string text) + { + return new Translation("ModName", "pt-BR", "key", text).HasValue(); + } + + [Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")] + public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")] + public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")] + public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).UsePlaceholder(value); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else if (!value) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder disabled."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder enabled."); + } + + [Test(Description = "Assert that the translation's Assert method throws the expected exception.")] + public void Translation_Assert([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.That(() => translation.Assert(), Throws.Nothing, "The assert unexpected threw an exception for a valid input."); + else + Assert.That(() => translation.Assert(), Throws.Exception.TypeOf<KeyNotFoundException>(), "The assert didn't throw an exception for invalid input."); + } + + [Test(Description = "Assert that the translation returns the expected text after setting the default.")] + public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Default(@default); + + // assert + if (!string.IsNullOrEmpty(text)) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + else if (!string.IsNullOrEmpty(@default)) + Assert.AreEqual(@default, translation.ToString(), "The translation returned an unexpected value given a null or empty base text, but valid default."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty base and default text."); + } + + /**** + ** Translation tokens + ****/ + [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] + public void Translation_Tokens([Values(TokenType.AnonymousObject, TokenType.Dictionary)] TokenType tokenType) + { + // arrange + string start = Guid.NewGuid().ToString("N"); + string middle = Guid.NewGuid().ToString("N"); + string end = Guid.NewGuid().ToString("N"); + const string input = "{{start}} tokens are properly replaced (including {{middle}} {{ MIDdlE}}) {{end}}"; + string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", input); + switch (tokenType) + { + case TokenType.AnonymousObject: + translation = translation.Tokens(new { start, middle, end }); + break; + + case TokenType.Dictionary: + translation = translation.Tokens(new Dictionary<string, object> { ["start"] = start, ["middle"] = middle, ["end"] = end }); + break; + + default: + throw new NotSupportedException($"Unknown token type {tokenType}."); + } + + // assert + Assert.AreEqual(expected, translation.ToString(), "The translation returned an unexpected text."); + } + + [Test(Description = "Assert that the translation can replace tokens in all valid formats.")] + [TestCase("{{value}}", "value")] + [TestCase("{{ value }}", "value")] + [TestCase("{{value }}", "value")] + [TestCase("{{ the_value }}", "the_value")] + [TestCase("{{ the.value_here }}", "the.value_here")] + [TestCase("{{ the_value-here.... }}", "the_value-here....")] + [TestCase("{{ tHe_vALuE-HEre.... }}", "tHe_vALuE-HEre....")] + public void Translation_Tokens_ValidFormats(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary<string, object> { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + [Test(Description = "Assert that translation tokens are case-insensitive and surrounding-whitespace-insensitive.")] + [TestCase("{{value}}", "value")] + [TestCase("{{VaLuE}}", "vAlUe")] + [TestCase("{{VaLuE }}", " vAlUe")] + public void Translation_Tokens_KeysAreNormalised(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary<string, object> { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + + /********* + ** Private methods + *********/ + /// <summary>Set a translation helper's locale and assert that it was set correctly.</summary> + /// <param name="helper">The translation helper to change.</param> + /// <param name="locale">The expected locale.</param> + /// <param name="localeEnum">The expected game language code.</param> + private void AssertSetLocale(TranslationHelper helper, string locale, LocalizedContentManager.LanguageCode localeEnum) + { + helper.SetLocale(locale, localeEnum); + Assert.AreEqual(locale, helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(localeEnum, helper.LocaleEnum, "The locale enum doesn't match the input value."); + } + + /// <summary>Get sample raw translations to input.</summary> + private IDictionary<string, IDictionary<string, string>> GetSampleData() + { + return new Dictionary<string, IDictionary<string, string>> + { + ["default"] = new Dictionary<string, string> + { + ["key A"] = "default A", + ["key C"] = "default C" + }, + ["en"] = new Dictionary<string, string> + { + ["key A"] = "en A", + ["key B"] = "en B" + }, + ["en-US"] = new Dictionary<string, string>(), + ["zzz"] = new Dictionary<string, string> + { + ["key A"] = "zzz A" + } + }; + } + + /// <summary>Get the expected translation output given <see cref="TranslationTests.GetSampleData"/>, based on the expected locale fallback.</summary> + private IDictionary<string, IDictionary<string, string>> GetExpectedTranslations() + { + return new Dictionary<string, IDictionary<string, string>> + { + ["default"] = new Dictionary<string, string> + { + ["key A"] = "default A", + ["key C"] = "default C" + }, + ["en"] = new Dictionary<string, string> + { + ["key A"] = "en A", + ["key B"] = "en B", + ["key C"] = "default C" + }, + ["en-us"] = new Dictionary<string, string> + { + ["key A"] = "en A", + ["key B"] = "en B", + ["key C"] = "default C" + }, + ["zzz"] = new Dictionary<string, string> + { + ["key A"] = "zzz A", + ["key C"] = "default C" + } + }; + } + + /// <summary>Get the default placeholder text when a translation is missing.</summary> + /// <param name="key">The translation key.</param> + private string GetPlaceholderText(string key) + { + return string.Format(Translation.PlaceholderText, key); + } + } +} diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs index ae4b833c..868ee4fa 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/StardewModdingAPI/Translation.cs @@ -11,6 +11,9 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// <summary>The placeholder text when the translation is <c>null</c> or empty, where <c>{0}</c> is the translation key.</summary> + internal const string PlaceholderText = "(no translation:{0})"; + /// <summary>The name of the relevant mod for error messages.</summary> private readonly string ModName; @@ -36,7 +39,7 @@ namespace StardewModdingAPI /// <param name="key">The translation key.</param> /// <param name="text">The underlying translation text.</param> internal Translation(string modName, string locale, string key, string text) - : this(modName, locale, key, text, $"(no translation:{key})") { } + : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } /// <summary>Construct an isntance.</summary> /// <param name="modName">The name of the relevant mod for error messages.</param> @@ -75,7 +78,7 @@ namespace StardewModdingAPI /// <param name="use">Whether to return a placeholder.</param> public Translation UsePlaceholder(bool use) { - return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? $"(no translation:{this.Key})" : null); + return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null); } /// <summary>Replace tokens in the text like <c>{{value}}</c> with the given values. Returns a new instance.</summary> diff --git a/src/crossplatform.targets b/src/crossplatform.targets index 00b731eb..31d4722d 100644 --- a/src/crossplatform.targets +++ b/src/crossplatform.targets @@ -33,7 +33,6 @@ </Reference> <Reference Include="Stardew Valley"> <HintPath>$(GamePath)\Stardew Valley.exe</HintPath> - <Private>False</Private> </Reference> <Reference Include="xTile, Version=2.0.4.0, Culture=neutral, processorArchitecture=x86"> <HintPath>$(GamePath)\xTile.dll</HintPath> |