Localisation failing in Blazor WebAssembly - c#

I am having difficulties adding multiple languages in Blazor WASM. I've been following various guides online, but what I am finding is that trying to change the culture during runtime will fail, unless a non-default culture was set in Program.Main().
For example, in Program.Main(), if I put the following:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
var culture = System.Globalization.CultureInfo.GetCultureInfo("es-ES");
System.Globalization.CultureInfo.DefaultThreadCurrentCulture = culture;
System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = culture;
System.Globalization.CultureInfo.CurrentCulture = culture;
System.Globalization.CultureInfo.CurrentUICulture = culture;
await builder.Build().RunAsync();
}
And correspondingly, have 2 .resx files, for example:
BlazorControl.resx
BlazorControl.es.resx
Then when BlazorControl is displayed, the strings will correctly be in Spanish. I have then put in a click event handler on that control that changes the culture to English:
#inject Microsoft.Extensions.Localization.IStringLocalizer<PagingControls> Localiser
<h1 #onclick="OnClicked">#Localiser["Hello"]</h1>
#code {
private void OnClicked()
{
var culture = System.Globalization.CultureInfo.GetCultureInfo("en-GB");
System.Globalization.CultureInfo.DefaultThreadCurrentCulture = culture;
System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = culture;
System.Globalization.CultureInfo.CurrentCulture = culture;
System.Globalization.CultureInfo.CurrentUICulture = culture;
StateHasChanged();
}
}
That works fine, great.
However, if I do not set a non-default culture in Program.Main(), for example having Program.Main() as:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
and the click event as:
#inject Microsoft.Extensions.Localization.IStringLocalizer<PagingControls> Localiser
<h1 #onclick="OnClicked">#Localiser["Hello"]</h1>
#code {
private void OnClicked()
{
var culture = System.Globalization.CultureInfo.GetCultureInfo("es-ES");
System.Globalization.CultureInfo.DefaultThreadCurrentCulture = culture;
System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = culture;
System.Globalization.CultureInfo.CurrentCulture = culture;
System.Globalization.CultureInfo.CurrentUICulture = culture;
StateHasChanged();
}
}
Then nothing happens. The control is displayed in English on startup (correctly), but is not changed by the click event.
My interpretation is that Blazor does not load the non-default resource file at all unless it is specifically required before the application launches. My question is, how to get this working properly?
I am considering just making my own localiser with JSON files, but I would prefer to do things the Microsoft way if possible.
Note: I could just set the culture to Spanish on startup, then change it to English when it loads, but that would only be a 'solution' if there were only 2 languages on the system, whereas I plan to have more.

I've had the same problem, where it seemed like all the other languages didn't load, if not selected as default.
The problem was, that I used a library like https://github.com/Blazored/LocalStorage to store the selected language in the browser. It seemed like a cleaner solution, than writing some JS myself and calling it using interops.
However, the problem with this is, that in order to read and write values this way, blazor must already be initialized. The localization framework doesn't seem to like this very much, so I fixed it with a little js snippet in the header of my index.html.
<script>
if (!localStorage.getItem("appCulture")) localStorage.setItem("appCulture", navigator.language || navigator.userLanguage);
</script>
It simply makes sure, that there is a value present in the localStorage, by the time Program.cs executes. Previously, this happened after the execution of host.Build(), since I used C# and JS interops to achieve the same thing.

Related

Change culture in controller ASP.Net 5

