I am creating an ASP.NET MVC application which needs to be translated to French. I have never implemented localization using resource files before. Currently I have two resource files, one each for English and french. The user currently has the ability to choose the language in the home screen by either clicking the English/French button. How do I load the resource file dynamically at runtime based on the language selected by the user? I don't need to take in to account of the language setting of the browser, the resources should be loaded only based on the language selected in the app. Any ideas on how this could be achieved please? I had a quick look online for solutions and couldn't find anything that loads resource files based on language selection within the app and ignoring the browser preferences.
Eidt
It is enough to name the resource file with the same name but with an extension which makes difference in the language, for example
here is support for three languages (English, which is the file which has no special extension, Arabic with ar.resx extension, and Turkish with tr.resx extension)
You may also to control the properties of the resource file to make the files which are dedicated to another language has No Code Generated property. You can change this property from the top when you open any resource file
Short Answer
you have to change the Culture of the thread like this
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(lang);
Long Answer
you need some code which is executed each time a new HTTP request is initiated. I am using the following code in my projects
public class LocalizedControllerActivator : IControllerActivator
{
public const string _DefaultLanguage = "en";
public const string _DefaultLanguageKey = "lang";
public IController Create(RequestContext requestContext, Type controllerType)
{
string lang = GetCurrentLanguageOrDefaultFromRouteData(requestContext);
if (lang != _DefaultLanguage)
{
try
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(lang);
}
catch (Exception)
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo(_DefaultLanguage);
}
}
return DependencyResolver.Current.GetService(controllerType) as IController;
}
private string GetCurrentLanguageOrDefaultFromRouteData(RequestContext requestContext)
{
// Get the {language} parameter from the RouteData
RouteValueDictionary routeData = requestContext.RouteData.Values;
if (!routeData.ContainsKey(_DefaultLanguageKey))
routeData[_DefaultLanguageKey] = _DefaultLanguage;
return routeData[_DefaultLanguageKey].ToString();
}
}
and you have to consider changing the route to make it accepting the required language
routes.MapRoute(
name: "Default",
url: "{lang}/{controller}/{action}/{id}",
constraints: new { lang = "en|fr" },
defaults: new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
and last you need to register the LocalizedContollerActivator when the Web Application start
protected void Application_Start()
{
ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new LocalizedControllerActivator()));
}
the url should be look like this
http://yourDomainOrLocalhost/en/controllerName/actionName
Related
For every web page within my ASP.NET Core 3.1 C# application, I want to automatically generate an canonical link for SEO purposes.
Currently (POC phase), I have the following snippet in every controller action function:
Uri actualUrl = new Uri($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}{HttpContext.Request.Path}{HttpContext.Request.QueryString}");
RouteValueDictionary values = RouteData.Values;
values.Remove("controller");
values.Remove("action");
foreach (var q in HttpContext.Request.Query)
values.Add(q.Key, q.Value);
// Further route/query parameter "fixes" here.
Uri canonicalUrl = new Uri(Url.ActionLink(values: values));
if (!canonicalUrl.Equals(actualUrl))
return RedirectPermanentPreserveMethod(canonicalUrl.ToString());
This snippet first builds a Uri with the current actual URL. Then it may "fixes" some important route/query parameters (as shown below). Finally it compares the actual uri with the desired uri, and redirects to the desired uri, when the actual uri is different compared to the desired uri (case sensitive).
RouteData.Values["subpage"] = "Digital-Contents";
This process enables the web application to generate the correct canonical url ( http://example.com/MyController/MyAction/Digital-Contents ) for the following sample urls.
http://example.com/mycontroller/myaction/digital-contents
http://example.com/Mycontroller/Myaction/Digital-contents
http://example.com/myconTROLLer/myACTion/digital-Contents
However, the POC is a massive duplication of code, and thus not desirable itself.
My first thought was to use a middleware. However, with an middleware, the action controller cannot "fix" route/query parameters, which are out-of-scope of the regular routing construct (like the "id" route parameter which is shown in most ASP.NET examples). E.g. ActionLink is capable of producing the correct case sensitive url slugs for controller and action, but cannot process ("fix") other route/query parameters.
My second thought was to use a generic class, but there I lose the context.
The best solution would be a single (void) function call, which can be placed before the actual action heavy processing (inside the action controller before processing data and generating output).
How to deduplicate this "automatic canonical redirector" code?
This may not be the best solution, i just modified the case based on the code you provided:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CanonicalUrlAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var httpContext = filterContext.HttpContext;
Uri actualUrl = new Uri($"{httpContext.Request.Scheme}://{httpContext.Request.Host}{httpContext.Request.Path}{httpContext.Request.QueryString}");
RouteValueDictionary values = filterContext.RouteData.Values;
values.Remove("controller");
values.Remove("action");
foreach (var q in httpContext.Request.Query)
values.Add(q.Key, q.Value);
// Further route/query parameter "fixes" here.
Uri canonicalUrl = new Uri(new UrlHelper(filterContext).ActionLink(values));
if (!canonicalUrl.Equals(actualUrl))
filterContext.Result = new LocalRedirectResult(canonicalUrl.ToString());
}
}
Usage
[CanonicalUrl]
public class HomeController : Controller {
}
If you're using names from view models to generate urls like example.com/some-category/some-product then i would use the helper in this Link to generate a slug in kebab case based on the model name (in my case its saved to db on model creation)
then with a custom route :
endpoints.MapControllerRoute(
name: "category",
pattern: "{Category}/{Product}",
defaults: new { controller = "Product", action = "Index" });
This pattern omits the action and controller names from route (which i prefer)
and gives you something like this example.com/some-category/some-product and in your action you just compare the model's slug with the route segment that is provided by the user (using the route contraint) like this:
public async Task<IActionResult> Index([FromRoute,Required] Category,[FromRoute,Required] Product)
and you do a route redirect on mismatch like this:
return RedirectToRoutePermanent("category", new { Product = Product.Slug, Category = Product.Category.Slug });
Hope this helps.
as i'm new to Razor Pages concept in ASP.NET Core, i want to apply a general URL to pass the culture parameter to the route
i have done that using MVC but i would like also to apply it with Razor pages
here is what i have done in MVC and its working as needed
routes.MapRoute(
name: "default",
template: "{culture}/{controller=Home}/{action=Index}/{id?}");
i have applied it with specific Page and its working too
options.Conventions.AddPageRoute("/RealEstate/Index", "{culture}/RealEstate");
but when i want to apply for all pages it doesn't work and i don't know what should be passed as a PageName
options.Conventions.AddPageRoute("*", "{culture}/{*url}");
also i want to exclude the admin folder from this convention to be siteName.com/admin instead of en-US/Admin also i need to set a default culture in the URL when the user opens the site for first time, like for example to be siteName.com and loads default culture, or even loads siteName.com/en-US by Default
Thanks.
You can apply a route model convention to a folder using AddFolderRouteModelConvention. The docs have an example of how to do this, which I've taken and modified for your purposes:
options.Conventions.AddFolderRouteModelConvention("/", model =>
{
foreach (var selector in model.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel
{
Order = -1,
Template = AttributeRouteModel.CombineTemplates(
"{culture}",
selector.AttributeRouteModel.Template),
};
}
});
This applies a convention to all pages, given that "/" is set as the folder and therefore applies at the root level. Rather than adding a new selector as in the example I linked, this modifies the existing selector to prepend the {culture} token, which you can access in your pages by name, e.g.:
public void OnGet(string culture)
{
// ...
}
Had we added a new selector, the pages would be accessible both with and without the culture, making it optional. With the approach I've shown, the {culture} token is required, as indicated in the OP.
Thanks for the helping of Kirk Larkin
i used his answer and i added a small modification to exclude Admin from the culture routing also to set default culture for the website when no culture chosen
options.Conventions.AddFolderRouteModelConvention("/", model =>
{
foreach (var selector in model.Selectors)
{
if (selector.AttributeRouteModel.Template.StartsWith("Admin"))
{
selector.AttributeRouteModel = new AttributeRouteModel
{
Order = -1,
Template =
selector.AttributeRouteModel.Template,
};
}
else
{
selector.AttributeRouteModel = new AttributeRouteModel
{
Order = -1,
Template = AttributeRouteModel.CombineTemplates(
"{culture=en-US}",
selector.AttributeRouteModel.Template),
};
}
}
});
Our current hosting company does not allow Host Header entries for multiple domains to different virtual directories under the one primary domain so we have some code in the Global.asax Begin_Request EventHandler. This works fine so far!
protected void Application_BeginRequest(object sender, EventArgs e)
{
Dictionary<string, string> domains = new Dictionary<string, string>() { { "otherdomain.com", "~/Other/Default.aspx" }, { "seconddomain.com", "~/SECOND/" } };
string requestedDomain = Request.ServerVariables["SERVER_NAME"].ToLower();
foreach (var domain in domains.Keys)
{
if (requestedDomain.Contains(domain))
Response.Redirect(domains[domain]);
}
}
These work:
www.primarydomain.com - does not match and falls through to ~/Default.aspx
www.otherdomain.com - matches and goes to ~/Other/Default.aspx (the "~/Other/" directory is also a virtual directory and we are using ASP.Net).
This doesn't:
www.seconddomain.com - www.seconddomainname.com/second/ FAILS!!!
The "~/SECOND/" directory is also a virtual directory however we are using ASP.Net MVC 3 with routing, the error we get is "HTTP 403" which would indicate we need to allow a file permissions, the "SECOND" directory is configured as a Virtual Web Directory.
EDIT: Added route
routes.MapRoute(
"Default", // Route name
"/SECOND/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
Have we missed something very basic, probably, or otherwise is there an overall solution for this type of hosting, routing and ASP.Net configuration?
I haven't encountered a problem like this, but have you tried Server.Transfer instead? I'm not sure if you are trying to do post requests using this route, if so, posted variables are lost using a response.redirect and you may not be able to map to the correct action on the controller that accepts a certain number of parameters.
I have a requirement to add specific functionality to an asp.net mvc2 web site to provide addtional SEO capability, as follows:
The incoming URL is plain text, perhaps a containing a sentence as follows
"http://somesite.com/welcome-to-our-web-site" or
"http://somesite.com/cool things/check-out-this-awesome-video"
In the MVC pipeline, I would like to take this URL, strip off the website name, look up the remaining portion in a database table and call an appropriate controller/view based on the content of the data in the table. All controllers will simply take a single parameter bieng the unique id from the lookup table. A different controller may be used depnding on different urls, but this must be derieved from the database.
If the url cannot be resolved a 404 error needs to be provided, if the url is found but obsolete then a 302 redirect needs to be provided.
Where the url is resolved it must be retained in the browser address bar.
I have had a look at the routing model, and custom routing and can't quite work out how to do it using these, as the controller would not be predefined, based on a simple route. I am also unsure of what to do to provide 404, 302 back to the headers also. Perhpas I need a custom httpmodule or similar but going there went beyond my understanding.
This must be possible somehow... we did it years ago in Classic ASP. Can anyone help with some details on how to achieve this?
Well, the simplest way would be to have an id somewhere in the url (usually the first option)
routes.MapRoute(
"SEORoute", // Route name
"{id}/{*seostuff}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional, seostuff = UrlParameter.Optional } // Parameter defaults
);
In your controller you'd have something like
public class HomeController {
public ActionResult Index(int id) {
//check database for id
if(id_exists) {
return new RedirectResult("whereever you want to redirect", true);
} else {
return new HttpNotFoundResult();
}
}
}
If you don't want to use the id method you could do something else like...
routes.MapRoute(
"SEORoute", // Route name
"{category}/{page_name}", // URL with parameters
new { controller = "Home", action = "Index", category = UrlParameter.Optional, pagename = UrlParameter.Optional } // Parameter defaults
);
public ActionResult Index(string category, string page_name) {
//same as before but instead of looking for id look for pagename
}
The problem with the latter is that you would need to account for all types of routes and it can get really difficult if you have a lot of parameters that match various types.
This should get you in the right direction. If you neeed some clarification let me know and I'll see if I can write a specific route to help you
Additional
You could probably do what you're looking for like
public ActionResult Index() {
//Create and instance of the new controlle ryou want to handle this request
SomeController controller = new SomeController();
controller.ControllerContext = this.ControllerContext;
return controller.YourControllerAction();
}
but I don't know any of the side effects by doing that...so it's probably not a good idea - but it seems to work.
I'm trying to implement localization with routes
I have the following:
routes.MapRoute( "DefaultLocalized",
"{lang}/{controller}/{action}/{id}",
new { controller = "Home",
action = "Index",
id = "",
lang = "en" }
);
routes.MapRoute( "Default",
"{controller}/{action}/{id}",
new { controller = "Home",
action = "Index",
id = "" }
);
When I call my page domain/en/home/index, it works fine but when i call domain/home/index I get error 404: resource cannot be found.
Also when I'm at domain/en/home/index and I click a secured page I get redirected to domain/Account/login how can I be redirected to domain/en/Account/login?
Also when I get an application error how can I be redirected to domain/en/home/error?
The real question is how can I implement localization with language as a route parameter?
The routes will match, by default, left-to-right, so "domain/home/index" will match first to lang=domain, controller=index, action (default to index), id (default to 0/null).
To fix this, I believe you can specify a regex on the MapRoute (matching, for example, languages with exactly 2 characters) - it has changed at some point, though... (sorry, no IDE at the moment, so I can't check exactly).
From memory, it might be:
routes.MapRoute( "DefaultLocalized",
"{lang}/{controller}/{action}/{id}",
new { controller = "Home",
action = "Index",
id = "",},
new { lang = "[a-z]{2}" }
);
Note that you probably aren't going to want every action to take a "string lang", so you should handle the "lang" part of the route either in a base-controller, or an action-filter (in either case, presumably add the info to the ViewData).
I know this is a very old question, but having just had to solve the complete set of related issues, I thought I would share my solution.
Below is a complete solution, including a few extra tricks to allow easy changing of language. It allows for specific cultures, not just specific languages (but only the language part is retained in this example).
Features include:
Fallback to browser locale in determining language
Uses cookies to retain language across visits
Override language with url
Supports changing language via link (e.g. simple menu options)
Step 1: Modify RegisterRoutes in RouteConfig
This new routing includes a constraint (as others also suggest) to ensure the language route does not grab certain standard paths. There is no need for a default language value as that is all handled by the LocalisationAttribute (see step 2).
public static void RegisterRoutes(RouteCollection routes)
{
...
// Special localisation route mapping - expects specific language/culture code as first param
routes.MapRoute(
name: "Localisation",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { lang = #"[a-z]{2}|[a-z]{2}-[a-zA-Z]{2}" }
);
// Default routing
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Step 2: Create a Localisation attribute
This will look at controller requests, before they are handled, and change the current culture based on the URL, a cookie, or the default browser culture.
// Based on: http://geekswithblogs.net/shaunxu/archive/2010/05/06/localization-in-asp.net-mvc-ndash-3-days-investigation-1-day.aspx
public class LocalisationAttribute : ActionFilterAttribute
{
public const string LangParam = "lang";
public const string CookieName = "mydomain.CurrentUICulture";
// List of allowed languages in this app (to speed up check)
private const string Cultures = "en-GB en-US de-DE fr-FR es-ES ro-RO ";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Try getting culture from URL first
var culture = (string)filterContext.RouteData.Values[LangParam];
// If not provided, or the culture does not match the list of known cultures, try cookie or browser setting
if (string.IsNullOrEmpty(culture) || !Cultures.Contains(culture))
{
// load the culture info from the cookie
var cookie = filterContext.HttpContext.Request.Cookies[CookieName];
var langHeader = string.Empty;
if (cookie != null)
{
// set the culture by the cookie content
culture = cookie.Value;
}
else
{
// set the culture by the location if not specified - default to English for bots
culture = filterContext.HttpContext.Request.UserLanguages == null ? "en-EN" : filterContext.HttpContext.Request.UserLanguages[0];
}
// set the lang value into route data
filterContext.RouteData.Values[LangParam] = langHeader;
}
// Keep the part up to the "-" as the primary language
var language = culture.Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries)[0];
filterContext.RouteData.Values[LangParam] = language;
// Set the language - ignore specific culture for now
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(language);
// save the locale into cookie (full locale)
HttpCookie _cookie = new HttpCookie(CookieName, culture);
_cookie.Expires = DateTime.Now.AddYears(1);
filterContext.HttpContext.Response.SetCookie(_cookie);
// Pass on to normal controller processing
base.OnActionExecuting(filterContext);
}
}
Step 3: Apply localisation to all controllers
e.g.
[Localisation] <<< ADD THIS TO ALL CONTROLLERS (OR A BASE CONTROLLER)
public class AccountController : Controller
{
Step 4: To change language (e.g. from a menu)
This is where it got a little tricky and required some workarounds.
Add a ChangeLanguage method to your account controller. This will strip out any existing language code from the "previous path" to allow the new language to take effect.
// Regex to find only the language code part of the URL - language (aa) or locale (aa-AA) syntax
static readonly Regex removeLanguage = new Regex(#"/[a-z]{2}/|/[a-z]{2}-[a-zA-Z]{2}/", RegexOptions.Compiled);
[AllowAnonymous]
public ActionResult ChangeLanguage(string id)
{
if (!string.IsNullOrEmpty(id))
{
// Decode the return URL and remove any language selector from it
id = Server.UrlDecode(id);
id = removeLanguage.Replace(id, #"/");
return Redirect(id);
}
return Redirect(#"/");
}
Step 5: Add language menu links
The menu options consist of a link with the new language specified as a route parameter.
e.g. (Razor example)
<li>#Html.ActionLink("English", "ChangeLanguage", "Account", new { lang = "en", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>
<li>#Html.ActionLink("Spanish", "ChangeLanguage", "Account", new { lang = "es", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>
The return URl is the current page, encoded so that it can become the id parameter of the URL. This means that you need to enable certain escape sequences that are otherwise refused by Razor as a potential security violation.
Note: for non-razor setups you basically want an anchor that has the new language, and the current page relative URL, in a path like:
http://website.com/{language}/account/changelanguage/{existingURL}
where {language} is the new culture code and {existingURL} is a URLencoded version of the current relative page address (so that we will return to the same page, with new language selected).
Step 6: Enable certain "unsafe" characters in URLs
The required encoding of the return URL means that you will need to enable certain escape characters, in the web.config, or the existing URL parameter will cause an error.
In your web.config, find the httpRuntime tag (or add it) in <system.web> and add the following to it (basically remove the % that is in the standard version of this attribute):
requestPathInvalidCharacters="<,>,&,:,\,?"
In your web.config, find the <system.webserver> section and add the following inside it:
<security>
<requestFiltering allowDoubleEscaping="true"/>
</security>
Add a contraint as new {lang = "[a-z]{2}"}.
Additionally, drop the default lang = "en". If you don't, the routing will grab the language rule when you are browsing it without it. So if you are looking at domain and you select About, it would use domain/en/Home/About instead of the more simple domain/Home/About
You could also introduce a constraint even tighter than Marc Gravell and Freddy Rios.
something like "en|de|fr|es". This would mean hardcoding the languages but usually these are few and known.