Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@ namespace My.Extensions.Localization.Json;
public class JsonLocalizationOptions : LocalizationOptions
{
public ResourcesType ResourcesType { get; set; } = ResourcesType.TypeBased;

/// <summary>
/// Gets or sets a value indicating whether to use the full generic type name for resource files.
/// When set to <c>true</c> (default), the full generic type name (e.g., "GenericClass`1[[...]]") is used.
/// When set to <c>false</c>, the generic type markers are stripped (e.g., "GenericClass"), making it easier to name resource files.
/// </summary>
public bool UseGenericResources { get; set; } = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class JsonStringLocalizerFactory : IStringLocalizerFactory
private readonly ConcurrentDictionary<string, JsonStringLocalizer> _localizerCache = new();
private readonly string _resourcesRelativePath;
private readonly ResourcesType _resourcesType = ResourcesType.TypeBased;
private readonly bool _useGenericResources;
private readonly ILoggerFactory _loggerFactory;

public JsonStringLocalizerFactory(
Expand All @@ -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));
}

Expand All @@ -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));
}
Expand All @@ -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 + ".");
}

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace My.Extensions.Localization.Json.Tests.Common;

internal class GenericTest<T>
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>));

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

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
<None Update="Resources\Common\Test.fr.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Common\GenericTest.fr.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Hello": "Bonjour Générique"
}