summaryrefslogtreecommitdiff
path: root/src/SMAPI.Tests/Core/TranslationTests.cs
blob: ced1525acf917044a35a31bfafd0ab04ca7d2b2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NUnit.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Framework.ModHelpers;
using StardewValley;

namespace SMAPI.Tests.Core
{
    /// <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  " };


        /*********
        ** 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("ModID", "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<string, Translation[]?>();
            TranslationHelper helper = new TranslationHelper("ModID", "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<Translation, Translation>(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<string, Translation[]>();
            TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
            foreach (string locale in expected.Keys)
            {
                this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);

                List<Translation> translations = new List<Translation>();
                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<Translation, Translation>(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<string, object>", "IDictionary<string, string>")] 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<string, object>":
                    translation = translation.Tokens(new Dictionary<string, object> { ["start"] = start, ["middle"] = middle, ["end"] = end });
                    break;

                case "IDictionary<string, string>":
                    translation = translation.Tokens(new Dictionary<string, string> { ["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<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_KeysAreNormalized(string text, string key)
        {
            // arrange
            string value = Guid.NewGuid().ToString("N");

            // act
            Translation translation = new Translation("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, Translation[]> GetExpectedTranslations()
        {
            var expected = new Dictionary<string, Translation[]>
            {
                ["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;
        }

        /// <summary>Get whether two translations have the same public values.</summary>
        /// <param name="a">The first translation to compare.</param>
        /// <param name="b">The second translation to compare.</param>
        private bool CompareEquality(Translation a, Translation b)
        {
            return a.Key == b.Key && a.ToString() == b.ToString();
        }

        /// <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);
        }


        /*********
        ** Test models
        *********/
        /// <summary>A model used to test token support.</summary>
        [SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Used dynamically via translation helper.")]
        [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used dynamically via translation helper.")]
        private class TokenModel
        {
            /*********
            ** Accessors
            *********/
            /// <summary>A sample token property.</summary>
            public string Start { get; }

            /// <summary>A sample token property.</summary>
            public string Middle { get; }

            /// <summary>A sample token field.</summary>
            public string End;


            /*********
            ** public methods
            *********/
            /// <summary>Construct an instance.</summary>
            /// <param name="start">A sample token property.</param>
            /// <param name="middle">A sample token field.</param>
            /// <param name="end">A sample token property.</param>
            public TokenModel(string start, string middle, string end)
            {
                this.Start = start;
                this.Middle = middle;
                this.End = end;
            }
        }
    }
}