I'm working on adding localization to my web application. I have configured the IStringLocalizer and it is correctly reading string resources from two different resx files, depending on the browser setting. It then maps those string resources to ViewData, from which my View is getting text in correct language (not sure if that is the best approach, but for now I don't want to spent more time on this).
The thing is that I also have a drop down list in my UI, that allows users to manually switch language. I'm reading the value set by user in my controller action and adding it to cookies, but now I'd also like to set my applications' culture to the one matching the string in cookie.
Is it possible to set application culture from the controller action in MVC Core? If yes, then how to do this correctly?
EDIT:
I have just learned that I can do something like this:
<a class="nav-item nav-link" asp-route-culture="en-US">English</a>
and it will add ?culture=en-US to my route, which will set culture for the page for me. Is there any way to do the same without having to keep it in an address bar?
EDIT 2:
Regarding answer by Adam Simon:
CookieRequestCultureProvider is what I'd like to use in my app, but the problem is that it is not producing any cookie. Documentation says that .net core will resolve which provider to use by checking which will give a working solution, starting from QueryStringRequestCultureProvider, then going to CookieRequestCultureProvider, then other providers.
My current Startup looks like this:
public class Startup
{
private const string defaultCulutreName = "en-US";
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo(defaultCulutreName),
new CultureInfo("pl-PL")
};
options.DefaultRequestCulture = new RequestCulture(defaultCulutreName, defaultCulutreName);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
//TRIED PLACING IT BEFORE AND AFTER UseRequestLocalization
//CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL"));
app.UseRequestLocalization(app.ApplicationServices
.GetService<IOptions<RequestLocalizationOptions>>().Value);
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL"));
app.UseMvc(ConfigureRoutes);
}
private void ConfigureRoutes(IRouteBuilder routeBuilder)
{
routeBuilder.MapRoute("Default", "{controller=About}/{action=About}");
}
}
Regarding CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL")) I have tried putting it in RequestLocalizationOptions in ConfigureServices, in Configure before UseRequestLocalization and after that. All with the same result.
The following "problems" appear with this solution:
MakeCookieValue method is not producing any .AspNetCore.Culture cookie
Chrome browser with language set to PL is using pl-PL culture
correctly, yet Firefox is using en-US culture with language set to PL
in options (despite commenting out options.DefaultRequestCulture =
new RequestCulture(defaultCulutreName, defaultCulutreName) line)
Somehow my localization is working by default without using query
strings nor cookies to provide culture for application, but this is
not how I'd like it to work, as I do not have any control over it
Somehow you must tie the selected culture to the user so if you don't want to carry it around in the URL, you must find another way to retain this piece of information between requests. Your options:
cookie
session
database
HTTP header
hidden input
Under normal circumstances using a cookie to store the language preference is a perfect choice.
In ASP.NET Core the best place to retrieve and set the culture for the current request is a middleware. Luckily, the framework includes one, which can be placed in the request pipeline by calling app.UseRequestLocalization(...) in your Startup.Configure method. By default this middleware will try to pick up the current culture from the request URL, cookies and Accept-Language HTTP header, in this order.
So, to summarize: you need to utilize the request localization middleware, store the user's culture preference in a cookie formatted like c=%LANGCODE%|uic=%LANGCODE% (e.g. c=en-US|uic=en-US) and you are done.
You find all the details in this MSDN article.
Bonus:
It then maps those string resources to ViewData, from which my View is
getting text in correct language (not sure if that is the best
approach, but for now I don't want to spent more time on this).
Passing localized texts to views in ViewData is cumbersome and error-prone. In ASP.NET Core we have view localization for this purpose. You just need to inject the IViewLocalizer component into your views to get a nice and convenient way to access your localized text resources. (Under the hood IViewLocalizer uses IStringLocalizer.)
About EDIT 2
MakeCookieValue method is not producing any .AspNetCore.Culture cookie
CookieRequestCultureProvider.MakeCookieValue method is just a helper to generate a cookie value in the correct format. It just returns a string and that's all. But even if it were meant to add the cookie to the response, calling it in Startup.Configure would be completely wrong as you configure the request pipeline there. (It seems to me you're a bit confused about request handling and middlewares in ASP.NET Core so I suggest studying this topic.)
So the correct setup of the request pipeline is something like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
#region Localization
// REMARK: you may refactor this into a separate method as it's better to avoid long methods with regions
var supportedCultures = new[]
{
new CultureInfo(defaultCultureName),
new CultureInfo("pl-PL")
};
var localizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(defaultCultureName, defaultCultureName),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures,
// you can change the list of providers, if you don't want the default behavior
// e.g. the following line enables to pick up culture ONLY from cookies
RequestCultureProviders = new[] { new CookieRequestCultureProvider() }
};
app.UseRequestLocalization(localizationOptions);
#endregion
app.UseStaticFiles();
app.UseMvc(ConfigureRoutes);
}
(A remark on the above: it's unnecessary to register RequestLocalizationOptions in the DI container.)
Then you can have some controller action setting the culture cookie:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult SetCulture(string culture, string returnUrl)
{
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
// making cookie valid for the actual app root path (which is not necessarily "/" e.g. if we're behind a reverse proxy)
new CookieOptions { Path = Url.Content("~/") });
return Redirect(returnUrl);
}
Finally, an example how to invoke this from a view:
#using Microsoft.AspNetCore.Localization
#using Microsoft.AspNetCore.Http.Extensions
#{
var httpContext = ViewContext.HttpContext;
var currentCulture = httpContext.Features.Get<IRequestCultureFeature>().RequestCulture.UICulture;
var currentUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, httpContext.Request.QueryString);
}
<form asp-action="SetCulture" method="post">
Culture: <input type="text" name="culture" value="#currentCulture">
<input type="hidden" name="returnUrl" value="#currentUrl">
<input type="submit" value="Submit">
</form>
Chrome browser with language set to PL is using pl-PL culture correctly, yet Firefox is using en-US culture with language set to PL in options (despite commenting out options.DefaultRequestCulture =
new RequestCulture(defaultCulutreName, defaultCulutreName) line)
I suspect Chrome browser sends the language preference in the Accept-Language header while FF not.
Somehow my localization is working by default without using query strings nor cookies to provide culture for application, but this is not how I'd like it to work, as I do not have any control over it
I repeat:
By default this middleware will try to pick up the current culture from the request URL, cookies and Accept-Language HTTP header, in this order.
You can configure this behavior by changing or replacing the RequestLocalizationOptions.RequestCultureProviders list.
Related
I have a .NET 6 Bloazor PWA application where i want to let the customer choose the language for the UI of the app.
I have prepared resource .resx files and set the Default language in Program.cs and set the AddLocalization service, but when i chose the language from the combo element the localized text on the page do not change at all.
Moreover i always see the 'keys' instead their corresponding strings in resurces file; it looks like the system do to find resource files at all.
where am i wrong ?
I used:
(.csproj file)
<PropertyGroup>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
(Program.cs):
builder.Services.AddLocalization(Options => Options.ResourcesPath= "ResourceFiles");
var host = builder.Build(); CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("it-IT");
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("it-IT");
await host.RunAsync();
then i defined the ResourceFiles directory
with inside Resource.resx , Resources.it.resx with two text records with
(Resources.it.resx and Resources.resx)
Name="intro1" value="Benvenuto!"
Name="intro2" value="Grazie per utilizzare la nostra app" `
(Resources.en.resx)
Name="intro1" value="Welcome!"
Name="intro2" value="Thank you for using our app" `
and in addition an empty Resources class to collect resx files and assign later it to IStringLocalizer in the razor page
namespace BlazorAppPWAalone.ResourceFiles
{
public class Resources
{
}
}
in my page:
#using System.Globalization
#using Microsoft.Extensions.Localization
#inject IStringLocalizer<Resources> Loc
<h1>Language change:</h1>
<select name="lingue" id="idlingua" #bind="sceltaLingua">
<option value="it-IT">Italiano</option>
<option value="en-US">Inglese</option>
</select>
<p>
<b>CurrentCulture</b>: #CultureInfo.CurrentCulture
<b>CurrentUICulture</b>: #CultureInfo.CurrentUICulture
</p>
<p>#Loc["intro1"], #Loc["intro2"]</p>
#code {
private string _sceltaLingua;
public string sceltaLingua
{
get
{
return _sceltaLingua;
}
set
{
_sceltaLingua = value;
CambiaLingua(_sceltaLingua);
}
}
private void CambiaLingua(string newlang)
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(newlang);
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(newlang);
StateHasChanged();
}
}
When i choose the language the #CurrentCulture and #CurrentUICulture variables show correctly the selected language but #Loc["intro1"], #Loc["intro2"] never change, it always displays default (italian) resource values.
Why ? where is my code 'bug' ?
I´ve not done this in WASM yet but in the documentation the CultureSelector does not actually set CurrentCulture to change the language but sets a key value pair in the browsers local storage and triggers a reload which causes DefaultThreadCurrentCulture and
DefaultThreadCurrentUICulture to be set to the culture specified in local storage.
And judging from this:
Always set DefaultThreadCurrentCulture and DefaultThreadCurrentUICulture to the same culture in order to use IStringLocalizer and IStringLocalizer.
just setting CurrentCulture and CurrentUICulture will probably not work.
I don´t know if you can just set DefaultThreadCurrentCulture and DefaultThreadCurrentUICulture from the component and reload or if you need to follow the documentations exact approach but that should be easy for you to try out.
We are using Azure b2c to handle our logins on our .net core MVC site.
We would like to use the optional state parameter to hold onto some data/a value between the initial request to the site (this value would likely be in a querystring param) which is then sent off to b2c to login, and the successfully logged in return back to the site.
OpenIDConnect allow the setting of this state value in the request, and will pass it back with the token response.
It appears that setting the value is relatively simple; in the OnRedirectToIdentityProvider event in the OpenIdConnectOptions like so:
public Task OnRedirectToIdentityProvider(RedirectContext context){
...
context.ProtocolMessage.SetParameter("state", "mystatevalue");
...
}
however, I cannot see how to get this value back again when the user is returned.
I can see that the OnTicketReceived event is hit, and this has a TicketReceivedContext which has a Form property with a state value in it, however this is still encrypted.
Where would i be able to get the un-encrypted value back from?
I have had a look at the Azure docs for b2c but I cannot find an example on this.
thanks
Managed to get this working by using the OnTokenValidated event.
This is able to get the unencrypted parameter as below.
...//first set up the new event
options.Events = new OpenIdConnectEvents()
{
...
OnTokenValidated = OnTokenValidated
};
...
private Task OnTokenValidated(TokenValidatedContext tokenValidatedContext)
{
var stateValue = tokenValidatedContext.ProtocolMessage.GetParameter("state");
if (stateValue != null)
{
//do something with that value..
}
return Task.CompletedTask;
}
Web API uses the Json.Net formatter to serialise its JSON responses which allows you to customise the format of the generated JSON very easily for the entire application at startup using:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
This allows you resolve the issues between C# syntax preferring PascalCase and javascript based clients preferring camelCase. However setting this globally on the API without taking into consideration who the client request is actually coming from seems to assume that an API will only have 1 type of client and whatever you set for your API is just the way it has to be.
With multiple client types for my API's (javascript, iOS, Android, C#), I'm looking for a way to set the Json.Net SerializerSettings per request such that the client can request their preferred format by some means (perhaps a custom header or queryString param) to override the default.
What would be the best way to set per-request Json.Net SerializerSettings in Web API?
With a bit of help from Rick Strahl's blog post on creating a JSONP media type formatter, I have come up with a solution that allows the API to dynamically switch from camelCase to PascalCase based on the client request.
Create a MediaTypeFormatter that derives from the default JsonMediaTypeFormatter and overrides the GetPerRequestFormatterInstance method. This is where you can implement your logic to set your serializer settings based on the request.
public class JsonPropertyCaseFormatter : JsonMediaTypeFormatter
{
private readonly JsonSerializerSettings globalSerializerSettings;
public JsonPropertyCaseFormatter(JsonSerializerSettings globalSerializerSettings)
{
this.globalSerializerSettings = globalSerializerSettings;
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
}
public override MediaTypeFormatter GetPerRequestFormatterInstance(
Type type,
HttpRequestMessage request,
MediaTypeHeaderValue mediaType)
{
var formatter = new JsonMediaTypeFormatter
{
SerializerSettings = globalSerializerSettings
};
IEnumerable<string> values;
var result = request.Headers.TryGetValues("X-JsonResponseCase", out values)
? values.First()
: "Pascal";
formatter.SerializerSettings.ContractResolver =
result.Equals("Camel", StringComparison.InvariantCultureIgnoreCase)
? new CamelCasePropertyNamesContractResolver()
: new DefaultContractResolver();
return formatter;
}
}
Note that I take a JsonSerializerSettings argument as a constructor param so that we can continue to use WebApiConfig to set up whatever other json settings we want to use and have them still applied here.
To then register this formatter, in your WebApiConfig:
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter());
config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local;
config.Formatters.Insert(0,
new JsonPropertyCaseFormatter(config.Formatters.JsonFormatter.SerializerSettings));
Now requests that have a header value of X-JsonResponseCase: Camel will receive camel case property names in the response. Obviously you could change that logic to use any header or query string param you like.
We have a WebBrowser embedded in our Windows Phone 7x application. This WebBrowser is pointed at our web servers. We need to be able to differentiate between a request coming from the app and a request coming from the native browser (or a WebBrowser embedded in another app, for instance). To do this we'd like to modify the User-Agent of all HTTP requests coming from said WebBrowser.
However, I can't find a way to do this. My initial thought was simply to override the Navigate functions adding "additionalHeaders." Unfortunately the WebBrowser class is sealed, so that option wasn't an option at all. I've searched high and low for a property or handler that's exposed that I might be able to take advantage of to no avail.
So, in short, is there a way to modify the User-Agent for a WebBrowser for all outbound HTTP requests?
I know this question is old, but in case this is of use to anyone, you could always use this for the WebBrowser's navigating event:
void wb_Navigating(object sender, NavigatingEventArgs e)
{
if (!e.Uri.ToString().Contains("!!!"))
{
e.Cancel = true;
string url = e.Uri.ToString();
if (url.Contains("?"))
url = url + "&!!!";
else
url = url + "?!!!";
wb.Navigate(new Uri(url), null, "User-Agent: " + "Your User Agent");
}
}
You just add "!!!" to all the urls for navigations that have your custom user agent. If the URL doesn't contain "!!!", it is a request from a clicked link and the WebBrowser cancels the navigation, and re-navigates with your custom user agent and "!!!" in the query string.
I tried a similar approach to msbg, where you store the URL in memory to avoid double checking it, and avoid modifying it with !!!. However, that approach doesn't preserve POST data, so it won't help me.
List<string> recentlyRequestedUrls = new List<string>();
void wb_Navigating(object sender, NavigatingEventArgs e)
{
if(!recentlyRequestedUrls.Contains(e.Uri.ToString()))
{
//new request, reinitiate it ourselves and save that we did to avoid infinite loop.
e.Cancel = true;
string url = e.Uri.ToString();
recentlyRequestedUrls.Add(url);
webBrowser1.Navigate(new Uri(url), null, "User-Agent: Your_User_Agent");
}
}
Set the user agent through additional headers, when invoking the Navigate method. Details here.
Investigating the Web API as part of an MVC 4 project as an alternative way to provide an AJAX-based API. I've extended AuthorizeAttribute for the MVC controllers such that, if an AJAX request is detected, a JSON-formatted error is returned. The Web API returns errors as HTML. Here's the AuthorizeAttribute that I'm using with the MVC controllers:
public class AuthorizeAttribute: System.Web.Mvc.AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
base.HandleUnauthorizedRequest(filterContext);
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "area", "" },
{ "controller", "Error" },
{ "action", ( filterContext.HttpContext.Request.IsAjaxRequest() ? "JsonHttp" : "Http" ) },
{ "id", "401" },
});
}
}
How could I reproduce this to provide equivalent functionality for the Web API?
I realize that I need to extend System.Web.Http.AuthorizeAttribute instead of System.Web.Mvc.AuthorizeAttribute but this uses an HttpActionContext rather than an AuthorizationContext and so I'm stuck by my limited knowledge of the Web API and the seemingly incomplete documentation on MSDN.
Am I even correct in thinking that this would be the correct approach?
Would appreciate any guidance.
To get the equivalent functionality in a Web API filter you can set the HttpActionContext.Response property to an instance of HttpResponseMessage that has the right redirect status code and location header:
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) {
var response = new HttpResponseMessage(HttpStatusCode.Redirect);
response.Headers.Location = new Uri("my new location");
actionContext.Response = response;
}
I would very much go with Marcin's answer - at the end of the day, he has written the code!
All I would add is that as Marcin is saying, your best bet is to have a dedicated controller to return the errors as appropriate - rather than setting the response code 401 with JSON content in the attribute.
The main reason is that Web API does the content-negotiation for you and if you want to do it yourself (see if you need to serve JSON or HTML) you lose all that functionality.