Localize an ASP.NET Core MVC app - asp.net-core

I'm struggling with getting my app to localize strings properly. Feel that I've searched every corner of the web without finding something that works that I expect it to.
I use the RouteDataRequestCultureProvider and first problem I'm trying to solve is to be able to use "short hand version" of the culture, e.g. sv instead of sv-SE and that sv is treated as sv-SE when the culture is created. This doesn't happen automatically.
Second is just getting the app to show a localized string. Here is how I configure the localization
public static IServiceCollection ConfigureLocalization(this IServiceCollection services)
{
services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new System.Globalization.CultureInfo("sv-SE"),
new System.Globalization.CultureInfo("en-US")
};
options.DefaultRequestCulture = new RequestCulture(culture: "sv-SE", uiCulture: "sv-SE");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[]
{
new RouteDataRequestCultureProvider
{
Options = options
}
};
});
return services;
}
And then in Configure I do app.UseRequestLocalizationOptions(); which inject the options previously configured.
In the folder Resources I've created the resources files Resource.resx and Resource.sv.resx.
In my view file I've tried both injecting IStringLocalizer (which fails since no default implementation is registered) and IStringLocalizer<My.Namespace.Resources.Resource> but none of the options works. I've also tried to old fashion way #My.Namespace.Resources.Resource.StringToLocalize
Is it impossible to have a shared resource file? Don't want to resort to Resource\Views\ViewA.resx and Resource\Controllers\AController.resx.
Thanks

So I got this to work. Inside my Resource folder I have my Lang.resx-files with public access modifier.
I found this and added a class Lang (named to file LangDummy not to conflict with resource files) that belongs to the root namespace of the project.
Then in my view imports file #inject Microsoft.Extensions.Localization.IStringLocalizer<My.Namespace.Lang> StringLocalizer and use it #StringLocalizer["PropertyInResxFile"]. Ideally I would like to use the generated static properties for type safety and easier refactoring and using nameof everywhere just feels bloated.
This works for localization via data annotation attributes as well but here I use ResourceType = typeof(My.Namespace.Resources.Lang), i.e. the class for the resx file

Related

Localization not working in .NET6 Razor Pages project

I was trying to get localization to work in my 'real' project, but was not able to do so. So I created a new stock .NET6 Razor Pages project to test localization in a fresh environment. However I am not quite able to do that there as well. At this point I have read about 6 articles, watched 2 tutorials and read the official documentation which says that it is up to date with .NET6, but I'm not sure about that (partly because the examples use old syntax, i.e. not things that came with .NET6). Every single one of those resources more or less did/said the same things, which is what I have right now, but it just doesn't work.
This is my Program.cs file:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
//builder.Services.AddRazorPages(); builder.Services.AddMvc().AddDataAnnotationsLocalization();
builder.Services.AddRazorPages().AddDataAnnotationsLocalization();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
var supportedCultures = new[] { "en" };
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.UseAuthorization();
app.MapRazorPages();
app.Run();
I have tested the "things I'll mention bellow" with ...AddRazorPages().AddData... and (AddMvc().AddData... with ...AddRazorPages();) as well. That's why one of those lines is commented.
The way I am testing the localization is just with an OnGet call on the Index page, and restarting the (local) server after every change just to not miss some dumb thing. On the OnGet, I just log the value to the console. But I'm always getting the key. Here is the IndexModel class:
public class IndexModel : PageModel
{
private readonly IStringLocalizer<IndexModel> _localizer;
public IndexModel(IStringLocalizer<IndexModel> localizer)
{
_localizer = localizer;
}
public void OnGet()
{
Console.WriteLine(_localizer["Test"].Value);
}
}
This is how my (relevant) folder structure looks like in the project's directory:
And finally the resx file:
Things I have tested are the .resx file names and the 2 possible locations. I am sure that both are correct, because when both files were active (neither had the .Dup (for duplicate) extension) at the end, I got an error from the compiler that multiple resources point to the same location or something like that.
The names I have tested are the following:
IndexModel.en.resx
Index.en.resx
IndexModel.cshtml.cs.en.resx
Index.cshtml.cs.en.resx
IndexModel.cs.en.resx
Index.cs.en.resx

Migration to Minimal API - Test Settings Json not overriding Program

