How can I (in ASP .NET MVC) get the CultureInfo of the current visitor (based on his/her browser languages)?
I have no idea where to start. I tried looking into the "Accept-Languages" header sent by the browser. But is that the best way of doing it?
Request.UserLanguages is the property you're looking for. Just keep in mind that this array may contain arbitrary (even non-exsitent) languages as set by request headers.
UPDATE
Example:
// Get Browser languages.
var userLanguages = Request.UserLanguages;
CultureInfo ci;
if (userLanguages.Count() > 0)
{
try
{
ci = new CultureInfo(userLanguages[0]);
}
catch(CultureNotFoundException)
{
ci = CultureInfo.InvariantCulture;
}
}
else
{
ci = CultureInfo.InvariantCulture;
}
// Here CultureInfo should already be set to either user's prefereable language
// or to InvariantCulture if user transmitted invalid culture ID
Asp.Net Core version: using RequestLocalization ie the culture is retrieved form the HTTP Request.
in Startup.cs - Configure
app.UseRequestLocalization();
Then in your Controller/Razor Page.cs
var locale = Request.HttpContext.Features.Get<IRequestCultureFeature>();
var BrowserCulture = locale.RequestCulture.UICulture.ToString();
You can use code similar to the following to get various details from your user (including languages):
MembershipUser user = Membership.GetUser(model.UserName);
string browser = HttpContext.Request.Browser.Browser;
string version = HttpContext.Request.Browser.Version;
string type = HttpContext.Request.Browser.Type;
string platform = HttpContext.Request.Browser.Platform;
string userAgent = HttpContext.Request.UserAgent;
string[] userLang = HttpContext.Request.UserLanguages
It appears Request.UserLanguages is not available in later mvc versions (Asp.net core mvc 2.0.2 didn't have it.)
I made an extension method for HTTPRequest. Use it as follows:
var requestedLanguages = Request.GetAcceptLanguageCultures();
The method will give you the cultures from the Accept-Language header in order of preference (a.k.a. "quality").
public static class HttpRequestExtensions
{
public static IList<CultureInfo> GetAcceptLanguageCultures(this HttpRequest request)
{
var requestedLanguages = request.Headers["Accept-Language"];
if (StringValues.IsNullOrEmpty(requestedLanguages) || requestedLanguages.Count == 0)
{
return null;
}
var preferredCultures = requestedLanguages.ToString().Split(',')
// Parse the header values
.Select(s => new StringSegment(s))
.Select(StringWithQualityHeaderValue.Parse)
// Ignore the "any language" rule
.Where(sv => sv.Value != "*")
// Remove duplicate rules with a lower value
.GroupBy(sv => sv.Value).Select(svg => svg.OrderByDescending(sv => sv.Quality.GetValueOrDefault(1)).First())
// Sort by preference level
.OrderByDescending(sv => sv.Quality.GetValueOrDefault(1))
.Select(sv => new CultureInfo(sv.Value.ToString()))
.ToList();
return preferredCultures;
}
}
Tested with ASP.NET Core MVC 2.0.2
It's similar to #mare's answer, but a bit more up-to-date and the q (quality) is not ignored. Also, you may want to append the CultureInfo.InvariantCulture to the end of the list, depending on your usage.
I am marking this question for myself with a star and sharing here some code that essentially turns the Request.UserLanguages into an array of CultureInfo instances for further use in your application. It is also more flexible to work with CultureInfo than just the ISO codes, because with CultureInfo you get access to all the properties of a culture (like Name, Two character language name, Native name, ...):
// Create array of CultureInfo objects
string locale = string.Empty;
CultureInfo[] cultures = new CultureInfo[Request.UserLanguages.Length + 1];
for (int ctr = Request.UserLanguages.GetLowerBound(0); ctr <= Request.UserLanguages.GetUpperBound(0);
ctr++)
{
locale = Request.UserLanguages[ctr];
if (!string.IsNullOrEmpty(locale))
{
// Remove quality specifier, if present.
if (locale.Contains(";"))
locale = locale.Substring(0, locale.IndexOf(';'));
try
{
cultures[ctr] = new CultureInfo(locale, false);
}
catch (Exception) { continue; }
}
else
{
cultures[ctr] = CultureInfo.CurrentCulture;
}
}
cultures[Request.UserLanguages.Length] = CultureInfo.InvariantCulture;
HTH
var userLanguage = CultureInfo.CurrentUICulture;
Related
I have found this link as a guide to make a multi language website which it should run with users preferred language which they have set on their browsers.
Get CultureInfo from current visitor and setting resources based on that?
as you see it has this code to do it
// Get Browser languages.
var userLanguages = Request.UserLanguages;
CultureInfo ci;
if (userLanguages.Count() > 0)
{
try
{
ci = new CultureInfo(userLanguages[0]);
}
catch(CultureNotFoundException)
{
ci = CultureInfo.InvariantCulture;
}
}
else
{
ci = CultureInfo.InvariantCulture;
}
// Here CultureInfo should already be set to either user's preferable language
// or to InvariantCulture if user transmitted invalid culture ID
but my question is that I do not know what exactly is the duty of CultureInfo.InvariantCulture and it does not work at all in my project. it is always null.
I changed the code to this , It works fine but I am not sure about possible exceptions may it have. I really appreciate any kind of help.
here is what I have and it works completely fine but just not sure about possible exceptions. I want the default language to be "en-US"
public ActionResult Index()
{
CultureInfo ci;
var userLanguages = Request.UserLanguages;
if (userLanguages == null)
{
ci = new CultureInfo("en-US");
}
else if (userLanguages.Count() > 0)
{
try
{
ci = new CultureInfo(userLanguages[0]);
}
catch (CultureNotFoundException)
{
ci = new CultureInfo("en-US");
}
}
else
{
ci = new CultureInfo("en-US");
}
return RedirectToAction(ci.TwoLetterISOLanguageName, "Home");
}
Your code look fine, if user transmitted invalid culture ID it will use the "en-US" culture!
The CultureInfo.InvariantCulture property is used if you are formatting or parsing a string that should be parseable by a piece of software independent of the user's local settings.
The default value is CultureInfo.InstalledUICulture so the default CultureInfo is depending on the executing OS's settings.
The code bellow should work as well to set the culture:
private static bool DoesCultureExist(string cultureName)
{
return CultureInfo.GetCultures(CultureTypes.AllCultures).Any(culture => string.Equals(culture.Name, cultureName, StringComparison.CurrentCultureIgnoreCase));
}
public ActionResult Index()
{
CultureInfo ci;
var userLanguages = Request.UserLanguages;
if (DoesCultureExist(userLanguages?[0]))
{
ci = new CultureInfo(userLanguages[0]);
}
else
{
ci = new CultureInfo("en-US");
}
return RedirectToAction(ci.TwoLetterISOLanguageName, "Home");
}
It's continue ASP.NET MVC incorect generation url when using pagination, but there I found how fix it. How fix that when using #Html.MvcSiteMap().SiteMapPath() I can't understand.
Problem in that when in actions ShowForum or ShowTopic and when I using pagination some forum or topic. In #Html.MvcSiteMap().SiteMapPath() I get url at parent page with number of page
UPDATE
For route configuration I'm using route attribute
[HttpGet]
[Route("{forumName}", Name = "showForum", Order = 6)]
[Route("{forumName}/Page/{page}", Order = 5)]
[OutputCache(Duration = 30, VaryByParam = "forumName;page", Location = OutputCacheLocation.ServerAndClient)]
public async Task<ActionResult> ShowForum(string forumName, int page = 1)
[HttpGet]
[RefreshDetectFilter]
[Block(VisibleBlock = false)]
[Route("{forum}/{topicName}", Name = "showTopic", Order = 8)]
[Route("{forum}/{topicName}/Page/{page}", Order = 7)]
[OutputCache(Duration = 30, VaryByParam = "topicName;page", Location = OutputCacheLocation.ServerAndClient)]
public async Task<ActionResult> ShowTopic(string forum, string topicName, int page = 1)
My ForumDynamicNodeProvider
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
var rootTitle = ManagerLocalization.Get("Forums", "FORUMS");
var pageParameter = new List<string> { "page" };
var url = "~/Forums";
var attr = new Dictionary<string, object> { { "Controller", "Forums" } };
var nodes = new List<DynamicNode>
{
new DynamicNode
{
Key = "forum_home",
Title = rootTitle,
Url = url,
Attributes = attr
}
};
var forums = this._forumsService.GetAllForumsForMap();
var topics = this._forumsService.GetAllTopicsForMap();
foreach (var forum in forums)
{
var forumRouteValue = new Dictionary<string, object> { { "forumName", forum.NameTranslit } };
nodes.Add(new DynamicNode
{
ParentKey = forum.ForumId != -1 ? $"forum_{forum.ForumId}" : "forum_home",
Key = $"forum_{forum.Id}",
Title = forum.Name,
PreservedRouteParameters = pageParameter,
Controller = "Forums",
Action = "ShowForum",
RouteValues = forumRouteValue,
});
var forumTopics = topics.Where(item => item.ForumId == forum.Id);
foreach (var topic in forumTopics)
{
var topicRouteValue = new Dictionary<string, object> { { "forum", forum.NameTranslit }, { "topicName", topic.TitleTranslite } };
nodes.Add(new DynamicNode
{
ParentKey = $"forum_{forum.Id}",
Key = $"topic_{topic.Id}",
Title = topic.Title,
PreservedRouteParameters = pageParameter,
Controller = "Forums",
Action = "ShowTopic",
RouteValues = topicRouteValue,
});
}
}
return nodes;
}
The problem is that you are using the same route key name {page} in two different places in the same node ancestry in combination with PreservedRouteParameters. PreservedRouteParameters gets its data from the current request. So, it is important that a route key have the same meaning in each request in the same node ancestry. For it to work correctly with PreservedRouteParamters, you need to do three things:
Use a different route key for each separate page parameter (for example, {forumPage} and {page}).
Ensure the ancestor page parameter is passed to the request of its descendants, so when building the URL to an ancestor node the value is in the current request. The simplest way is to build the URL with the page information of all ancestors ({forumName}/Page/{forumPage}/{topicName}/Page/{page}).
Any route keys that have the same meaning between nodes should stay the same ({forumName} in both routes).
Then you need to add the parameters when building the URL of the child node. You must build the URL manually within your application because the request will not have all of the parameters unless you do.
#Html.ActionLink("TheTopicName", "ShowTopic", "Forums",
new { forumName = 1, forumPage = 2, topicName = "foo", page = 1 }, null)
The reason you must supply all of the data in the child node request is because the ancestor node needs it to build its URL. It pulls this information from the request, so it must be present in the request for it to function. MvcSiteMapProvider has no way of knowing what the current page number of the ancestor node is unless it is provided in the request by a URL that is built outside of your menu.
See the MvcSiteMapProvider-Forcing-A-Match-2-Levels project in the code download for How to Make MvcSiteMapProvider Remember a User's Position for a similar configuration and the solution. In that case, it is using productId instead of forumPage as the parameter that is preserved on the descendant nodes so you can navigate back to the parent product.
Note that you could use a similar configuration (with PreservedRouteParameters and SiteMapTitleAttribute) for your entire forum rather than using a dynamic node provider. However, in that case I would suggest you disable the /sitemap.xml endpoint and roll your own.
I found how this fix, thank you to NightOwl888. I'm not the first time understood what should to do.
First I removed initialization PreservedRouteParameters in ForumDynamicNodeProvider
Second I added in action
if (forumPage > 1)
{
var node = SiteMaps.Current.FindSiteMapNodeFromKey(forumName);
if (node != null)
{
node.RouteValues["forumPage"] = forumPage;
}
}
Also I need change generation tree in ForumDynamicNodeProvider because SiteMaps.Current doesn't work in async
In Swashbuckle there is a setting called OrderActionGroupsBy which is supposed to change the ordering within the API, but nothing I do is working and I'm can't determine whether this is a Swashbuckle problem, or due to my IComparer any idea what I'm doing wrong?
This is setting the configurations
config.EnableSwagger(c =>
{
...
c.OrderActionGroupsBy(new CustomStringComparer());
c.GroupActionsBy(apiDesc => GroupBy(apiDesc));
...
}
This is grouping the actions by type instead of controllerName.
private static string GroupBy(ApiDescription apiDesc)
{
var controllerName = apiDesc.ActionDescriptor.ControllerDescriptor.ControllerName;
var path = apiDesc.RelativePath;
if (controllerName.Contains("Original"))
{
controllerName = controllerName.Replace("Original", "");
}
// Check if it is one of the entities if so group by that
// Otherwise group by controller
var entities = new List<string>() { "Users", "Apps", "Groups" };
var e = entities.Where(x => attr.Contains(x.ToLower())).FirstOrDefault();
if (e != null)
{
return e;
}
return controllerName;
}
This is my attempt at an IComparer I want Users first and then after that alphabetical
class CustomStringComparer : IComparer<string>
{
public int Compare(string x, string y)
{
if (x.CompareTo(y) == 0)
return 0;
if (x.CompareTo("Users") == 0)
return -1;
if (y.CompareTo("Users") == 0)
return 1;
return x.CompareTo(y);
}
}
}
This isn't working it always defaults to alphabetical no matter what I do.
Looks like this is a bug with Swashbuckle/Swagger-ui
Using OrderActionGroupsBy is correctly sorting the JSON file, but then swagger ui automatically resorts this to alphabetical order.
I have filed bugs with both Swashbuckle and swagger-ui since this seems to go against what is said in swagger-ui's doc regarding apisSorter.
Apply a sort to the API/tags list. It can be 'alpha' (sort by name) or
a function (see Array.prototype.sort() to know how sort function
works). Default is the order returned by the server unchanged.
Swashbuckle issue
swagger-ui issue
swagger-ui specific stackoverflow question
I have a project mvc 4 c# I'm using resource files for localization. I have action to change the culture
public ActionResult ChangeCulture(string lang, string returnUrl)
{
if (lang.Equals("he-IL"))
{
CultureInfo ci = new CultureInfo(lang);
ci.DateTimeFormat.ShortDatePattern = "dd/MM/yy";
ci.DateTimeFormat.LongDatePattern = "dd/MM/yy";
ci.DateTimeFormat.LongTimePattern = "HH:mm";
ci.DateTimeFormat.ShortTimePattern = "HH:mm";
ci = new CultureInfo(lang);
Thread.CurrentThread.CurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
else if (lang.Equals("en-US"))
{
CultureInfo ci = new CultureInfo(lang);
ci = new CultureInfo(lang);
Thread.CurrentThread.CurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
else
{
return new HttpStatusCodeResult(404);
}
return Redirect(returnUrl);
}
in the view I get the text from resource and it's working good.
<div>#(ViewRes.GlobalResource.Hello)</div>
but when I return text from SignalR hub it's always the same language. why?
SignalR does not localize your text within it's stack, you have to select the correctly localized text and return that string yourself. This should be as easy as requesting your resource from a ResourceManager and letting it select the correctly localized text for you. A really simple example of that looks something like this:
public string YourHubMethod()
{
ResourceManager resourceManager = new ResourceManager("YourNamespace.YourResourcesName",
typeof(YourHubType).Assembly);
return resourceManager.GetString("SomeResourceName");
}
Update:
Now that you've provided some more information about how you remember the current culture between requests for your web application in the comments (you use a cookie) I can give you more information on how to make this transfer over to SignalR.
You'll first want to override your Hub's OnConnected property and there you can extract the cookie value and store it in SignalR connection state like so:
public override Task OnConnected()
{
Client.Culture = Context.Request.Cookies["YourCultureCookieName"];
return base.OnConnected();
}
Once it's stored in the client state, you can now build a HubPipelineModule that looks for that state and sets the culture for each logical SignalR request:
public class CallerCulturePipelineModule : HubPipelineModule
{
protected override bool OnBeforeIncoming(IHubIncomingInvokerContext context)
{
// Use the value we stored in the Culture property of the caller's state when they connected
CultureInfo callerCultureInfo = new CultureInfo(context.Hub.Context.Caller.Culture);
Thread.CurrentThread.CurrentUICulture = callerCultureInfo;
Thread.CurrentThread.CurrentCulture = callerCultureInfo;
return base.OnBeforeIncoming(context);
}
}
You then need to make sure to register the pipeline module as part of your Application_Start:
GlobalHost.HubPipeline.AddModule(new CallerCulturePipelineModule());
Finally, if the user changes their culture while they're already connected you would need to update their connection state with the newly selected value (since they're not going to reconnect).There's actually no way to modify connection state outside the hub itself, so you would need to actually get the IHubContext for your hub and update the client through an explicit callback to the client inside of your existing MVC controller's ChangeCulture method. To do this I would suggest you pass the connection ID to ChangeCulture as an optional query string variable (since the user might not be connected to your hub all the time?) and then use that to call back to the client:
string connectionId = <get connectionId from request here>;
IHubContext yourHubContext GlobalHost.GetHubContext<YourHub>();
yourHubContext.Client(connectionId).UpdateCultureState(lang);
And then on the client side you just update the state in JavaScript:
yourHubProxy.client.updateCultureState = function(lang)
{
yourHubProxy.state.culture = lang;
}
I am fairly new to asp.net, and have little experience with iis. I would like to have each user of my application get their own sub-domain, but all use the same controllers. The subdomain would then control what content is displayed.
Example:
user1subdomain.mydomain.com/Whatever
user2subdomain.mydomain.com/Whatever
Will both use the same controller. Ideally a parameter could give the user name to the controller, which could then display the appropriate content. I would like it to be flexible enough that new subdomains could be added to the database without rewriting routing rules every time a new subdomain is added.
MVC is not bound to the domain, just to the path (e.g. http://domain/path).
To do this properly you need the following...
Wildcard DNS setup for
*.yourdomain.com pointing to your server.
The site in IIS setup with
no Host Header. Any other sites
hosted in that instance of IIS on
the same IP must have Host headers
specified.
Your application will need to check the
request host header either on page load,
session start or some other event.
I found an easier answer on this person's blog. Very surprised this works as well as it does and that this solution is more than 4 years old.
http://blog.maartenballiauw.be/post/2009/05/20/aspnet-mvc-domain-routing.aspx
A custom route implementation:
public class DomainRoute : Route
{
public string Domain { get; set; }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// Build regex
domainRegex = CreateRegex(Domain);
pathRegex = CreateRegex(Url);
// Request information
string requestDomain = httpContext.Request.Headers["host"];
if (!string.IsNullOrEmpty(requestDomain))
{
if (requestDomain.IndexOf(":") > 0)
{
requestDomain = requestDomain.Substring(0, requestDomain.IndexOf(":"));
}
}
else
{
requestDomain = httpContext.Request.Url.Host;
}
string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
// Match domain and route
Match domainMatch = domainRegex.Match(requestDomain);
Match pathMatch = pathRegex.Match(requestPath);
// Route data
RouteData data = null;
if (domainMatch.Success && pathMatch.Success)
{
data = new RouteData(this, RouteHandler);
// Add defaults first
if (Defaults != null)
{
foreach (KeyValuePair<string, object> item in Defaults)
{
data.Values[item.Key] = item.Value;
}
}
// Iterate matching domain groups
for (int i = 1; i < domainMatch.Groups.Count; i++)
{
Group group = domainMatch.Groups[i];
if (group.Success)
{
string key = domainRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
// Iterate matching path groups
for (int i = 1; i < pathMatch.Groups.Count; i++)
{
Group group = pathMatch.Groups[i];
if (group.Success)
{
string key = pathRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
}
return data;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
}
public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
{
// Build hostname
string hostname = Domain;
foreach (KeyValuePair<string, object> pair in values)
{
hostname = hostname.Replace("{" + pair.Key + "}", pair.Value.ToString());
}
// Return domain data
return new DomainData
{
Protocol = "http",
HostName = hostname,
Fragment = ""
};
}}
And here is how it can be used.
routes.Add("DomainRoute", new DomainRoute(
"{controller}-{action}.example.com", // Domain with parameters
"{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
));
Mostly not a problem. I think!
In terms of the application/routing the routing starts where the domain ends so mapping multiple domains to the same application is not a problem, that will just work.
In terms of IIS you can map as many domains as you want (well there's bound to be a limit) to a single site - I'm not sure if you can use a wildcard - what version of IIS are you using?
When a request arrives there are events you can hook to look at the domain and hence set up parameters you want (user for example), the root URL for the request is available from the context later in the cycle too - but you'll want to pick it up early.
If you can do wildcards it becomes fairly trivial - pick up the request, validate the subdomain against the users in the database (if not valid redirect to the default site), set the user and carry on through the normal routing.
If you can't do wildcards then the challenge is adding host headers to the IIS application (website) on the fly from your application as users are added to the database.