diff --git a/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs b/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs index 4eb1cae..8d56841 100644 --- a/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs +++ b/src/My.Extensions.Localization.Json/JsonLocalizationOptions.cs @@ -5,4 +5,11 @@ namespace My.Extensions.Localization.Json; public class JsonLocalizationOptions : LocalizationOptions { public ResourcesType ResourcesType { get; set; } = ResourcesType.TypeBased; + + /// + /// Gets or sets a value indicating whether to use the full generic type name for resource files. + /// When set to true (default), the full generic type name (e.g., "GenericClass`1[[...]]") is used. + /// When set to false, the generic type markers are stripped (e.g., "GenericClass"), making it easier to name resource files. + /// + public bool UseGenericResources { get; set; } = true; } \ 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 a45a9ea..137227f 100644 --- a/src/My.Extensions.Localization.Json/JsonStringLocalizerFactory.cs +++ b/src/My.Extensions.Localization.Json/JsonStringLocalizerFactory.cs @@ -18,6 +18,7 @@ public class JsonStringLocalizerFactory : IStringLocalizerFactory private readonly ConcurrentDictionary _localizerCache = new(); private readonly string _resourcesRelativePath; private readonly ResourcesType _resourcesType = ResourcesType.TypeBased; + private readonly bool _useGenericResources; private readonly ILoggerFactory _loggerFactory; public JsonStringLocalizerFactory( @@ -28,6 +29,7 @@ public JsonStringLocalizerFactory( _resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty; _resourcesType = localizationOptions.Value.ResourcesType; + _useGenericResources = localizationOptions.Value.UseGenericResources; _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } @@ -52,8 +54,10 @@ public IStringLocalizer Create(Type resourceSource) ? typeInfo.Name : TrimPrefix(typeInfo.FullName, assemblyName + "."); - resourcesPath = Path.Combine(PathHelpers.GetApplicationRoot(), GetResourcePath(assembly)); typeName = TryFixInnerClassPath(typeName); + typeName = TryStripGenericTypeMarkers(typeName); + + resourcesPath = Path.Combine(PathHelpers.GetApplicationRoot(), GetResourcePath(assembly)); return _localizerCache.GetOrAdd($"culture={CultureInfo.CurrentUICulture.Name}, typeName={typeName}", _ => CreateJsonStringLocalizer(resourcesPath, typeName)); } @@ -79,6 +83,7 @@ public IStringLocalizer Create(string baseName, string location) if (_resourcesType == ResourcesType.TypeBased) { baseName = TryFixInnerClassPath(baseName); + baseName = TryStripGenericTypeMarkers(baseName); resourceName = TrimPrefix(baseName, location + "."); } @@ -129,4 +134,20 @@ private static string TryFixInnerClassPath(string path) return fixedPath; } + + private string TryStripGenericTypeMarkers(string typeName) + { + if (_useGenericResources) + { + return typeName; + } + + var backtickIndex = typeName.IndexOf('`', StringComparison.Ordinal); + if (backtickIndex >= 0) + { + return typeName.Substring(0, backtickIndex); + } + + return typeName; + } } \ No newline at end of file diff --git a/test/My.Extensions.Localization.Json.Tests/Common/GenericTest.cs b/test/My.Extensions.Localization.Json.Tests/Common/GenericTest.cs new file mode 100644 index 0000000..9e8a027 --- /dev/null +++ b/test/My.Extensions.Localization.Json.Tests/Common/GenericTest.cs @@ -0,0 +1,5 @@ +namespace My.Extensions.Localization.Json.Tests.Common; + +internal class GenericTest +{ +} diff --git a/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs b/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs index 8e230dc..1eef765 100644 --- a/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs +++ b/test/My.Extensions.Localization.Json.Tests/JsonStringLocalizerFactoryTests.cs @@ -112,11 +112,48 @@ public async Task LocalizerReturnsTranslationFromInnerClass() var response = await client.GetAsync("/"); } - private void SetupLocalizationOptions(string resourcesPath, ResourcesType resourcesType = ResourcesType.TypeBased) + [Fact] + public void CreateLocalizerForGenericType_WithUseGenericResourcesFalse_StripsGenericMarkers() + { + SetupLocalizationOptions("Resources", ResourcesType.TypeBased, useGenericResources: false); + LocalizationHelper.SetCurrentCulture("fr"); + + // Arrange + var localizerFactory = new JsonStringLocalizerFactory(_localizationOptions.Object, _loggerFactory); + + // Act - Create localizer for a generic type + var localizer = localizerFactory.Create(typeof(GenericTest)); + + // Assert + Assert.NotNull(localizer); + Assert.Equal("Bonjour Générique", localizer["Hello"]); + } + + [Fact] + public void CreateLocalizerForGenericType_WithUseGenericResourcesTrue_UsesFullTypeName() + { + SetupLocalizationOptions("Resources", ResourcesType.TypeBased, useGenericResources: true); + LocalizationHelper.SetCurrentCulture("fr"); + + // Arrange + var localizerFactory = new JsonStringLocalizerFactory(_localizationOptions.Object, _loggerFactory); + + // Act - Create localizer for a generic type (with default UseGenericResources = true, + // the resource name includes generic markers, so it won't find the resource file) + var localizer = localizerFactory.Create(typeof(GenericTest)); + + // Assert - Resource not found because the file is named GenericTest.fr.json, not GenericTest`1[[...]] + Assert.NotNull(localizer); + var result = localizer["Hello"]; + Assert.True(result.ResourceNotFound); + } + + private void SetupLocalizationOptions(string resourcesPath, ResourcesType resourcesType = ResourcesType.TypeBased, bool useGenericResources = true) => _localizationOptions.Setup(o => o.Value) .Returns(() => new JsonLocalizationOptions { ResourcesPath = resourcesPath, - ResourcesType = resourcesType + ResourcesType = resourcesType, + UseGenericResources = useGenericResources }); public class InnerClassStartup 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 82785a0..33a92d5 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 @@ -39,6 +39,9 @@ PreserveNewest + + PreserveNewest + diff --git a/test/My.Extensions.Localization.Json.Tests/Resources/Common/GenericTest.fr.json b/test/My.Extensions.Localization.Json.Tests/Resources/Common/GenericTest.fr.json new file mode 100644 index 0000000..4232242 --- /dev/null +++ b/test/My.Extensions.Localization.Json.Tests/Resources/Common/GenericTest.fr.json @@ -0,0 +1,3 @@ +{ + "Hello": "Bonjour Générique" +}