Thanks to this answer: Integration test and hosting ASP.NET Core 6.0 without Startup class
I have been able to perform integration tests with API.
WebApplicationFactory<Program>? app = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
});
});
HttpClient? client = app.CreateClient();
This has worked using the appsettings.json from the API project. Am now trying to use integrationtestsettings.json instead using:
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(ProjectDirectoryLocator.GetProjectDirectory())
.AddJsonFile("integrationtestsettings.json")
.Build();
WebApplicationFactory<Program>? app = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration(cfg => cfg.AddConfiguration(configuration));
builder.ConfigureServices(services =>
{
});
});
_httpClient = app.CreateClient();
I have inspected the configuration variable and can see the properties loaded from my integrartiontestsettings.json file. However, the host is still running using the appsettings.json from the server project.
Previously, in .Net5, I was using WebHostBuilder and the settings were overridden by test settings.
WebHostBuilder webHostBuilder = new();
webHostBuilder.UseStartup<Startup>();
webHostBuilder.ConfigureAppConfiguration(cfg => cfg.AddConfiguration(_configuration));
But cannot get the test settings to apply using the WebApplicationFactory.
It seems the method has changed.
Changing:
builder.ConfigureAppConfiguration(cfg => cfg.AddConfiguration(configuration));
To:
builder.UseConfiguraton(configuration);
has done the trick.
builder.ConfigureAppConfiguration, now it's configuring the app (after your WebApplicationBuilder.Build() is called) and your WebApplication is created.
You need to "inject" your configurations before the .Build() is done. This is why you need to call UseConfiguraton instead of ConfigureAppConfiguration.

How to fix Obj Folder Assembly Error: "Executables cannot be satellite assemblies; culture should always be empty"

Here's what I've added in the code that makes me experience this error:
I'm using VSCode.
In StartUp.cs - ConfigureServices
services.AddLocalization (options => options.ResourcesPath = "Resources");
services.AddMvc ().SetCompatibilityVersion (Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1)
.AddViewLocalization (LanguageViewLocationExpanderFormat.Suffix, opts => { opts.ResourcesPath = "Resources"; });
In StartUp.cs - Configure
var localizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
localizationOptions.RequestCultureProviders.Clear();
localizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
app.UseRequestLocalization(localizationOptions);
I've created the Resource file in this folder:
Resources
Views
Grants
Index.en-US.resx
Index.hu-HU.resx
And called the values in this view:
View
Grants
Index.cshtml
Problem:
When I build the project, I got an error on the newly created OBJ folder in xxx.Resources.cs file, "Executables cannot be satellite assemblies; culture should always be empty"
obj
Debug
netcoreapp2.2
en-US
xxx.resources.cs
xxx.resources.dll
[assembly: System.Reflection.AssemblyCultureAttribute("en-US")]
The project will run but this line will give you an error and won't be recognized.
What I wanted to do is totally eliminate this error everytime that I will build the project.
I'm also thinking that it might be an intellisense error.
I had the same issue. Solved by disabling parsing of files in obj/* folder, by adding exclude patterns in omnisharp.json file.
"fileOptions": {
"systemExcludeSearchPatterns": [
"**/bin/**/*",
"**/obj/**/*"
]
}
I think this is the right approax, as the obj folder is not a part of the project (in terms of source code), but some kind of generates that I perceive as a black-box.
See Omnisharp configuration documentation for details.

GetRequiredService from within Configure

