I am using IStringLocalizer approach to localize my Blazor app as discussed here.
Injecting the IStringLocalizer on razor pages works great. I also need this to localize some services - whether scoped or even singleton services.
Using constructor injection to inject my IStringLocalizer service into the service works. However, when users change the language via UI, the service (whether singleton or scoped) keeps the initial IStringLocalizer - i.e. the one with the original language used when starting the app, not the updated language selected by the user.
What is the suggested approach to retrieve the updated IStringLocalizer from code?
EDIT
To prevent more details, here is some piece of code.
First, I add a Resources folder and create there a default LocaleResources.resx (with public modifiers) and a LocaleResources.fr.resx file, which contain the key-value pairs for each language.
Supported cultures are defined in the appsettings.json file as
"Cultures": {
"en-US": "English",
"fr": "Français (Suisse)",
...
}
In startup, I register the Resources folder and the supported cultures :
public void ConfigureServices(IServiceCollection services {
...
services.AddLocalization(options => options.ResourcesPath = "Resources");
...
services.AddSingleton<MySingletonService>();
services.AddScoped<MyScopedService>();
}
// --- helper method to retrieve the Cultures from appsettings.json
protected RequestLocalizationOptions GetLocalizationOptions() {
var cultures = Configuration.GetSection("Cultures")
.GetChildren().ToDictionary(x => x.Key, x => x.Value);
var supportedCultures = cultures.Keys.ToArray();
var localizationOptions = new RequestLocalizationOptions()
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
return localizationOptions;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
...
app.UseRequestLocalization(GetLocalizationOptions());
...
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
I created an empty LocaleResources.razor control at the root of the project (this is a trick used to inject a single resource file to all components).
I included a routing controller to change language :
[Route("[controller]/[action]")]
public class CultureController : Controller {
public IActionResult SetCulture(string culture, string redirectUri) {
if (culture != null) {
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture)));
}
return LocalRedirect(redirectUri);
}
}
And the language UI switcher looks like this (I use SyncFusion control here, but it could be any lookup actually, that shouldn't really matter)
#inject NavigationManager NavigationManager
#inject IConfiguration Configuration
<SfComboBox TValue="string" TItem="Tuple<string, string>" Placeholder="Select language" DataSource="#Cultures"
#bind-Value="selectedCulture" CssClass="lan-switch" Width="80%">
<ComboBoxFieldSettings Text="Item2" Value="Item1"></ComboBoxFieldSettings>
</SfComboBox>
<style>
.lan-switch {
margin-left: 5%;
}
</style>
#code {
string _activeCulture = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
private string selectedCulture {
get => _activeCulture;
set {
_activeCulture = value;
SelectionChanged(value);
}
}
List<Tuple<string, string>> Cultures;
protected override void OnInitialized() {
var cultures = Configuration.GetSection("Cultures")
.GetChildren().ToDictionary(x => x.Key, x => x.Value);
Cultures = cultures.Select(p => Tuple.Create<string, string>(p.Key, p.Value)).ToList();
}
protected override void OnAfterRender(bool firstRender) {
if (firstRender && selectedCulture != AgendaSettings.SelectedLanguage) {
selectedCulture = AgendaSettings.SelectedLanguage;
}
}
private void SelectionChanged(string culture) {
if (string.IsNullOrWhiteSpace(culture)) {
return;
}
AgendaSettings.SelectedLanguage = culture;
var uri = new Uri(NavigationManager.Uri)
.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
var query = $"?culture={Uri.EscapeDataString(culture)}&" +
$"redirectUri={Uri.EscapeDataString(uri)}";
NavigationManager.NavigateTo("/Culture/SetCulture" + query, forceLoad: true);
}
}
Finally, to the injection. I inject the IStringLocalizer to pages as follows and it works perfectly fine on razor controls:
#inject IStringLocalizer<LocaleResources> _loc
<h2>#_loc["hello world"]</h2>
Above, when I change language, the page displays the value in the corresponding resource file.
Now, to services: the MySingletonService and MyScopedService are registered at startup. They both have a constructor like
protected IStringLocalizer<LocaleResources> _loc;
public MySingletonService(IStringLocalizer<LocaleResources> loc) {
_loc = loc;
}
public void someMethod() {
Console.WriteLine(_loc["hello world"])
}
I run someMethod on a timer. Strangely, when I break on the above line, the result seems to oscillate : once it returns the default language's value, once the localized one...!
The answer to my question was: your code is correct!
The reason, I found out, is that I use a Scoped service that is started on the default App's start page:
protected async override Task OnAfterRenderAsync(bool firstRender) {
if (firstRender) {
MyScopedService.StartTimer();
}
await base.OnAfterRenderAsync(firstRender);
}
When users change language, the whole page is refreshed and a new instance of the scoped service is created and timer started. As my service did not implement IDisposable, the timer was not actually stopped.
So 2 solutions here:
use singleton services
make servcie disposable and ensure tasks are cancelled when service is disposed of.
Related
I am trying to localize the DisplayFormat of several of my view models. I have been able to localize the Display:Name, Required and RegularExpression messages all from a shared resource file in a separate project.
In addition, I have been able to localize my razor views and any messages generated from my controllers. After some research, it appears I can't localize the DisplayFormat in the same manner as the other data annotations. Other posts on SO indicate I should create a custom attribute that inherits from Attribute or DisplayAtttribute.
DisplayFormat data annotation using resource string
Model Class DisplayFormat() How can I localization NullDisplayText?
Ideally I would like to retrieve the correct format string from my shared resource file within the custom attribute while passing in the ResourceKey name. I am not sure how to go about setting this up. Possibly using the IStringLocalizer<SharedResource>?
I have an extension method to setup localization services at startup
public static class LocalizationExtensions
{
public static IServiceCollection AddLocalizationServices(this IServiceCollection services)
{
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("es-MX")
};
options.DefaultRequestCulture = new RequestCulture("en-US");
// Formatting numbers, dates, etc.
options.SupportedCultures = supportedCultures;
// UI strings that we have localized.
options.SupportedUICultures = supportedCultures;
});
services.AddLocalization(options => { options.ResourcesPath = "Resources"; } );
services.AddControllersWithViews()
.AddRazorRuntimeCompilation()
.AddViewLocalization(options => {
options.ResourcesPath = "Resources";
})
.AddDataAnnotationsLocalization(options => {
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
return factory.Create(typeof(SharedResource));
};
});
//services.AddScoped<RequestLocalizationCookiesMiddleware>();
return services;
}
}
In my configure method in startup
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
Any help would be appreciated.
I've been tasked with making an update to an existing asp core mvc site. I had to add a filter to an existing report.
The site uses resource files for string translations.
After uploading the site to the server, the site is running, everything is fine. the site runs under a different port for test purposes than the live site.
The moment my colleague tests the site, the site shows with it's keys only.
When i check immediatly after, for me as well, no resource strings are shown, only the keys.
Restarting IIS doesn't help. The only thing that temporarily works, is copying the files again from my computer to the server. Then the translation strings work again. Untill they don't.
I have no clue as what could be the cause and i am looking for ideas to check.
The current things i want to confirm is, when it reverts to keys, if the .resources.dll is still there. Although the virus scanner logs don't show activity, and the production site which is on the same server doesn't expoerince this problem, sepite having the same .resources.dll.
If you think knowing the startup.cs might be helpfull, let me know, or if someone thinks 'if i could only know that', just give out a shout. All input is welcome. It's difficult for me now to know what info could be usefull and what not. Posting a link to the whole reporsitory isn't really an option for me. I hope you understand.
EDIT 2021/02/23
Added a redacted startup.cs
Personal progress, it's not the wrong language it's picking up rather then it's showing the resource keys after x time. The resource keys happen to be in english. It's as if the french resource dll gets deleted, but only infor the test version of the site.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
...
using DevExpress.AspNetCore;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
namespace ...
{
public class Startup
{
private const string Loginpath = "";
public static string LiveUrl { get; set; }
public IConfiguration Configuration { get; }
public IHostingEnvironment HostingEnvironment { get; }
private RequestLocalizationOptions SiteLocalizationOptions { get; }
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
// new CultureInfo("en-US"),
new CultureInfo("fr-FR"),
};
SiteLocalizationOptions = new RequestLocalizationOptions()
{
DefaultRequestCulture = new RequestCulture("fr-FR"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
//the third culture provider looks at the browsers preferenced culture. When the first visitor give english as prefered language, somehow parts of the site become english for subsequent all visitors.
//at some point there was an english site option, and artifacts of that remain throughout the code.
//clearing the providers should force the site to always use English.
SiteLocalizationOptions.RequestCultureProviders.Clear();
HostingEnvironment = env;
...
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
//Add localization and translations
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.Configure<RequestLocalizationOptions>(options =>
{
options.SupportedCultures = SiteLocalizationOptions.SupportedCultures;
options.SupportedUICultures = SiteLocalizationOptions.SupportedUICultures;
options.DefaultRequestCulture = SiteLocalizationOptions.DefaultRequestCulture;
});
services.AddDevExpressControls();
services.AddMvc().AddJsonOptions(options => options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver())
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SiteResource));
})
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver())
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => options.LoginPath = new PathString(Loginpath));
...
//config
...
services.Configure<DatabaseSettings>(Configuration.GetSection("Database"));
services.Configure<HtmlPageSettings>(Configuration.GetSection("HtmlPages"));
services.Configure<ReportSettings>(Configuration.GetSection("Reporting"));
services.Configure<FilesSettings>(Configuration.GetSection("Files"));
services.Configure<AdminSettings>(Configuration.GetSection("Administrator"));
LatContext.ConnectionString = ...;
CroContext.ConnectionString = ...;
if (HostingEnvironment.IsDevelopment())
{
//repositories
services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IStoredProcedures, StoredProcedures>();
//services
services.AddScoped<ILoginService, LoginService>();
}
else if (HostingEnvironment.IsEnvironment("Mock"))
{
//repositories
//No need to add repositories, since they are not used when the services are mocked
//services
services.AddScoped<ILoginService, MockLoginService>();
}
else //production
{
//repositories
services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IStoredProcedures, StoredProcedures>();
//services
services.AddScoped<ILoginService, LoginService>();
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else if (env.IsEnvironment("Mock"))
{
}
else //production
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
//app.UseStaticFiles(new StaticFileOptions
//{
// FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "node_modules")),
// RequestPath = "/node_modules"
//});
app.UseCookiePolicy();
app.UseAuthentication();
app.UseRequestLocalization(SiteLocalizationOptions);
...
app.UseDevExpressControls();
app.UseMvc(mvcRoutes =>
{
...
});
}
}
}
My current routing is like https://localhost:44312/Games/Sea%20of%20Thieves.
I want to change as https://localhost:44312/Games/Sea-of-Thieves.
I'm working on .Net Core Mvc Project. Any ideas ?
[Route("Games/{gameName}")]
public IActionResult GameDetail(string gameName)
{
Game requestedGame = _unitOfWork.Games.GetGameByName(gameName);
GameDetailModel gameDetailModel = _unitOfWork.Games.GetGameDetail(requestedGame.Id, User.Identity.Name);
return View(gameDetailModel);
}
You should replace spaces with - when you create a link on your web site.
For example you can create an interface that replace space with -. then inject the interface on your view or place you create link.
public interface IUrlHelper
{
string RemoveSpace(string url);
}
public class UrlHelper : IUrlHelper
{
public string RemoveSpace(string url)
{
return url.Replace(" ","-");
}
}
in your view or anywhere you create a link you should use this interface
#inject IUrlHelper urlHelper
TestUrl
Note : remember that register IUrlHelper in services.
You can use middleware in startup Configure:
app.Use(async (context, next) =>
{
var url = context.Request.Path.Value;
if (url.Contains("/Games/")&&url.Contains(" "))
{
context.Response.Redirect(url.Replace(" ", "-"));
return;
}
await next();
});
result:
documentation is very sparce and all i tried results in the deserializer injected but normal odata url's not working anymore.
https://github.com/OData/WebApi/issues/158 has solutions buut for 5.6.
The final relevant comment is:
#dbenzhuser - In that commit, look at ODataFormatterTests.cs for how
inject a custom deserializer/deserializer provider. You can still use
a custom DeserializerProvider but it's injected through DI instead of
injecting it through ODataMediaTypeFormatters.
which is quite meaningless. I tried the code there, but it breaks, as I said, the URL's.
Right now my Odata setup is simple:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddOData();
\UnitTest\Microsoft.AspNet.OData.Test.Shared\Formatter\ODataFormatterTests.cs
has examples to inject them (like in lines 379-383)
config.MapODataServiceRoute("IgnoredRouteName", null, builder =>
builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => ODataTestUtil.GetEdmModel())
.AddService<ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new CustomSerializerProvider())
.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp =>
ODataRoutingConventions.CreateDefaultWithAttributeRouting("IgnoredRouteName", config)));
but I seem unable to get this working without removing the core odata routing.
Anyone an idea how to use that for the current version without breaking the base functionality?
There are three steps if you want to maintain the base functionality:
Your DeserializerProvider implementation should default to the base implementation for all scenarios that your custom Deserializer can't manage. In the following example the custom deserializer only operates on Resources and not Sets:
public class EntityTypeDeserializerProvider : DefaultODataDeserializerProvider
{
private readonly DataContractDeserializer _dataContractDeserializer;
public EntityTypeDeserializerProvider(IServiceProvider rootContainer)
: base(rootContainer)
{
_dataContractDeserializer = new DataContractDeserializer(this);
}
public override ODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType)
{
if(edmType.Definition.TypeKind == EdmTypeKind.Complex || edmType.Definition.TypeKind == EdmTypeKind.Entity)
return _dataContractDeserializer;
else
return base.GetEdmTypeDeserializer(edmType);
}
}
As with the provider your custom _Deserializer should call through to the base implementation for everything that you do not need to customize. In the following implementation we are only trying to enforce the Order of the properties that are deserialized as well as calling the DataContract OnDeserializing and OnDeserialized methods, the rest of the deserialization is unaffected:
/// <summary>
/// OData serializer that oberys the DataMember Order Attribute and OnDeserializing and OnDeserialized attributes on the resource definition
/// </summary>
public class DataContractDeserializer : ODataResourceDeserializer
{
public DataContractDeserializer(ODataDeserializerProvider provider)
: base(provider) { }
public override object CreateResourceInstance(IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
var resource = base.CreateResourceInstance(structuredType, readContext);
var type = resource.GetType();
if(type.GetCustomAttributesData().Any(x => x.AttributeType == typeof(DataContractAttribute)))
{
// manually call OnDeserializing
var init = type.GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).FirstOrDefault(x => x.GetCustomAttributesData().Any(a => a.AttributeType == typeof(OnDeserializingAttribute)));
if(init != null)
{
init.Invoke(resource, new object[] { new StreamingContext(StreamingContextStates.Remoting, readContext ) });
}
}
return resource;
}
public override object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
var resource = base.ReadResource(resourceWrapper, structuredType, readContext);
var type = resource.GetType();
if (type.GetCustomAttributesData().Any(x => x.AttributeType == typeof(DataContractAttribute)))
{
// manually call OnDeserialized
var final = type.GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic).FirstOrDefault(x => x.GetCustomAttributesData().Any(a => a.AttributeType == typeof(OnDeserializedAttribute)));
if (final != null)
{
final.Invoke(resource, new object[] { new StreamingContext(StreamingContextStates.Remoting, readContext) });
}
}
return resource;
}
public override void ApplyStructuralProperties(object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
var type = resource.GetType();
var memberDescriptors = type.GetProperties().Where(x => x.HasAttribute<DataMemberAttribute>());
if (memberDescriptors.Any())
{
var orderedProperties = from p in resourceWrapper.Resource.Properties
let clsProperty = memberDescriptors.FirstOrDefault(m => m.Name == p.Name)
let memberAtt = (DataMemberAttribute)(clsProperty?.GetCustomAttributes(true).FirstOrDefault(a => a.GetType() == typeof(DataMemberAttribute)))
orderby (memberAtt?.Order).GetValueOrDefault()
select p;
foreach (var property in orderedProperties)
{
ApplyStructuralProperty(resource, property, structuredType, readContext);
}
}
else
base.ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext);
}
}
Finally, You need to replace the default DeserializerProvider registration with your own, the following is an example of an overload to MapODataServiceRoute that registers the DeserializerProvider from the previous 2 examples.
I have commented out an example of registering a specific SerializerProvider
private static ODataRoute MapODataServiceRoute(this HttpConfiguration configuration, string routeName,
string routePrefix, IEdmModel model, ODataBatchHandler batchHandler = null, ODataUriResolver uriResolver = null, IList<IODataRoutingConvention> routingConventions = null)
{
return configuration.MapODataServiceRoute(routeName, routePrefix, builder =>
builder
.AddService(ServiceLifetime.Singleton, sp => model)
//.AddService<ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new DefaultODataSerializerProvider(sp))
.AddService<ODataDeserializerProvider>(ServiceLifetime.Singleton, sp => new EntityTypeDeserializerProvider(sp))
.AddService(ServiceLifetime.Singleton, sp => batchHandler ?? new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer))
.AddService(ServiceLifetime.Singleton, sp => uriResolver ?? new ODataUriResolver())
.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp =>
routingConventions ??
ODataRoutingConventions.CreateDefaultWithAttributeRouting(routeName, configuration)
)
);
}
my first question that I can't find the exact answer elsewhere.
Using Automapper 4.2.1, Unity 4.01
I'm trying to have an Automapper profile call my service interface again to get a nested object to map to my destination property. Ultimately I would prefer to not use ResolveUsing, but if that works that is fine with me.
public class HeadlineMap : Profile
{
private readonly IDeserializeModels _deserialize;
public HeadlineMap(IDeserializeModels argDeserializeModels)
{
_deserialize = argDeserializeModels;
}
protected override void Configure()
{
CreateMap<LiveSiteHeadline, Headline>().
...
ForMember(t => t.VideoTitle, options => options.MapFrom(f => f.videoTitle)).
ForMember(t => t.Disclaimer, options => options.ResolveUsing<ResolveDisclaimerReference>().ConstructedBy(()=> new ResolveDisclaimerReference(_deserialize)));
}
}
public class ResolveDisclaimerReference : ValueResolver<LiveSiteHeadline, Object>
{
private readonly IDeserializeModels _deserialize;
public ResolveDisclaimerReference(IDeserializeModels argDeserializeModels)
{
_deserialize = argDeserializeModels;
}
protected override object ResolveCore(LiveSiteHeadline source)
{
return _deserialize.GetResponseObject(source.disclaimer.urlPattern, source.disclaimer.project, source.disclaimer.path);
}
}
I'm using the recommended 4.2.1 way of registering all of my maps within the Unity container:
var config = new MapperConfiguration(cfg =>
{
foreach (var profile in profiles)
{
cfg.AddProfile(profile);
}
});
container.RegisterType<MapperConfiguration>(new ContainerControlledLifetimeManager(),
new InjectionFactory(c => config))
.RegisterType<IMapper>(new InjectionFactory(c => c.Resolve<MapperConfiguration>().CreateMapper()));
But when I run the above I get -
"Missing type map configuration or unsupported mapping.".
Any ideas on what I'm missing? The other examples using ObjectFactory, Ninject don't seem to work as I must not be using those.