I've searched around and haven't been able to find an answer that worked for me.
My question is: how to set the culture of the current response within a controller method.
The scenario is as follows. We have a general area of the web application, which works with culture set via a cookie, which works fine.
One small part of the website, though, needs to be in Danish always, except if a query parameter is set to, for instance, en-GB.
In the controller method receiving the request I add the culture change to the cookie, but the returned view is still displayed in the previous language. A refresh of the page will then display the view in the correct language, since the cookie now has the correct values.
What I would like is to also change the culture of the current request besides the cookie, so that the returned view is displayed in the correct language.
My controller's code is as follows:
[Route("{organizationId}/{arrangementId}")]
public IActionResult LandingPage(int organizationId, int arrangementId, [FromQuery] string lang, [FromQuery] bool? allowup)
{
if (organizationId <= 0 || arrangementId <= 0)
{
return RedirectToAction("Index");
}
_utils.SetLoginRedirectUrl($"/Skolevaccination/Questionnaire/{organizationId}/{arrangementId}", Response);
SetLanguage(!string.IsNullOrWhiteSpace(lang) ? lang : "da-DK");
ViewBag.allowup = allowup ?? false;
return View("Index");
}
private void SetLanguage(string lang)
{
string cultureCookieValue;
switch (lang)
{
case "gb":
case "en-GB":
cultureCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("en-GB"));
_cultureService.SetCulture(new CultureInfo("en-GB"));
break;
case "dk":
case "da-DK":
cultureCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("da-DK"));
_cultureService.SetCulture(new CultureInfo("da-DK"));
break;
default:
_logger.LogInformation($"Trying to set a language {lang}, which isn't supported.");
return;
}
Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, cultureCookieValue);
}
The call to _cultureService.SetCulture(new CultureInfo("da-DK")) uses the following code, which are my attempts at solving the problem:
public void SetCulture(CultureInfo cultureInfo)
{
if (!GetSupportedCultures().Contains(cultureInfo))
{
throw new CultureNotFoundException("The given culture is not supported.");
}
Thread.CurrentThread.CurrentUICulture = cultureInfo;
Thread.CurrentThread.CurrentCulture = cultureInfo;
//CultureInfo.CurrentCulture = cultureInfo;
//CultureInfo.CurrentUICulture = cultureInfo;
}
As seen I tried setting the thread's current culture and using the static methods on CultureInfo as well (the commented out lines).
A lot of the posts I found online mentioned that setting the thread's culture was a thing used previous to ASP.Net 5 (ASP.Net core) and that all culture settings are now set up in the startup file. But if I change it in the startup file, I change it for the whole website, which is not what we want. And I need to set the cookie as well, since subsequent pages on the small part of the whole website needs to use the language set via the URL or the default one, but their urls will not have the language defined there.
If this can't be solved by setting the current culture directly, could it perhaps be solved by setting up a routing area for this specific part of the web app?
Thanks in advance!
EDIT 2018/10/05
I found a solution after looking closer at the view and its IViewLocalizer.
After looking closer at the documentation for the localizer classes I found that you can use their method WithCulture. This returns a new localizer for the same strings but for the given culture.
But after still having troubles with the view not being localized correctly, I found that the injected localizer in the view isn't the same as the one injected in the controller, so to fully localize the request, the localizer in both the controller and the view needs to be exchanged using the WithCulture method.
My solution ended up like the following:
In the controller:
private void SetLanguage(string lang, string defaultLang)
{
if (string.IsNullOrWhiteSpace(lang) || !_cultureService.IsSupportedCulture(lang)) {
_logger.LogInformation($"Tried to change culture to unsupported culture (to {lang}). Culture has been set to default culture {defaultLang}.");
lang = defaultLang;
}
string cultureCookieValue;
switch (lang)
{
case "gb":
case "en-GB":
cultureCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("en-GB"));
_localizer = _localizer.WithCulture(new CultureInfo("en-GB"));
ViewBag.changeCulture = true;
ViewBag.lang = "en-GB";
break;
case "dk":
case "da-DK":
cultureCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("da-DK"));
_localizer = _localizer.WithCulture(new CultureInfo("da-DK"));
ViewBag.changeCulture = true;
ViewBag.lang = "da-DK";
break;
default:
_logger.LogInformation($"Trying to set a language {lang}, which isn't supported.");
return;
}
Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, cultureCookieValue);
}
When calling this method in the controller, I set some data in the view bag, which is used in the view to also create a new localizer there:
#inject IViewLocalizer ViewLocalizer
#{
IHtmlLocalizer Localizer = ViewLocalizer;
if (ViewBag.changeCulture) {
var lang = ViewBag.lang;
Localizer = Localizer.WithCulture(new CultureInfo(lang));
}
var allowUserPasswordLogin = ViewBag.allowup;
}
When calling the WithCulture method the returned type is IHtmlLocalizer, which can't be cast back to a IViewLocalizer, but IViewLocalizer extends the IHtmlLocalizer interface, so it all works out with a few lines of code.

