theDrifter
theDrifter

Reputation: 1706

Localize ASP.NET Core 6 Web API with a single file per language from a shared project

There are many answered questions out there covering this topic, but i cannot make it work here.

Our .NET Core 6 solution contains many projects - most of them RESTful APIs - which can share the resources for localization.

So the goal is now setting up a SharedLocResources project containing all the resx-files with the translations which then can be accessed in all other project. The request locale is provided by the Accept-Language header. Getting this to work is not the problem. The setup of using the resources files is what I am struggling with.

RestfulAPI.csproj
 | - Controllers/MyController.cs
SharedLocalizationResources.csproj
 |
 | - Properties
    | - LocalizationService.cs
    | - SharedResource.cs //Dummy class to group shared resources
    | - Resources.resx
    | - Resources.ar.resx
    | - Resources.bn.resx
    ....

A custom service uses the IStringLocalizerFactory. It creates an IStringLocalizer instance together with a dummy class providing the type.

SharedResources.cs resides within the SharedLocResources project.

namespace SharedLocalizationResources;

public class LocalizationService
{
    private readonly IStringLocalizer localizer;

    public LocalizationService(IStringLocalizerFactory factory)
    {
        var type = typeof(SharedResource);
        var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
        localizer = factory.Create("SharedResource", assemblyName.Name);
    }
}

namespace SharedLocalizationResources;
/// <summary>
/// Dummy class to group shared resources
/// </summary>
public class SharedResource{}

This service is added to the startup routine:

builder.Services.AddSingleton<LocalizationService>();
builder.Services.AddLocalization(options => options.ResourcesPath = "SharedLocalizationResources");  // Resource-containing project name

builder.Services.AddMvc().AddViewLocalization()
.AddDataAnnotationsLocalization(options =>
{
    options.DataAnnotationLocalizerProvider = (type, factory) =>
    {
        var assemblyName = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName);
        return factory.Create("SharedResource", assemblyName.Name);
    };
});

builder.Services.Configure<RequestLocalizationOptions>(
options =>
{
    var supportedCultures = new List<CultureInfo>
    {
        new("ar"),
        new("bn"),
        new("bg"),
        new("en"),
        new("de")
    };

    options.DefaultRequestCulture = new RequestCulture(culture: "de", uiCulture: "de");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;

    options.RequestCultureProviders.Insert(0, new QueryStringRequestCultureProvider());
});

But using this in a controller, I see only the default "Test" string, but not the localized resources.

public MyController( IStringLocalizer<SharedResource> sharedLocalizer)
{
    sharedLocalizer = sharedLocalizer; 
}

public ActionResult<string> Version()
{
    var res = new ActionResult<string>(sharedLocalizer["Test"]);
    return res;
}

How do I have to fix the setup to make the IStringLocalizer make use of the resources from the SharedLocalizationResources project?

Upvotes: 0

Views: 389

Answers (1)

Mitselplik
Mitselplik

Reputation: 1133

Put a breakpoint in your constructor and inspect sharedLocalizer for a private property called _resourceBaseName. The underlying class (probably Microsoft.Extensions.Localization.ResourceManagerStringLocalizer) often mangles the full type name by injecting .Resources. after the first moniker.

They do this with the intention that your project's \Resources folder can easily mirror the rest of your application and string localization will auto-magically work. For example, if you have a Razor page at WebApp\Pages\MyPageModel.cshtml and your underlying code class injects IStringLocalizer<WebApp.Pages.MyPageModel> _localizer (as in the plethora of online examples), then Microsoft's code tries to be helpful and look for an implementation of WebApp.Resources.Pages.MyPageModel. Yay for magic string manipulation they don't tell you about!

This unfortunately doesn't help at all when you are trying access a resource file located somewhere else.

In your case, the class is likely looking for a resource class at SharedLocalizationResources.Resources.Properties.SharedResource and not finding it obviously.

My recommendation would be to stop using the \Properties folder to store your resource files, add a \Resources folder and put all your resources in there, then add a dummy class at the project root that you use as your generic type when declaring your IStringLocalizer<T> variables elsewhere.

So you would have a dummy class at SharedLocalizationResources.SharedResource and all your resource files we be located at SharedLocalizationResources\Resources\SharedResource.resx.

Hope this helps!

Upvotes: 0

Related Questions