I've translated my mvc website, which is working great. If I select another language (Dutch or English) the content gets translated.
This works because I set the culture in the session.
Now I want to show the selected culture(=culture) in the url.
If it is the default language it should not be showed in the url, only if it is not the default language it should show it in the url.
e.g.:
For default culture (dutch):
site.com/foo
site.com/foo/bar
site.com/foo/bar/5
For non-default culture (english):
site.com/en/foo
site.com/en/foo/bar
site.com/en/foo/bar/5
My problem is that I always see this:
site.com/nl/foo/bar/5
even if I clicked on English (see _Layout.cs). My content is translated in English but the route parameter in the url stays on "nl" instead of "en".
How can I solve this or what am I doing wrong?
I tried in the global.asax to set the RouteData but doesn't help.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
routes.LowercaseUrls = true;
routes.MapRoute(
name: "Errors",
url: "Error/{action}/{code}",
defaults: new { controller = "Error", action = "Other", code = RouteParameter.Optional }
);
routes.MapRoute(
name: "DefaultWithCulture",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { culture = "[a-z]{2}" }
);// or maybe: "[a-z]{2}-[a-z]{2}
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Global.asax.cs:
protected void Application_Start()
{
MvcHandler.DisableMvcResponseHeader = true;
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
if (HttpContext.Current.Session != null)
{
CultureInfo ci = (CultureInfo)this.Session["Culture"];
if (ci == null)
{
string langName = "nl";
if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0)
{
langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2);
}
ci = new CultureInfo(langName);
this.Session["Culture"] = ci;
}
HttpContextBase currentContext = new HttpContextWrapper(HttpContext.Current);
RouteData routeData = RouteTable.Routes.GetRouteData(currentContext);
routeData.Values["culture"] = ci;
Thread.CurrentThread.CurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
_Layout.cs (where I let user change language)
// ...
<ul class="dropdown-menu" role="menu">
<li class="@isCurrentLang("nl")">@Html.ActionLink("Nederlands", "ChangeCulture", "Culture", new { lang = "nl", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "nl" })</li>
<li class="@isCurrentLang("en")">@Html.ActionLink("English", "ChangeCulture", "Culture", new { lang = "en", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "en" })</li>
</ul>
// ...
CultureController: (=where I set the Session that I use in GlobalAsax to change the CurrentCulture and CurrentUICulture)
public class CultureController : Controller
{
// GET: Culture
public ActionResult Index()
{
return RedirectToAction("Index", "Home");
}
public ActionResult ChangeCulture(string lang, string returnUrl)
{
Session["Culture"] = new CultureInfo(lang);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
}
Best Answer
There are several issues with this approach, but it boils down to being a workflow issue.
CultureController
whose only purpose is to redirect the user to another page on the site. Keep in mindRedirectToAction
will send an HTTP 302 response to the user's browser, which will tell it to lookup the new location on your server. This is an unnecessary round-trip across the network.HttpContext.Current.Request.UserLanguages
from the user, which might be different from the culture they requested in the URL.The third issue is primarily because of a fundamentally different view between Microsoft and Google about how to handle globalization.
Microsoft's (original) view was that the same URL should be used for every culture and that the
UserLanguages
of the browser should determine what language the website should display.Google's view is that every culture should be hosted on a different URL. This makes more sense if you think about it. It is desirable for every person who finds your website in the search results (SERPs) to be able to search for the content in their native language.
Globalization of a web site should be viewed as content rather than personalization - you are broadcasting a culture to a group of people, not an individual person. Therefore, it typically doesn't make sense to use any personalization features of ASP.NET such as session state or cookies to implement globalization - these features prevent search engines from indexing the content of your localized pages.
If you can send the user to a different culture simply by routing them to a new URL, there is far less to worry about - you don't need a separate page for the user to select their culture, simply include a link in the header or footer to change the culture of the existing page and then all of the links will automatically switch to the culture the user has chosen (because MVC automatically reuses route values from the current request).
Fixing the Issues
First of all, get rid of the
CultureController
and the code in theApplication_AcquireRequestState
method.CultureFilter
Now, since culture is a cross-cutting concern, setting the culture of the current thread should be done in an
IAuthorizationFilter
. This ensures the culture is set before theModelBinder
is used in MVC.You can set the filter globally by registering it as a global filter.
Language Selection
You can simplify the language selection by linking to the same action and controller for the current page and including it as an option in the page header or footer in your
_Layout.cshtml
.As mentioned previously, all other links on the page will automatically be passed a culture from the current context, so they will automatically stay within the same culture. There is no reason to pass the culture explicitly in those cases.
With the above link, if the current URL is
/Home/Contact
, the link that is generated will be/Home/About
. If the current URL is/en/Home/Contact
, the link will be generated as/en/Home/About
.Default Culture
Finally, we get to the heart of your question. The reason your default culture is not being generated correctly is because routing is a 2-way map and regardless of whether you are matching an incoming request or generating an outgoing URL, the first match always wins. When building your URL, the first match is
DefaultWithCulture
.Normally, you can fix this simply by reversing the order of the routes. However, in your case that would cause the incoming routes to fail.
So, the simplest option in your case is to build a custom route constraint to handle the special case of the default culture when generating the URL. You simply return false when the default culture is supplied and it will cause the .NET routing framework to skip the
DefaultWithCulture
route and move to the next registered route (in this caseDefault
).All that is left is to add the constraint to your routing configuration. You also should remove the default setting for culture in the
DefaultWithCulture
route since you only want it to match when there is a culture supplied in the URL anyway. TheDefault
route on the other hand should have a culture because there is no way to pass it through the URL.AttributeRouting
For AttributeRouting, you can simplify things by automating the creation of 2 different routes for each action. You need to tweak each route a little bit and add them to the same class structure that
MapMvcAttributeRoutes
uses. Unfortunately, Microsoft decided to make the types internal so it requires Reflection to instantiate and populate them.RouteCollectionExtensions
Here we just use the built in functionality of MVC to scan our project and create a set of routes, then insert an additional route URL prefix for the culture and the
CultureConstraint
before adding the instances to our MVC RouteTable.There is also a separate route that is created for resolving the URLs (the same way that AttributeRouting does it).
Then it is just a matter of calling this method instead of
MapMvcAttributeRoutes
.