How to disable localizer in Bot Framework

Is there a way to completely disable the Bot Framework default localizer? The localizer seems to translate prompts inconsistently and in unexpected places. Also my bot sometimes cannot understand common user inputs (help, quit, back, yes, no) since it seems to be expecting them in a different language.
I didn't configure any localization settings so I'm guessing this behaviour is caused by the default Bot Framework localization. I'm looking for a way to completely avoid any attempts to translation and keep my bot using English only.
Have a look to the dedicated section of the documentation about localization: https://learn.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-formflow-localize
The bot framework is automatically using the locale from the message to select the right Resources, but you can override this information by setting your thread's CurrentUICulture and CurrentCulture, and ideally also your Locale property in your MessageActivity
CultureInfo lang = ...;
Thread.CurrentThread.CurrentCulture = lang;
Thread.CurrentThread.CurrentUICulture = lang;
context.Activity.AsMessageActivity().Locale = lang.ToString();
Don't forget to set it for each Thread that will send messages as there is no global solution to switch the language.
If you want to go deeper, you can have a look to the bot framework sources:
LocalizedScope class
SetAmbientThreadCulture class in
PostToBot
Edit:
For the prompts part, if I remember well I had to create my own public abstract class MyPrompt<T, U> : IDialog<T> and in that one:
protected virtual IMessageActivity MakePrompt(IDialogContext context, string prompt, IReadOnlyList<U> options = null, IReadOnlyList<string> descriptions = null, string speak = null)
{
var msg = context.MakeMessage();
// force Culture
CultureInfo lang = ...;
if (lang != null)
{
Thread.CurrentThread.CurrentCulture = lang;
Thread.CurrentThread.CurrentUICulture = lang;
context.Activity.AsMessageActivity().Locale = lang.ToString();
}
if (options != null && options.Count > 0)
{
promptOptions.PromptStyler.Apply(ref msg, prompt, options, descriptions, speak);
}
else
{
promptOptions.PromptStyler.Apply(ref msg, prompt, speak);
}
return msg;
}

SignalR server --> client call not working

