diff --git a/EmbyImageLanguageFallback.Tests/EmbyImageLanguageFallback.Tests.csproj b/EmbyImageLanguageFallback.Tests/EmbyImageLanguageFallback.Tests.csproj
new file mode 100644
index 0000000..c13fc11
--- /dev/null
+++ b/EmbyImageLanguageFallback.Tests/EmbyImageLanguageFallback.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+ SYSLIB0050
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/EmbyImageLanguageFallback.Tests/FallbackSelectionTests.cs b/EmbyImageLanguageFallback.Tests/FallbackSelectionTests.cs
new file mode 100644
index 0000000..7dcd75a
--- /dev/null
+++ b/EmbyImageLanguageFallback.Tests/FallbackSelectionTests.cs
@@ -0,0 +1,766 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using Xunit;
+
+namespace EmbyImageLanguageFallback.Tests
+{
+ ///
+ /// Regression tests for the image language fallback selection logic in
+ /// . These tests invoke the
+ /// private BuildFallbackSelection, RewriteSelectedFallbackImages,
+ /// and related helper methods via reflection so that the production surface
+ /// remains unchanged.
+ ///
+ public class FallbackSelectionTests
+ {
+ private readonly object _provider;
+ private readonly MethodInfo _buildFallbackSelection;
+ private readonly MethodInfo _rewriteSelectedFallbackImages;
+ private readonly MethodInfo _normalizeLanguage;
+ private readonly MethodInfo _isLanguageNeutral;
+ private readonly MethodInfo _languageMatches;
+
+ public FallbackSelectionTests()
+ {
+ var providerType = typeof(FallbackRemoteImageProvider);
+
+ // Create an instance without calling the constructor (avoids Emby DI dependencies).
+ _provider = FormatterServices.GetUninitializedObject(providerType);
+
+ _buildFallbackSelection = providerType.GetMethod(
+ "BuildFallbackSelection",
+ BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ _rewriteSelectedFallbackImages = providerType.GetMethod(
+ "RewriteSelectedFallbackImages",
+ BindingFlags.NonPublic | BindingFlags.Instance,
+ null,
+ new[] { typeof(IEnumerable), typeof(IEnumerable) },
+ null)!;
+
+ _normalizeLanguage = providerType.GetMethod(
+ "NormalizeLanguage",
+ BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ _isLanguageNeutral = providerType.GetMethod(
+ "IsLanguageNeutral",
+ BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ _languageMatches = providerType.GetMethod(
+ "LanguageMatches",
+ BindingFlags.NonPublic | BindingFlags.Instance)!;
+ }
+
+ #region Helpers
+
+ private List InvokeBuildFallbackSelection(
+ List images,
+ IReadOnlyCollection primaryLanguages,
+ IReadOnlyCollection fallbackLanguages,
+ bool includeNeutral)
+ {
+ return (List)_buildFallbackSelection.Invoke(
+ _provider,
+ new object[] { images, primaryLanguages, fallbackLanguages, includeNeutral })!;
+ }
+
+ private List InvokeRewriteSelectedFallbackImages(
+ IEnumerable images,
+ IEnumerable fallbackLanguages)
+ {
+ return (List)_rewriteSelectedFallbackImages.Invoke(
+ _provider,
+ new object[] { images, fallbackLanguages })!;
+ }
+
+ private string? InvokeNormalizeLanguage(string? language)
+ {
+ return (string?)_normalizeLanguage.Invoke(_provider, new object?[] { language });
+ }
+
+ private bool InvokeIsLanguageNeutral(RemoteImageInfo image)
+ {
+ return (bool)_isLanguageNeutral.Invoke(_provider, new object[] { image })!;
+ }
+
+ private bool InvokeLanguageMatches(RemoteImageInfo image, string language)
+ {
+ return (bool)_languageMatches.Invoke(_provider, new object[] { image, language })!;
+ }
+
+ private static RemoteImageInfo MakeImage(string url, string? language, ImageType type = ImageType.Primary, string? displayLanguage = null)
+ {
+ return new RemoteImageInfo
+ {
+ Url = url,
+ Language = language,
+ Type = type,
+ DisplayLanguage = displayLanguage
+ };
+ }
+
+ #endregion
+
+ // ──────────────────────────────────────────────
+ // BuildFallbackSelection — core regression tests
+ // ──────────────────────────────────────────────
+
+ [Fact]
+ public void PrimaryLanguageImages_AreSelectedFirst()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/zh1", "zh"),
+ MakeImage("http://img/en1", "en"),
+ MakeImage("http://img/ja1", "ja"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "en" },
+ includeNeutral: false);
+
+ Assert.Equal(2, result.Count);
+ Assert.Equal("http://img/zh1", result[0].Url);
+ Assert.Equal("http://img/en1", result[1].Url);
+ }
+
+ [Fact]
+ public void FallbackLanguageImages_UsedWhenPrimaryMissing()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/en1", "en"),
+ MakeImage("http://img/ja1", "ja"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: false);
+
+ Assert.Single(result);
+ Assert.Equal("http://img/ja1", result[0].Url);
+ }
+
+ [Fact]
+ public void NeutralImages_UsedWhenPrimaryAndFallbackMissing()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/neutral1", null),
+ MakeImage("http://img/de1", "de"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Single(result);
+ Assert.Equal("http://img/neutral1", result[0].Url);
+ }
+
+ [Fact]
+ public void NeutralImages_ExcludedWhenDisabled()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/neutral1", null),
+ MakeImage("http://img/de1", "de"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: false);
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void FallbackOrder_PrimaryThenFallbackThenNeutral()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/neutral", null),
+ MakeImage("http://img/ja1", "ja"),
+ MakeImage("http://img/zh1", "zh"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Equal(3, result.Count);
+ Assert.Equal("http://img/zh1", result[0].Url);
+ Assert.Equal("http://img/ja1", result[1].Url);
+ Assert.Equal("http://img/neutral", result[2].Url);
+ }
+
+ [Fact]
+ public void DuplicateUrls_AreDeduplicatedAcrossTiers()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/same", "zh"),
+ MakeImage("http://img/same", "ja"),
+ MakeImage("http://img/same", null),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Single(result);
+ Assert.Equal("zh", result[0].Language);
+ }
+
+ [Fact]
+ public void MultiplePrimaryLanguages_AllMatched()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/zh1", "zh"),
+ MakeImage("http://img/ko1", "ko"),
+ MakeImage("http://img/en1", "en"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh", "ko" },
+ fallbackLanguages: new[] { "en" },
+ includeNeutral: false);
+
+ Assert.Equal(3, result.Count);
+ Assert.Contains(result, r => r.Url == "http://img/zh1");
+ Assert.Contains(result, r => r.Url == "http://img/ko1");
+ Assert.Contains(result, r => r.Url == "http://img/en1");
+ }
+
+ [Fact]
+ public void MultipleFallbackLanguages_AllMatched()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/en1", "en"),
+ MakeImage("http://img/ja1", "ja"),
+ MakeImage("http://img/ko1", "ko"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "en", "ja" },
+ includeNeutral: false);
+
+ Assert.Equal(2, result.Count);
+ Assert.Contains(result, r => r.Url == "http://img/en1");
+ Assert.Contains(result, r => r.Url == "http://img/ja1");
+ }
+
+ [Fact]
+ public void FallbackLanguages_ExcludesPrimaryOverlap()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/zh1", "zh"),
+ MakeImage("http://img/zh2", "zh-CN"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "zh", "ja" },
+ includeNeutral: false);
+
+ // "zh" in fallback should be excluded since it's already a primary language.
+ // Both zh images should come via primary match, not duplicated.
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
+ public void EmptyPrimaryAndFallback_OnlyNeutralReturned()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/en1", "en"),
+ MakeImage("http://img/neutral", null),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: Array.Empty(),
+ fallbackLanguages: Array.Empty(),
+ includeNeutral: true);
+
+ Assert.Single(result);
+ Assert.Equal("http://img/neutral", result[0].Url);
+ }
+
+ [Fact]
+ public void EmptyPrimaryAndFallback_NeutralDisabled_ReturnsEmpty()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/en1", "en"),
+ MakeImage("http://img/neutral", null),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: Array.Empty(),
+ fallbackLanguages: Array.Empty(),
+ includeNeutral: false);
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void NoImagesProvided_ReturnsEmpty()
+ {
+ var result = InvokeBuildFallbackSelection(
+ new List(),
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void UnrelatedLanguages_Excluded()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/de1", "de"),
+ MakeImage("http://img/fr1", "fr"),
+ MakeImage("http://img/es1", "es"),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: false);
+
+ Assert.Empty(result);
+ }
+
+ // ──────────────────────────────────────────────
+ // Language alias / variant matching
+ // ──────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("zh-CN")]
+ [InlineData("zh_CN")]
+ [InlineData("zh")]
+ [InlineData("zho")]
+ [InlineData("chi")]
+ [InlineData("chinese")]
+ public void ChineseAliases_MatchPrimaryZh(string imageLanguage)
+ {
+ var images = new List
+ {
+ MakeImage("http://img/1", imageLanguage),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: Array.Empty(),
+ includeNeutral: false);
+
+ Assert.Single(result);
+ }
+
+ [Theory]
+ [InlineData("ja")]
+ [InlineData("ja-JP")]
+ [InlineData("ja_JP")]
+ [InlineData("jpn")]
+ [InlineData("japanese")]
+ public void JapaneseAliases_MatchFallbackJa(string imageLanguage)
+ {
+ var images = new List
+ {
+ MakeImage("http://img/1", imageLanguage),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: false);
+
+ Assert.Single(result);
+ }
+
+ [Theory]
+ [InlineData("en")]
+ [InlineData("en-US")]
+ [InlineData("en_US")]
+ [InlineData("eng")]
+ [InlineData("english")]
+ public void EnglishAliases_MatchFallbackEn(string imageLanguage)
+ {
+ var images = new List
+ {
+ MakeImage("http://img/1", imageLanguage),
+ };
+
+ var result = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "en" },
+ includeNeutral: false);
+
+ Assert.Single(result);
+ }
+
+ // ──────────────────────────────────────────────
+ // IsLanguageNeutral
+ // ──────────────────────────────────────────────
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("null")]
+ [InlineData("00")]
+ public void IsLanguageNeutral_TrueForNeutralValues(string? language)
+ {
+ var image = MakeImage("http://img/1", language);
+ Assert.True(InvokeIsLanguageNeutral(image));
+ }
+
+ [Theory]
+ [InlineData("zh")]
+ [InlineData("en")]
+ [InlineData("ja")]
+ [InlineData("de")]
+ public void IsLanguageNeutral_FalseForRealLanguages(string language)
+ {
+ var image = MakeImage("http://img/1", language);
+ Assert.False(InvokeIsLanguageNeutral(image));
+ }
+
+ // ──────────────────────────────────────────────
+ // NormalizeLanguage
+ // ──────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("zh-CN", "zh-Hans")]
+ [InlineData("zh_CN", "zh-Hans")]
+ [InlineData("chinese", "zh-Hans")]
+ [InlineData("ja-JP", "ja")]
+ [InlineData("jpn", "ja")]
+ [InlineData("eng", "en")]
+ [InlineData("en-US", "en")]
+ public void NormalizeLanguage_MapsToCanonical(string input, string expected)
+ {
+ Assert.Equal(expected, InvokeNormalizeLanguage(input));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void NormalizeLanguage_NullOrWhitespace_ReturnsNull(string? input)
+ {
+ Assert.Null(InvokeNormalizeLanguage(input));
+ }
+
+ // ──────────────────────────────────────────────
+ // RewriteSelectedFallbackImages
+ // ──────────────────────────────────────────────
+
+ [Fact]
+ public void RewriteFallback_ChangesLanguageToEnglish()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/ja1", "ja"),
+ };
+
+ var result = InvokeRewriteSelectedFallbackImages(images, new[] { "ja" });
+
+ Assert.Single(result);
+ Assert.Equal("en", result[0].Language);
+ Assert.Equal("English", result[0].DisplayLanguage);
+ }
+
+ [Fact]
+ public void RewriteFallback_DoesNotChangePrimaryLanguageImages()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/zh1", "zh"),
+ };
+
+ var result = InvokeRewriteSelectedFallbackImages(images, new[] { "ja" });
+
+ Assert.Single(result);
+ Assert.Equal("zh", result[0].Language);
+ }
+
+ [Fact]
+ public void RewriteFallback_PreservesOriginalUrl()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/ja1", "ja"),
+ };
+
+ var result = InvokeRewriteSelectedFallbackImages(images, new[] { "ja" });
+
+ Assert.Equal("http://img/ja1", result[0].Url);
+ }
+
+ [Fact]
+ public void RewriteFallback_MixedLanguages_OnlyFallbackRewritten()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/zh1", "zh"),
+ MakeImage("http://img/ja1", "ja"),
+ MakeImage("http://img/neutral", null),
+ };
+
+ var result = InvokeRewriteSelectedFallbackImages(images, new[] { "ja" });
+
+ Assert.Equal(3, result.Count);
+ Assert.Equal("zh", result[0].Language);
+ Assert.Equal("en", result[1].Language);
+ Assert.Null(result[2].Language);
+ }
+
+ // ──────────────────────────────────────────────
+ // Full fallback chain regression tests
+ // ──────────────────────────────────────────────
+
+ [Fact]
+ public void FullChain_PrimaryAvailable_NoFallbackUsed()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/zh1", "zh", ImageType.Primary),
+ MakeImage("http://img/ja1", "ja", ImageType.Primary),
+ MakeImage("http://img/en1", "en", ImageType.Primary),
+ MakeImage("http://img/neutral", null, ImageType.Primary),
+ };
+
+ var selected = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ // Primary match comes first, then fallback, then neutral
+ Assert.True(selected.Count >= 1);
+ Assert.Equal("http://img/zh1", selected[0].Url);
+ }
+
+ [Fact]
+ public void FullChain_PrimaryMissing_FallbackUsed()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/ja1", "ja", ImageType.Primary),
+ MakeImage("http://img/en1", "en", ImageType.Primary),
+ MakeImage("http://img/neutral", null, ImageType.Primary),
+ };
+
+ var selected = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Equal(2, selected.Count);
+ Assert.Equal("http://img/ja1", selected[0].Url);
+ Assert.Equal("http://img/neutral", selected[1].Url);
+ }
+
+ [Fact]
+ public void FullChain_PrimaryAndFallbackMissing_NeutralUsed()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/en1", "en", ImageType.Primary),
+ MakeImage("http://img/de1", "de", ImageType.Primary),
+ MakeImage("http://img/neutral", null, ImageType.Primary),
+ };
+
+ var selected = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Single(selected);
+ Assert.Equal("http://img/neutral", selected[0].Url);
+ }
+
+ [Fact]
+ public void FullChain_AllMissing_NeutralDisabled_ReturnsEmpty()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/en1", "en", ImageType.Primary),
+ MakeImage("http://img/de1", "de", ImageType.Primary),
+ MakeImage("http://img/neutral", null, ImageType.Primary),
+ };
+
+ var selected = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: false);
+
+ Assert.Empty(selected);
+ }
+
+ [Fact]
+ public void FullChain_MultipleImageTypes_EachTypeHandledIndependently()
+ {
+ // The BuildFallbackSelection method processes all images regardless of
+ // type. The per-type grouping happens in GetImages. Verify that images
+ // of different types coexist.
+ var images = new List
+ {
+ MakeImage("http://img/poster-zh", "zh", ImageType.Primary),
+ MakeImage("http://img/banner-ja", "ja", ImageType.Banner),
+ MakeImage("http://img/thumb-neutral", null, ImageType.Thumb),
+ };
+
+ var selected = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Equal(3, selected.Count);
+ Assert.Contains(selected, r => r.Url == "http://img/poster-zh");
+ Assert.Contains(selected, r => r.Url == "http://img/banner-ja");
+ Assert.Contains(selected, r => r.Url == "http://img/thumb-neutral");
+ }
+
+ [Fact]
+ public void FullChain_BackdropImages_FallbackChainWorks()
+ {
+ var images = new List
+ {
+ MakeImage("http://img/backdrop-en", "en", ImageType.Backdrop),
+ MakeImage("http://img/backdrop-ja", "ja", ImageType.Backdrop),
+ MakeImage("http://img/backdrop-neutral", null, ImageType.Backdrop),
+ };
+
+ var selected = InvokeBuildFallbackSelection(
+ images,
+ primaryLanguages: new[] { "zh" },
+ fallbackLanguages: new[] { "ja" },
+ includeNeutral: true);
+
+ Assert.Equal(2, selected.Count);
+ Assert.Equal("http://img/backdrop-ja", selected[0].Url);
+ Assert.Equal("http://img/backdrop-neutral", selected[1].Url);
+ }
+
+ // ──────────────────────────────────────────────
+ // LanguageMatches regression tests
+ // ──────────────────────────────────────────────
+
+ [Fact]
+ public void LanguageMatches_ZhCN_MatchesZh()
+ {
+ var image = MakeImage("http://img/1", "zh-CN");
+ Assert.True(InvokeLanguageMatches(image, "zh"));
+ }
+
+ [Fact]
+ public void LanguageMatches_CaseInsensitive()
+ {
+ var image = MakeImage("http://img/1", "ZH-CN");
+ Assert.True(InvokeLanguageMatches(image, "zh"));
+ }
+
+ [Fact]
+ public void LanguageMatches_DisplayLanguageFallback_NonNormalizableText()
+ {
+ // Display language text that is NOT normalizable (so it survives NormalizeLanguage)
+ // but still contains the keyword used in DisplayLanguageMatches.
+ var image = MakeImage("http://img/1", "unknown", displayLanguage: "Japanese (Standard)");
+ Assert.True(InvokeLanguageMatches(image, "ja"));
+ }
+
+ [Fact]
+ public void LanguageMatches_DisplayLanguage_NormalizedAway_DoesNotMatch()
+ {
+ // "Japanese" normalizes to "ja", so the substring check in DisplayLanguageMatches
+ // cannot find "japanese" in the normalized value "ja". This is expected behavior.
+ var image = MakeImage("http://img/1", "unknown", displayLanguage: "Japanese");
+ Assert.False(InvokeLanguageMatches(image, "ja"));
+ }
+
+ [Fact]
+ public void LanguageMatches_NoMatch()
+ {
+ var image = MakeImage("http://img/1", "de");
+ Assert.False(InvokeLanguageMatches(image, "ja"));
+ }
+
+ // ──────────────────────────────────────────────
+ // PluginOptions tests
+ // ──────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("zh-CN", "zh-Hans")]
+ [InlineData("zh_CN", "zh-Hans")]
+ [InlineData("ZH-CN", "zh-Hans")]
+ [InlineData("chinese", "zh-Hans")]
+ [InlineData("jpn", "ja")]
+ [InlineData("eng", "en")]
+ [InlineData("kor", "ko")]
+ [InlineData("unknown-lang", "unknown-lang")]
+ public void GetCanonicalLanguageValue_ResolvesCorrectly(string input, string expected)
+ {
+ Assert.Equal(expected, PluginOptions.GetCanonicalLanguageValue(input));
+ }
+
+ [Fact]
+ public void GetCanonicalLanguageValue_NullInput_ReturnsNull()
+ {
+ Assert.Null(PluginOptions.GetCanonicalLanguageValue(null));
+ }
+
+ [Fact]
+ public void GetLanguageAliases_ReturnsAllAliasesForZh()
+ {
+ var aliases = PluginOptions.GetLanguageAliases("zh");
+ Assert.Contains("zh-Hans", aliases, StringComparer.OrdinalIgnoreCase);
+ Assert.Contains("zh", aliases, StringComparer.OrdinalIgnoreCase);
+ Assert.Contains("zh-CN", aliases, StringComparer.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void GetLanguageAliases_UnknownLanguage_ReturnsSingleAlias()
+ {
+ var aliases = PluginOptions.GetLanguageAliases("xx-custom");
+ Assert.Single(aliases);
+ Assert.Equal("xx-custom", aliases.First());
+ }
+ }
+}
diff --git a/EmbyImageLanguageFallback.Tests/GlobalUsings.cs b/EmbyImageLanguageFallback.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/EmbyImageLanguageFallback.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/EmbyImageLanguageFallback.csproj b/EmbyImageLanguageFallback.csproj
index fc52dc9..449f663 100644
--- a/EmbyImageLanguageFallback.csproj
+++ b/EmbyImageLanguageFallback.csproj
@@ -2,6 +2,7 @@
netstandard2.0
+ $(DefaultItemExcludes);EmbyImageLanguageFallback.Tests\**
diff --git a/EmbyImageLanguageFallback.sln b/EmbyImageLanguageFallback.sln
index bc5feb0..7e571e4 100644
--- a/EmbyImageLanguageFallback.sln
+++ b/EmbyImageLanguageFallback.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 18.3.11520.95 d18.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbyImageLanguageFallback", "EmbyImageLanguageFallback.csproj", "{B012E32B-0643-1CD5-1A8C-B23BF9340CAD}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbyImageLanguageFallback.Tests", "EmbyImageLanguageFallback.Tests\EmbyImageLanguageFallback.Tests.csproj", "{C7216891-843E-4A93-87A9-72627B183DA4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{B012E32B-0643-1CD5-1A8C-B23BF9340CAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B012E32B-0643-1CD5-1A8C-B23BF9340CAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B012E32B-0643-1CD5-1A8C-B23BF9340CAD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C7216891-843E-4A93-87A9-72627B183DA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C7216891-843E-4A93-87A9-72627B183DA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C7216891-843E-4A93-87A9-72627B183DA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C7216891-843E-4A93-87A9-72627B183DA4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE