using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using NUnit.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModHelpers;
using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewValley;
namespace SMAPI.Tests.Core
{
/// Unit tests for and .
[TestFixture]
public class TranslationTests
{
/*********
** Data
*********/
/// Sample translation text for unit tests.
public static string?[] Samples = { null, "", " ", "boop", " boop " };
/*********
** Unit tests
*********/
/****
** Translation helper
****/
[Test(Description = "Assert that the translation helper correctly handles no translations.")]
public void Helper_HandlesNoTranslations()
{
// arrange
var data = new Dictionary>();
// act
ITranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
Translation translation = helper.Get("key");
Translation[]? translationList = helper.GetTranslations()?.ToArray();
// 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(translationList, "The full list of translations is unexpectedly null.");
Assert.AreEqual(0, translationList!.Length, "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();
TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys)
{
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
actual[locale] = helper.GetTranslations()?.ToArray();
}
// 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]).Using(this.CompareEquality), $"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_Get_ReturnsExpectedText()
{
// arrange
var data = this.GetSampleData();
var expected = this.GetExpectedTranslations();
// act
var actual = new Dictionary();
TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys)
{
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
List translations = new List();
foreach (Translation translation in expected[locale])
translations.Add(helper.Get(translation.Key));
actual[locale] = translations.ToArray();
}
// 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]).Using(this.CompareEquality), $"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("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("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("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("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 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("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("anonymous object", "class", "IDictionary", "IDictionary")] string structure)
{
// 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("pt-BR", "key", input);
switch (structure)
{
case "anonymous object":
translation = translation.Tokens(new { start, middle, end });
break;
case "class":
translation = translation.Tokens(new TokenModel(start, middle, end));
break;
case "IDictionary":
translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end });
break;
case "IDictionary":
translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end });
break;
default:
throw new NotSupportedException($"Unknown structure '{structure}'.");
}
// 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("pt-BR", "key", text).Tokens(new Dictionary { [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_KeysAreNormalized(string text, string key)
{
// arrange
string value = Guid.NewGuid().ToString("N");
// act
Translation translation = new Translation("pt-BR", "key", text).Tokens(new Dictionary { [key] = value });
// assert
Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text.");
}
/*********
** Private methods
*********/
/// Set a translation helper's locale and assert that it was set correctly.
/// The translation helper to change.
/// The expected locale.
/// The expected game language code.
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.");
}
/// Get sample raw translations to input.
private IDictionary> GetSampleData()
{
return new Dictionary>
{
["default"] = new Dictionary
{
["key A"] = "default A",
["key C"] = "default C"
},
["en"] = new Dictionary
{
["key A"] = "en A",
["key B"] = "en B"
},
["en-US"] = new Dictionary(),
["zzz"] = new Dictionary
{
["key A"] = "zzz A"
}
};
}
/// Get the expected translation output given , based on the expected locale fallback.
private IDictionary GetExpectedTranslations()
{
var expected = new Dictionary
{
["default"] = new[]
{
new Translation("default", "key A", "default A"),
new Translation("default", "key C", "default C")
},
["en"] = new[]
{
new Translation("en", "key A", "en A"),
new Translation("en", "key B", "en B"),
new Translation("en", "key C", "default C")
},
["zzz"] = new[]
{
new Translation("zzz", "key A", "zzz A"),
new Translation("zzz", "key C", "default C")
}
};
expected["en-us"] = expected["en"].ToArray();
return expected;
}
/// Get whether two translations have the same public values.
/// The first translation to compare.
/// The second translation to compare.
private bool CompareEquality(Translation a, Translation b)
{
return a.Key == b.Key && a.ToString() == b.ToString();
}
/// Get the default placeholder text when a translation is missing.
/// The translation key.
private string GetPlaceholderText(string key)
{
return string.Format(Translation.PlaceholderText, key);
}
/// Create a fake mod manifest.
private IModMetadata CreateModMetadata()
{
string id = $"smapi.unit-tests.fake-mod-{Guid.NewGuid():N}";
string tempPath = Path.Combine(Path.GetTempPath(), id);
return new ModMetadata(
displayName: "Mod Display Name",
directoryPath: tempPath,
rootPath: tempPath,
manifest: new Manifest(
uniqueID: id,
name: "Mod Name",
author: "Mod Author",
description: "Mod Description",
version: new SemanticVersion(1, 0, 0)
),
dataRecord: null,
isIgnored: false
);
}
/*********
** Test models
*********/
/// A model used to test token support.
[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Used dynamically via translation helper.")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used dynamically via translation helper.")]
private class TokenModel
{
/*********
** Accessors
*********/
/// A sample token property.
public string Start { get; }
/// A sample token property.
public string Middle { get; }
/// A sample token field.
public string End;
/*********
** public methods
*********/
/// Construct an instance.
/// A sample token property.
/// A sample token field.
/// A sample token property.
public TokenModel(string start, string middle, string end)
{
this.Start = start;
this.Middle = middle;
this.End = end;
}
}
}
}