I'm currently using SignalR to communicate between a server and multiple separate processes spawned by the server itself.
Both Server & Client are coded in C#. I'm using SignalR 2.2.0.0
On the server side, I use OWIN to run the server.
I am also using LightInject as an IoC container.
Here is my code:
public class AgentManagementStartup
{
public void ConfigurationOwin(IAppBuilder app, IAgentManagerDataStore dataStore)
{
var serializer = new JsonSerializer
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
TypeNameHandling = TypeNameHandling.Auto,
TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple
};
var container = new ServiceContainer();
container.RegisterInstance(dataStore);
container.RegisterInstance(serializer);
container.Register<EventHub>();
container.Register<ManagementHub>();
var config = container.EnableSignalR();
app.MapSignalR("", config);
}
}
On the client side, I register this way:
public async Task Connect()
{
try
{
m_hubConnection = new HubConnection(m_serverUrl, false);
m_hubConnection.Closed += OnConnectionClosed;
m_hubConnection.TraceLevel = TraceLevels.All;
m_hubConnection.TraceWriter = Console.Out;
var serializer = m_hubConnection.JsonSerializer;
serializer.TypeNameHandling = TypeNameHandling.Auto;
serializer.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
m_managementHubProxy = m_hubConnection.CreateHubProxy(AgentConstants.ManagementHub.Name);
m_managementHubProxy.On("closeRequested", CloseRequestedCallback);
await m_hubConnection.Start();
}
catch (Exception e)
{
m_logger.Error("Exception encountered in Connect method", e);
}
}
On the server side I send a close request the following way:
var managementHub = GlobalHost.ConnectionManager.GetHubContext<ManagementHub>();
managementHub.Clients.All.closeRequested();
I never receive any callback in CloseRequestedCallback. Neither on the Client side nor on the server side I get any errors in the logs.
What did I do wrong here ?
EDIT 09/10/15
After some research and modifications, I found out it was linked with the replacement of the IoC container. When I removed everything linked to LightInject and used SignalR as is, everything worked. I was surprised about this since LightInject documented their integration with SignalR.
After I found this, I realised that the GlobalHost.DependencyResolver was not the same as the one I was supplying to the HubConfiguration. Once I added
GlobalHost.DependencyResolver = config.Resolver;
before
app.MapSignalR("", config);
I am now receiving callbacks within CloseRequestedCallback. Unfortunately, I get the following error as soon as I call a method from the Client to the Server:
Microsoft.AspNet.SignalR.Client.Infrastructure.SlowCallbackException
Possible deadlock detected. A callback registered with "HubProxy.On"
or "Connection.Received" has been executing for at least 10 seconds.
I am not sure about the fix I found and what impact it could have on the system. Is it OK to replace the GlobalHost.DependencyResolver with my own without registering all of its default content ?
EDIT 2 09/10/15
According to this, changing the GlobalHost.DependencyResolver is the right thing to do. Still left with no explanation for the SlowCallbackException since I do nothing in all my callbacks (yet).
Issue 1: IoC Container + Dependency Injection
If you want to change the IoC for you HubConfiguration, you also need to change the one from the GlobalHost so that returns the same hub when requesting it ouside of context.
Issue 2: Unexpected SlowCallbackException
This exception was caused by the fact that I was using SignalR within a Console Application. The entry point of the app cannot be an async method so to be able to call my initial configuration asynchronously I did as follow:
private static int Main()
{
var t = InitAsync();
t.Wait();
return t.Result;
}
Unfortunately for me, this causes a lot of issues as described here & more in details here.
By starting my InitAsync as follow:
private static int Main()
{
Task.Factory.StartNew(async ()=> await InitAsync());
m_waitInitCompletedRequest.WaitOne(TimeSpan.FromSeconds(30));
return (int)EndpointErrorCode.Ended;
}
Everything now runs fine and I don't get any deadlocks.
For more details on the issues & answers, you may also refer to the edits in my question.

Page.Culture vs Thread.CurrentThread.CurrentCulture

