diff --git a/samples/LocalizationSample.Blazor.Client/Program.cs b/samples/LocalizationSample.Blazor.Client/Program.cs index 31d5145..cdd0c0a 100644 --- a/samples/LocalizationSample.Blazor.Client/Program.cs +++ b/samples/LocalizationSample.Blazor.Client/Program.cs @@ -14,7 +14,7 @@ public static async Task Main(string[] args) builder.RootComponents.Add("app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); - builder.Services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + builder.Services.AddJsonLocalization(options => options.ResourcesPath = new[] { "Resources" }); await builder.Build().RunAsync(); } diff --git a/samples/LocalizationSample.Blazor.Server/Startup.cs b/samples/LocalizationSample.Blazor.Server/Startup.cs index 3b7cd0f..51efd7a 100644 --- a/samples/LocalizationSample.Blazor.Server/Startup.cs +++ b/samples/LocalizationSample.Blazor.Server/Startup.cs @@ -28,7 +28,7 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("fr-FR"); - services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + services.AddJsonLocalization(options => options.ResourcesPath = new[] { "Resources" }); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton(); diff --git a/samples/LocalizationSample.Mvc/Startup.cs b/samples/LocalizationSample.Mvc/Startup.cs index b1b35e5..0db9a41 100644 --- a/samples/LocalizationSample.Mvc/Startup.cs +++ b/samples/LocalizationSample.Mvc/Startup.cs @@ -12,7 +12,7 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); - services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + services.AddJsonLocalization(options => options.ResourcesPath = new[] { "Resources" }); services.AddMvc() .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix) .AddDataAnnotationsLocalization(options => diff --git a/samples/LocalizationSample/Startup.cs b/samples/LocalizationSample/Startup.cs index fe01557..084abbe 100644 --- a/samples/LocalizationSample/Startup.cs +++ b/samples/LocalizationSample/Startup.cs @@ -14,7 +14,7 @@ public class Startup { public void ConfigureServices(IServiceCollection services) { - services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + services.AddJsonLocalization(options => options.ResourcesPath = new[] { "Resources" }); } public void Configure(IApplicationBuilder app, IHostEnvironment env, IStringLocalizer localizer1, IStringLocalizer localizer2) diff --git a/src/My.Extensions.Localization.Json/Internal/JsonResourceManager.cs b/src/My.Extensions.Localization.Json/Internal/JsonResourceManager.cs index 5d016c8..636ec10 100644 --- a/src/My.Extensions.Localization.Json/Internal/JsonResourceManager.cs +++ b/src/My.Extensions.Localization.Json/Internal/JsonResourceManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -8,22 +9,30 @@ namespace My.Extensions.Localization.Json.Internal; public class JsonResourceManager { - private readonly JsonFileWatcher _jsonFileWatcher; + private readonly List _jsonFileWatchers = new(); private readonly ConcurrentDictionary> _resourcesCache = new(); + private readonly ConcurrentDictionary> _loadedFilesCache = new(); - public JsonResourceManager(string resourcesPath, string resourceName = null) + public JsonResourceManager(string[] resourcesPaths, string resourceName = null) { - ResourcesPath = resourcesPath; + ResourcesPaths = resourcesPaths ?? Array.Empty(); ResourceName = resourceName; - _jsonFileWatcher = new(resourcesPath); - _jsonFileWatcher.Changed += RefreshResourcesCache; + foreach (var path in ResourcesPaths) + { + SetupFileWatcher(path); + } + } + + public JsonResourceManager(string[] resourcesPaths) + : this(resourcesPaths, null) + { } public string ResourceName { get; } - public string ResourcesPath { get; } + public string[] ResourcesPaths { get; } public string ResourcesFilePath { get; private set; } @@ -111,12 +120,21 @@ public virtual string GetString(string name, CultureInfo culture) } private void TryLoadResourceSet(CultureInfo culture) + { + // Load from all resources paths (merging resources, first path takes precedence) + foreach (var path in ResourcesPaths) + { + TryLoadResourceSetFromPath(path, culture); + } + } + + private void TryLoadResourceSetFromPath(string basePath, CultureInfo culture) { if (string.IsNullOrEmpty(ResourceName)) { - var file = Path.Combine(ResourcesPath, $"{culture.Name}.json"); + var file = Path.Combine(basePath, $"{culture.Name}.json"); - TryAddResources(file); + TryAddResources(file, culture); } else { @@ -124,16 +142,19 @@ private void TryLoadResourceSet(CultureInfo culture) var rootCulture = culture.Name[..2]; if (ResourceName.Contains('.')) { - resourceFiles = Directory.EnumerateFiles(ResourcesPath, $"{ResourceName}.{rootCulture}*.json"); + if (Directory.Exists(basePath)) + { + resourceFiles = Directory.EnumerateFiles(basePath, $"{ResourceName}.{rootCulture}*.json"); + } if (!resourceFiles.Any()) { - resourceFiles = GetResourceFiles(rootCulture); + resourceFiles = GetResourceFiles(basePath, rootCulture); } } else { - resourceFiles = GetResourceFiles(rootCulture); + resourceFiles = GetResourceFiles(basePath, rootCulture); } foreach (var file in resourceFiles) @@ -141,38 +162,70 @@ private void TryLoadResourceSet(CultureInfo culture) var fileName = Path.GetFileNameWithoutExtension(file); var cultureName = fileName[(fileName.LastIndexOf('.') + 1)..]; - culture = CultureInfo.GetCultureInfo(cultureName); + var fileCulture = CultureInfo.GetCultureInfo(cultureName); - TryAddResources(file); + TryAddResources(file, fileCulture); } } - IEnumerable GetResourceFiles(string culture) + IEnumerable GetResourceFiles(string baseResourcesPath, string cultureName) { var resourcePath = ResourceName.Replace('.', Path.AltDirectorySeparatorChar); var resourcePathLastDirectorySeparatorIndex = resourcePath.LastIndexOf(Path.AltDirectorySeparatorChar); var resourceName = resourcePath[(resourcePathLastDirectorySeparatorIndex + 1)..]; var resourcesPath = resourcePathLastDirectorySeparatorIndex == -1 - ? ResourcesPath - : Path.Combine(ResourcesPath, resourcePath[..resourcePathLastDirectorySeparatorIndex]); + ? baseResourcesPath + : Path.Combine(baseResourcesPath, resourcePath[..resourcePathLastDirectorySeparatorIndex]); return Directory.Exists(resourcesPath) - ? Directory.EnumerateFiles(resourcesPath, $"{resourceName}.{culture}*.json") + ? Directory.EnumerateFiles(resourcesPath, $"{resourceName}.{cultureName}*.json") : []; } - void TryAddResources(string resourceFile) + void TryAddResources(string resourceFile, CultureInfo resourceCulture) { - var key = $"{ResourceName}.{culture.Name}"; + var key = $"{ResourceName}.{resourceCulture.Name}"; + + // Track loaded files to avoid re-loading + var loadedFiles = _loadedFilesCache.GetOrAdd(key, _ => new HashSet()); + if (!loadedFiles.Add(resourceFile)) + { + // File already loaded for this key, skip + return; + } + if (!_resourcesCache.ContainsKey(key)) { var resources = JsonResourceLoader.Load(resourceFile); _resourcesCache.TryAdd(key, new ConcurrentDictionary(resources)); } + else + { + // Merge resources from additional paths (don't override existing keys) + var existingResources = _resourcesCache[key]; + var additionalResources = JsonResourceLoader.Load(resourceFile); + + foreach (var item in additionalResources) + { + existingResources.TryAdd(item.Key, item.Value); + } + } } } + private void SetupFileWatcher(string path) + { + if (!Directory.Exists(path)) + { + return; + } + + var watcher = new JsonFileWatcher(path); + watcher.Changed += RefreshResourcesCache; + _jsonFileWatchers.Add(watcher); + } + private void RefreshResourcesCache(object sender, FileSystemEventArgs e) { var key = Path.GetFileNameWithoutExtension(e.FullPath); diff --git a/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs b/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs index 4eb1cae..b30d68a 100644 --- a/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs +++ b/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs @@ -1,8 +1,11 @@ -using Microsoft.Extensions.Localization; +using System; +using Microsoft.Extensions.Localization; namespace My.Extensions.Localization.Json; public class JsonLocalizationOptions : LocalizationOptions { public ResourcesType ResourcesType { get; set; } = ResourcesType.TypeBased; + + public new string[] ResourcesPath { get; set; } = Array.Empty(); } \ No newline at end of file diff --git a/src/My.Extensions.Localization.Json/JsonStringLocalizerFactory.cs b/src/My.Extensions.Localization.Json/JsonStringLocalizerFactory.cs index 9c9e4c4..bf0c58a 100644 --- a/src/My.Extensions.Localization.Json/JsonStringLocalizerFactory.cs +++ b/src/My.Extensions.Localization.Json/JsonStringLocalizerFactory.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; @@ -16,7 +17,7 @@ public class JsonStringLocalizerFactory : IStringLocalizerFactory { private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache(); private readonly ConcurrentDictionary _localizerCache = new(); - private readonly string _resourcesRelativePath; + private readonly string[] _resourcesPaths; private readonly ResourcesType _resourcesType = ResourcesType.TypeBased; private readonly ILoggerFactory _loggerFactory; @@ -26,7 +27,7 @@ public JsonStringLocalizerFactory( { ArgumentNullException.ThrowIfNull(localizationOptions); - _resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty; + _resourcesPaths = localizationOptions.Value.ResourcesPath ?? Array.Empty(); _resourcesType = localizationOptions.Value.ResourcesType; _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } @@ -35,14 +36,12 @@ public IStringLocalizer Create(Type resourceSource) { ArgumentNullException.ThrowIfNull(resourceSource); - string resourcesPath = string.Empty; - // TODO: Check why an exception happen before the host build if (resourceSource.Name == "Controller") { - resourcesPath = Path.Combine(PathHelpers.GetApplicationRoot(), GetResourcePath(resourceSource.Assembly)); + var resourcesPaths = GetResourcePaths(resourceSource.Assembly); - return _localizerCache.GetOrAdd(resourceSource.Name, _ => CreateJsonStringLocalizer(resourcesPath, TryFixInnerClassPath("Controller"))); + return _localizerCache.GetOrAdd(resourceSource.Name, _ => CreateJsonStringLocalizer(resourcesPaths, TryFixInnerClassPath("Controller"))); } var typeInfo = resourceSource.GetTypeInfo(); @@ -52,10 +51,10 @@ public IStringLocalizer Create(Type resourceSource) ? typeInfo.Name : TrimPrefix(typeInfo.FullName, rootNamespace + "."); - resourcesPath = Path.Combine(PathHelpers.GetApplicationRoot(), GetResourcePath(assembly)); + var paths = GetResourcePaths(assembly); typeName = TryFixInnerClassPath(typeName); - return _localizerCache.GetOrAdd($"culture={CultureInfo.CurrentUICulture.Name}, typeName={typeName}", _ => CreateJsonStringLocalizer(resourcesPath, typeName)); + return _localizerCache.GetOrAdd($"culture={CultureInfo.CurrentUICulture.Name}, typeName={typeName}", _ => CreateJsonStringLocalizer(paths, typeName)); } public IStringLocalizer Create(string baseName, string location) @@ -67,13 +66,13 @@ public IStringLocalizer Create(string baseName, string location) { var assemblyName = new AssemblyName(location); var assembly = Assembly.Load(assemblyName); - var resourcesPath = Path.Combine(PathHelpers.GetApplicationRoot(), GetResourcePath(assembly)); + var resourcesPaths = GetResourcePaths(assembly); string resourceName = null; if (baseName == string.Empty) { resourceName = baseName; - return CreateJsonStringLocalizer(resourcesPath, resourceName); + return CreateJsonStringLocalizer(resourcesPaths, resourceName); } if (_resourcesType == ResourcesType.TypeBased) @@ -83,29 +82,34 @@ public IStringLocalizer Create(string baseName, string location) resourceName = TrimPrefix(baseName, rootNamespace + "."); } - return CreateJsonStringLocalizer(resourcesPath, resourceName); + return CreateJsonStringLocalizer(resourcesPaths, resourceName); }); } protected virtual JsonStringLocalizer CreateJsonStringLocalizer( - string resourcesPath, + string[] resourcesPaths, string resourceName) { var resourceManager = _resourcesType == ResourcesType.TypeBased - ? new JsonResourceManager(resourcesPath, resourceName) - : new JsonResourceManager(resourcesPath); + ? new JsonResourceManager(resourcesPaths, resourceName) + : new JsonResourceManager(resourcesPaths); var logger = _loggerFactory.CreateLogger(); return new JsonStringLocalizer(resourceManager, _resourceNamesCache, logger); } - private string GetResourcePath(Assembly assembly) + private string[] GetResourcePaths(Assembly assembly) { var resourceLocationAttribute = assembly.GetCustomAttribute(); + + if (resourceLocationAttribute != null) + { + return new[] { Path.Combine(PathHelpers.GetApplicationRoot(), resourceLocationAttribute.ResourceLocation) }; + } - return resourceLocationAttribute == null - ? _resourcesRelativePath - : resourceLocationAttribute.ResourceLocation; + return _resourcesPaths + .Select(p => Path.Combine(PathHelpers.GetApplicationRoot(), p)) + .ToArray(); } private static string GetRootNamespace(Assembly assembly) diff --git a/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmark.cs b/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmark.cs index ab7fa1e..18cd1fc 100644 --- a/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmark.cs +++ b/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmark.cs @@ -13,7 +13,7 @@ public class JsonResourceManagerBenchmark static JsonResourceManagerBenchmark() { - var resources = "Resources"; + var resources = new[] { "Resources" }; _jsonResourceManager = new JsonResourceManager(resources, Path.Combine("fr-FR.json")); _frenchCulture = CultureInfo.GetCultureInfo("fr-FR"); } diff --git a/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmarks.cs b/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmarks.cs index 0fa6296..a53c13c 100644 --- a/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmarks.cs +++ b/test/My.Extensions.Localization.Json.Benchmarks/JsonResourceManagerBenchmarks.cs @@ -15,7 +15,7 @@ public class JsonResourceManagerBenchmarks static JsonResourceManagerBenchmarks() { - _jsonResourceManager = new JsonResourceManager("Resources\\fr-FR.json"); + _jsonResourceManager = new JsonResourceManager(new[] { "Resources\\fr-FR.json" }); _frenchCulture = CultureInfo.GetCultureInfo("fr-FR"); } diff --git a/test/My.Extensions.Localization.Json.Tests/AdditionalResources/Common/Test.fr-FR.json b/test/My.Extensions.Localization.Json.Tests/AdditionalResources/Common/Test.fr-FR.json new file mode 100644 index 0000000..5572aae --- /dev/null +++ b/test/My.Extensions.Localization.Json.Tests/AdditionalResources/Common/Test.fr-FR.json @@ -0,0 +1,4 @@ +{ + "AdditionalKey": "Clé supplémentaire", + "Welcome": "Bienvenue" +} diff --git a/test/My.Extensions.Localization.Json.Tests/JsonLocalizationServiceCollectionExtensionsTests.cs b/test/My.Extensions.Localization.Json.Tests/JsonLocalizationServiceCollectionExtensionsTests.cs index c79c89e..71d390b 100644 --- a/test/My.Extensions.Localization.Json.Tests/JsonLocalizationServiceCollectionExtensionsTests.cs +++ b/test/My.Extensions.Localization.Json.Tests/JsonLocalizationServiceCollectionExtensionsTests.cs @@ -31,7 +31,7 @@ public void AddJsonLocalizationWithOptions() // Act JsonLocalizationServiceCollectionExtensions.AddJsonLocalization(services, - options => options.ResourcesPath = "Resources"); + options => options.ResourcesPath = new[] { "Resources" }); var localizationConfigureOptions = (ConfigureNamedOptions)services .SingleOrDefault(sd => sd.ServiceType == typeof(IConfigureOptions)) @@ -44,6 +44,6 @@ public void AddJsonLocalizationWithOptions() localizationConfigureOptions.Action.Invoke(localizationOptions); - Assert.Equal("Resources", localizationOptions.ResourcesPath); + Assert.Equal(new[] { "Resources" }, localizationOptions.ResourcesPath); } } \ No newline at end of file diff --git a/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs b/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs index aee3e2e..6eeb66d 100644 --- a/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs +++ b/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs @@ -88,7 +88,7 @@ public async Task LocalizerReturnsTranslationFromInnerClass() var webHostBuilder = new WebHostBuilder() .ConfigureServices(services => { - services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + services.AddJsonLocalization(options => options.ResourcesPath = new[] { "Resources" }); }) .Configure(app => { @@ -186,7 +186,7 @@ public void CreateLocalizerWithBasenameAndLocation_WithRootNamespaceAttribute() private void SetupLocalizationOptions(string resourcesPath, ResourcesType resourcesType = ResourcesType.TypeBased) => _localizationOptions.Setup(o => o.Value) .Returns(() => new JsonLocalizationOptions { - ResourcesPath = resourcesPath, + ResourcesPath = new[] { resourcesPath }, ResourcesType = resourcesType }); @@ -196,7 +196,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddLocalization(); - services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + services.AddJsonLocalization(options => options.ResourcesPath = new[] { "Resources" }); } public void Configure(IApplicationBuilder app, IStringLocalizer localizer) diff --git a/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerTests.cs b/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerTests.cs index b2c91f1..801d069 100644 --- a/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerTests.cs +++ b/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerTests.cs @@ -22,7 +22,7 @@ public JsonStringLocalizerTests() { var _localizationOptions = new Mock>(); _localizationOptions.Setup(o => o.Value) - .Returns(() => new JsonLocalizationOptions { ResourcesPath = "Resources" }); + .Returns(() => new JsonLocalizationOptions { ResourcesPath = new[] { "Resources" } }); var localizerFactory = new JsonStringLocalizerFactory(_localizationOptions.Object, NullLoggerFactory.Instance); var location = "My.Extensions.Localization.Json.Tests"; var basename = $"{location}.Common.{nameof(Test)}"; @@ -135,7 +135,7 @@ public async void CultureBasedResourcesUsesIStringLocalizer() { services.AddJsonLocalization(options => { - options.ResourcesPath = "Resources"; + options.ResourcesPath = new[] { "Resources" }; options.ResourcesType = ResourcesType.CultureBased; }); }) diff --git a/test/My.Extensions.Localization.Json.Tests/MultipleResourcesPathsTests.cs b/test/My.Extensions.Localization.Json.Tests/MultipleResourcesPathsTests.cs new file mode 100644 index 0000000..78cfae4 --- /dev/null +++ b/test/My.Extensions.Localization.Json.Tests/MultipleResourcesPathsTests.cs @@ -0,0 +1,159 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using My.Extensions.Localization.Json.Tests.Common; +using Xunit; + +namespace My.Extensions.Localization.Json.Tests; + +public class MultipleResourcesPathsTests +{ + [Fact] + public void LocalizerReturnsTranslationFromFirstPath() + { + // Arrange + var localizationOptions = new Mock>(); + localizationOptions.Setup(o => o.Value) + .Returns(() => new JsonLocalizationOptions + { + ResourcesPath = new[] { "Resources", "AdditionalResources" } + }); + var localizerFactory = new JsonStringLocalizerFactory(localizationOptions.Object, NullLoggerFactory.Instance); + var location = "My.Extensions.Localization.Json.Tests"; + var basename = $"{location}.Common.{nameof(Test)}"; + var localizer = localizerFactory.Create(basename, location); + + LocalizationHelper.SetCurrentCulture("fr-FR"); + + // Act + var translation = localizer["Hello"]; + + // Assert + Assert.Equal("Bonjour", translation); + } + + [Fact] + public void LocalizerReturnsTranslationFromSecondPath() + { + // Arrange + var localizationOptions = new Mock>(); + localizationOptions.Setup(o => o.Value) + .Returns(() => new JsonLocalizationOptions + { + ResourcesPath = new[] { "Resources", "AdditionalResources" } + }); + var localizerFactory = new JsonStringLocalizerFactory(localizationOptions.Object, NullLoggerFactory.Instance); + var location = "My.Extensions.Localization.Json.Tests"; + var basename = $"{location}.Common.{nameof(Test)}"; + var localizer = localizerFactory.Create(basename, location); + + LocalizationHelper.SetCurrentCulture("fr-FR"); + + // Act + var translation = localizer["AdditionalKey"]; + + // Assert + Assert.Equal("Clé supplémentaire", translation); + } + + [Fact] + public void LocalizerMergesResourcesFromMultiplePaths() + { + // Arrange + var localizationOptions = new Mock>(); + localizationOptions.Setup(o => o.Value) + .Returns(() => new JsonLocalizationOptions + { + ResourcesPath = new[] { "Resources", "AdditionalResources" } + }); + var localizerFactory = new JsonStringLocalizerFactory(localizationOptions.Object, NullLoggerFactory.Instance); + var location = "My.Extensions.Localization.Json.Tests"; + var basename = $"{location}.Common.{nameof(Test)}"; + var localizer = localizerFactory.Create(basename, location); + + LocalizationHelper.SetCurrentCulture("fr-FR"); + + // Act & Assert - key from first path + var translationFromFirst = localizer["Hello"]; + Assert.Equal("Bonjour", translationFromFirst); + + // Act & Assert - key from second path + var translationFromSecond = localizer["AdditionalKey"]; + Assert.Equal("Clé supplémentaire", translationFromSecond); + } + + [Fact] + public void LocalizerFirstPathTakesPrecedence() + { + // Arrange + var localizationOptions = new Mock>(); + localizationOptions.Setup(o => o.Value) + .Returns(() => new JsonLocalizationOptions + { + ResourcesPath = new[] { "Resources", "AdditionalResources" } + }); + var localizerFactory = new JsonStringLocalizerFactory(localizationOptions.Object, NullLoggerFactory.Instance); + var location = "My.Extensions.Localization.Json.Tests"; + var basename = $"{location}.Common.{nameof(Test)}"; + var localizer = localizerFactory.Create(basename, location); + + LocalizationHelper.SetCurrentCulture("fr-FR"); + + // Act - Hello exists in both paths + var translation = localizer["Hello"]; + + // Assert - First path value should win + Assert.Equal("Bonjour", translation); + } + + [Fact] + public void LocalizerWorksWithSinglePath() + { + // Arrange + var localizationOptions = new Mock>(); + localizationOptions.Setup(o => o.Value) + .Returns(() => new JsonLocalizationOptions + { + ResourcesPath = new[] { "Resources" } + }); + var localizerFactory = new JsonStringLocalizerFactory(localizationOptions.Object, NullLoggerFactory.Instance); + var location = "My.Extensions.Localization.Json.Tests"; + var basename = $"{location}.Common.{nameof(Test)}"; + var localizer = localizerFactory.Create(basename, location); + + LocalizationHelper.SetCurrentCulture("fr-FR"); + + // Act + var translation = localizer["Hello"]; + + // Assert + Assert.Equal("Bonjour", translation); + } + + [Fact] + public void LocalizerWorksWithMultiplePathsIncludingNonExistent() + { + // Arrange + var localizationOptions = new Mock>(); + localizationOptions.Setup(o => o.Value) + .Returns(() => new JsonLocalizationOptions + { + ResourcesPath = new[] { "Resources", "AdditionalResources", "NonExistentPath" } + }); + var localizerFactory = new JsonStringLocalizerFactory(localizationOptions.Object, NullLoggerFactory.Instance); + var location = "My.Extensions.Localization.Json.Tests"; + var basename = $"{location}.Common.{nameof(Test)}"; + var localizer = localizerFactory.Create(basename, location); + + LocalizationHelper.SetCurrentCulture("fr-FR"); + + // Act + var translationFromFirst = localizer["Hello"]; + var translationFromSecond = localizer["AdditionalKey"]; + + // Assert + Assert.Equal("Bonjour", translationFromFirst); + Assert.Equal("Clé supplémentaire", translationFromSecond); + } +} diff --git a/test/My.Extensions.Localization.Json.Tests/My.Extensions.Localization.Json.Tests.csproj b/test/My.Extensions.Localization.Json.Tests/My.Extensions.Localization.Json.Tests.csproj index ea10982..7f4d482 100644 --- a/test/My.Extensions.Localization.Json.Tests/My.Extensions.Localization.Json.Tests.csproj +++ b/test/My.Extensions.Localization.Json.Tests/My.Extensions.Localization.Json.Tests.csproj @@ -41,6 +41,9 @@ PreserveNewest + + PreserveNewest +