I'm trying to access one of my services from within the Configure call within Startup.cs in aspnet core. I'm doing the following however I get the following error "No service for type 'UserService' has been registered." Now I know it is registered because I can use it in a controller so I'm just doing something wrong when it comes to using it here. Please can someone point me in the right direction. I'm happy with taking a different approach to setting up Tus if there's a better way of achieving what I want.
var userService = app.ApplicationServices.GetRequiredService<UserService>();
userService.UpdateProfileImage(file.Id);
The below is where I'm wanting to use
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
... Other stuff here...
app.InitializeSimpleInjector(container, Configuration);
container.Verify();
app.UseTus(httpContext =>
{
var restaurantEndpoint = "/restaurant/images";
var userEndpoint = "/account/images";
var endPoint = "/blank/images";
if (httpContext.Request.Path.StartsWithSegments(new PathString(restaurantEndpoint)))
{
endPoint = restaurantEndpoint;
}
if (httpContext.Request.Path.StartsWithSegments(new PathString(userEndpoint)))
{
endPoint = userEndpoint;
}
return new BranchTusConfiguration
{
Store = new TusDiskStore(#"C:\tusfiles\"),
UrlPath = endPoint,
Events = new Events
{
OnBeforeCreateAsync = ctx =>
{
return Task.CompletedTask;
},
OnCreateCompleteAsync = ctx =>
{
return Task.CompletedTask;
},
OnFileCompleteAsync = async ctx =>
{
var file = await ( (ITusReadableStore)ctx.Store ).GetFileAsync(ctx.FileId, ctx.CancellationToken);
var userService = app.ApplicationServices.GetRequiredService<UserService>();
userService.UpdateProfileImage(file.Id);
}
}
};
});
... More stuff here...
};
My end goal is to move this to an IApplicationBuilder extension to clean up my startup.cs but that shouldn't affect anything if it's working from within startup.cs
Edit: Add to show the registration of the userService. There is a whole lot of other stuff being registered and cross wired in the InitializeSimpleInjector method which I've left out. can add it all if need be..
public static void InitializeSimpleInjector(this IApplicationBuilder app, Container container, IConfigurationRoot configuration)
{
// Add application presentation components:
container.RegisterMvcControllers(app);
container.RegisterMvcViewComponents(app);
container.Register<UserService>(Lifestyle.Scoped);
container.CrossWire<IServiceProvider>(app);
container.Register<IServiceCollection, ServiceCollection>(Lifestyle.Scoped);
}
Please read the Simple Injector integration page for ASP.NET Core very closely, as Simple Injector integrates very differently with ASP.NET Core as Microsoft documented how DI Containers should integrate. The Simple Injector documentation states:
Please note that when integrating Simple Injector in ASP.NET Core, you do not replace ASP.NET’s built-in container, as advised by the Microsoft documentation. The practice with Simple Injector is to use Simple Injector to build up object graphs of your application components and let the built-in container build framework and third-party components
What this means is that, since the built-in container is still in place, resolving components using app.ApplicationServices.GetRequiredService<T>()—while they are registered in Simple Injector—will not work. In that case you are asking the built-in container and it doesn't know about the existence of those registrations.
Instead, you should resolve your type(s) using Simple Injector:
container.GetInstance<UserService>()

Is there a way to add two resource folders to aspnet core localization?

I'm currently developing the SDK for one project and as a requirement I need to add two resources locations. One will be provided with the SDK lib and another to be provided by the consumer app.
Currently, according to docs, this is how to add localization:
services.AddLocalization(options => options.ResourcesPath = "Resources");
I'm calling this method from my BaseStartup class that will be inherited by the consumer app's Startup class. So I need to be able to setup the location of the SDK's resources folder and the consumer app's one as well.
Maybe something like:
services.AddLocalization(options =>
{
options.ResourcesPath = "SDKResources";
options.FromAssembly = sdkResourcesAssembly;
});
services.AddLocalization(options =>
{
options.ResourcesPath = "AppResources";
options.FromAssembly = appResourcesAssembly;
});
Is this possible? If so, how? If not, is there a workaround?
Checking online and even the source code (https://github.com/aspnet/Localization) wasn't of much help. The only thing I can think of is using IStringLocalizerFactory which accepts an assembly and the name of the file. Would it work? For instance, adding services.AddLocalization() and then just creating a wrapper class that would provide the consumer app with the strings using the factories created using IStringLocalizerFactory?
Thanks!
I found out about two ways it can be done, first by adding resources from different assemblies:
I created a base startup class to handle all source assemblies containing my resource classes and then I load them.
serviceCollection.AddLocalization();
var resourceTypes = typeof(BaseResource<>).Assembly.GetDerivedGenericTypes(typeof(BaseResource<>));
if (typeFromResourceAssembly != null)
resourceTypes.AddRange(typeFromResourceAssembly.Assembly.GetDerivedGenericTypes(typeof(BaseResource<>)));
foreach (var resourceType in resourceTypes)
{
serviceCollection.AddScoped(resourceType, resourceType);
}
return serviceCollection;
Second, by adding different resource folders:
services.Configure<ClassLibraryLocalizationOptions>(
options => options.ResourcePaths = new Dictionary<string, string>
{
{ "ResourceClass", "ResourcesFolder" },
{ "Localization.CustomResourceClass", "Folder1/Folder2" }
}
);