I have been using Thread.CurrentThread.CurrentUICulture with System.Threading and System.Globalization, for a while now to manually set the language used by my ASP.net pages, mainly WebForms and WebPages with Razor.
See MSDN: Thread.CurrentThread.CurrentUICulture
I recently read a tutorial that was using Page.UICulture instead (actually, UICulture which appears to be strongly typed). To my surprise I ended up with exactly the same result; they both changed my websites' ui language settings and read the correct resource file.
See MSDN: Page.UICulture
To me, the Thread.CurrentUICulture makes more sense (I say that intuitively, since it's literally "changing the culture of the current thread").
But calling Page.Culture is much easier and doesn't require to call yet another pair of ASP.net using, so I've settled for that solution for now.
Is there a fundamental difference between the two or are they perfectly interchangeable ?
The reason why I worry is that I have a bunch of old websites developed with the first method and I am afraid to run into interchangeability conflicts if I update them to the second one rashly.
Note: I usually focus on UICulture in my line of work and Culture is very accessory to what I do, but I am asking the question for the both of them.
Page.UICulture is a wrapper around Thread.CurrentThread property and is ment for internal .NET framework use:
This property is a shortcut for the CurrentThread property. The culture is a property of the executing thread
This API supports the .NET Framework infrastructure and is not intended to be used directly from your code.
Looking at the source code, you can clearly see that:
public string UICulture
{
set
{
CultureInfo newUICulture = null;
if(StringUtil.EqualsIgnoreCase(value, HttpApplication.AutoCulture))
{
CultureInfo browserCulture = CultureFromUserLanguages(false);
if(browserCulture != null)
{
newUICulture = browserCulture;
}
}
else if(StringUtil.StringStartsWithIgnoreCase(value, HttpApplication.AutoCulture))
{
CultureInfo browserCulture = CultureFromUserLanguages(false);
if(browserCulture != null)
{
newUICulture = browserCulture;
}
else
{
try
{
newUICulture = HttpServerUtility.CreateReadOnlyCultureInfo(value.Substring(5));
}
catch {}
}
}
else
{
newUICulture = HttpServerUtility.CreateReadOnlyCultureInfo(value);
}
if (newUICulture != null)
{
Thread.CurrentThread.CurrentUICulture = newUICulture;
_dynamicUICulture = newUICulture;
}
}
get { return Thread.CurrentThread.CurrentUICulture.DisplayName; }
}
They do exactly the same thing.
As you can see on the documentation page:
This property is a shortcut for the CurrentThread property. The culture is a property of the executing thread

DisplayModeProvider.Instance.Modes.Add Called unconditionally

Our current project is utilizing the following code snippet found online to direct users to a mobile view as required:
DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("Mobile")
{
ContextCondition = (context => RequirementsHelper.BrowserIsMobile(context.GetOverriddenUserAgent()))
});
It allows us to easily direct users to index.cshtml or index.mobile.cshtml depending on their user agent string. So far so good.
Extending off this idea, I was tempted to implement a localization engine based on DisplayModeProvider (as different versions of our sites are to look significantly different, but function almost identically.)
So my initial quick test was to create the following method:
protected void DisplayNZSkin()
{
System.Web.WebPages.DisplayModeProvider.Instance.Modes.Add(new System.Web.WebPages.DefaultDisplayMode("NZ")
{
ContextCondition = (context => true)
});
}
Which I'll be able to call when I determine that the NZ skin is to show (which unfortunately relies on some database calls). The idea was that when this is called, it forces the rendering of index.nz.cshtml.
It was a bad idea. The mere existence of this method in my controller, uncalled, made all pages render their .nz versions.
Stepping through, shows that the code is always executed. The Function (context => true) is invoked in every page call.
What's going on there?
I had almost the same Idea, so I'll put my version and let's see if it solves your problem.
I started with this article
http://www.codeproject.com/Articles/576315/LocalizeplusyourplusMVCplusappplusbasedplusonplusa
but changed this to use this because the filter was after the modelbinding.
public class ExtendedControllerFactory:DefaultControllerFactory
{
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(GetCultureFromURLOrAnythingElse());
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(GetCultureFromURLOrAnythingElse());
return base.GetControllerInstance(requestContext, controllerType);
}
and this way the global.asax should have this
protected void Application_Start()
{
ControllerBuilder.Current.SetControllerFactory(new ExtendedControllerFactory());
System.Web.WebPages.DisplayModeProvider.Instance.Modes.Insert(0, new System.Web.WebPages.DefaultDisplayMode("EN")
{
ContextCondition = context => Thread.CurrentThread.CurrentCulture.Name.Equals("en-US", StringComparison.CurrentCultureIgnoreCase)
});
System.Web.WebPages.DisplayModeProvider.Instance.Modes.Insert(0, new System.Web.WebPages.DefaultDisplayMode("BR")
{
ContextCondition = context => Thread.CurrentThread.CurrentCulture.Name.Equals("pt-BR", StringComparison.CurrentCultureIgnoreCase)
});
I think this should do what you want, but I never tried this in production.

Categories