I am developing application using ASP.NET Core 2.2 and I am struggling with how to implement route localization, ex. depending on request I need to redirect to route /en/products, if language is not specified in the route.
If language is not specified then get locale from accept-language header.
Below demo is applied to use twoLetterLanguageName.Refer to this tutorial
1.Create a RouteDataRequestCultureProvider class:
public class RouteDataRequestCultureProvider : RequestCultureProvider
{
public int IndexOfCulture;
public int IndexofUICulture;
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
throw new ArgumentNullException(nameof(httpContext));
string culture = null;
string uiCulture = null;
var twoLetterCultureName = httpContext.Request.Path.Value.Split('/')[IndexOfCulture]?.ToString();
var twoLetterUICultureName = httpContext.Request.Path.Value.Split('/')[IndexofUICulture]?.ToString();
if (twoLetterCultureName == "de")
culture = "de-DE";
else if (twoLetterCultureName == "en")
culture = uiCulture = "en-US";
if (twoLetterUICultureName == "de")
culture = "de-DE";
else if (twoLetterUICultureName == "en")
culture = uiCulture = "en-US";
if (culture == null && uiCulture == null)
return NullProviderCultureResult;
if (culture != null && uiCulture == null)
uiCulture = culture;
if (culture == null && uiCulture != null)
culture = uiCulture;
var providerResultCulture = new ProviderCultureResult(culture, uiCulture);
return Task.FromResult(providerResultCulture);
}
}
2.And a LanguageRouteConstraintclass
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.ContainsKey("culture"))
return false;
var culture = values["culture"].ToString();
return culture == "en" || culture == "de";
}
}
3.startup.cs ConfigureServices:
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en"),
new CultureInfo("de"),
};
options.DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[]{ new RouteDataRequestCultureProvider{
IndexOfCulture=1,
IndexofUICulture=1
}};
});
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("culture", typeof(LanguageRouteConstraint));
});
4.startup.cs Configure
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
//other middlewares
app.UseMvc(routes =>
{
routes.MapRoute(
name: "LocalizedDefault",
template: "{culture:culture}/{controller}/{action}/{id?}",
defaults: new {controller = "Home", action = "Index" });
//constraints: new { culture = new CultureConstraint(defaultCulture: "en", pattern: "[a-z]{2}") });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
}
Then you could change culture in browser url directly using /en/Home/Privacy.
If language is not specified then get locale from accept-language header
You could use Url Rewriting Middleware to check the route value and redirect it to a new route with default culture.
1.Create a Redirect rule:
public class RewriteRules
{
public static void RedirectRequests(RewriteContext context)
{
//Your logic
var request = context.HttpContext.Request;
var path = request.Path.Value;
var userLangs = request.Headers["Accept-Language"].ToString();
var firstLang = userLangs.Split(',').FirstOrDefault();
var defultCulture = string.IsNullOrEmpty(firstLang) ? "en" : firstLang.Substring(0,2);
//Add your conditions of redirecting
if ((path.Split("/")[1] != "en") && (path.Split("/")[1] != "de"))// If the url does not contain culture
{
context.HttpContext.Response.Redirect($"/{defultCulture}{ request.Path.Value }");
}
}
}
2..Use the middleware in Startup Configure method:
app.UseAuthentication();//before the Rewriter middleware
app.UseRewriter(new RewriteOptions()
.Add(RewriteRules.RedirectRequests)
);
//MVC middleware
Then if your input /Home/Privacy in browser it will redirect to url like /en/Home/Privacy.
Related
I am using Request Localization in a NET Core 7 and Razor Pages application:
builder.Services.AddRazorPages();
builder.Services.Configure<RequestLocalizationOptions>(options => {
options.DefaultRequestCulture = new RequestCulture("pt");
options.SupportedCultures = new List<CultureInfo> { new CultureInfo("en"), new CultureInfo("pt") };
options.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider {
RouteDataStringKey = "culture",
UIRouteDataStringKey = "culture",
Options = options
});
});
WebApplication application = builder.Build();
application.UseRouting();
application.MapRazorPages();
application.UseRequestLocalization();
// ...
A few of my razor pages are:
Index: #page "/{culture?}" Example: /pt
About: #page "/{culture?}/about" Example: /pt/about
When the culture is null or invalid I want to redirect to same page with default culture.
I am trying to do this using a middleware:
public class RedirectUnsupportedCulturesMiddleware {
private readonly RequestDelegate _next;
private readonly string _routeDataStringKey;
public RedirectUnsupportedCulturesMiddleware(
RequestDelegate next,
IOptions<RequestLocalizationOptions> monitor) {
RequestLocalizationOptions options = monitor.Value;
_next = next;
var provider = options.RequestCultureProviders
.Select(x => x as RouteDataRequestCultureProvider)
.Where(x => x != null)
.FirstOrDefault();
_routeDataStringKey = provider.RouteDataStringKey;
}
public async Task Invoke(HttpContext context) {
var requestedCulture = context.GetRouteValue(_routeDataStringKey)?.ToString();
var cultureFeature = context.Features.Get<IRequestCultureFeature>();
var actualCulture = cultureFeature?.RequestCulture.Culture.Name;
if (string.IsNullOrEmpty(requestedCulture) ||
!string.Equals(requestedCulture, actualCulture, StringComparison.OrdinalIgnoreCase)) {
var newCulturedPath = GetNewPath(context, actualCulture);
context.Response.Redirect(newCulturedPath);
return;
}
await _next.Invoke(context);
}
private string GetNewPath(HttpContext context, string newCulture) {
var routeData = context.GetRouteData();
var router = routeData.Routers[0];
var virtualPathContext = new VirtualPathContext(
context,
routeData.Values,
new RouteValueDictionary { { _routeDataStringKey, newCulture } });
return router.GetVirtualPath(virtualPathContext).VirtualPath;
}
}
When I access the home page, e.g "/", I get an exception in the following:
var router = routeData.Routers[0];
Questions
How to solve this?
Can I use a 301 redirect from thee middleware?
Can I use the Net Core UseRewriter to accomplish the same objective?
Here is a whole working demo you could follow:
1.Custom IPageRouteModelConvention to map the route and then no need modify page route like #page "/{culture?}/xxx"
public class CustomCultureRouteRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
List<SelectorModel> selectorModels = new List<SelectorModel>();
foreach (var selector in model.Selectors.ToList())
{
var template = selector.AttributeRouteModel.Template;
selectorModels.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "/{culture}" + "/" + template
}
});
}
foreach (var m in selectorModels)
{
model.Selectors.Add(m);
}
}
}
2.Add this page conventions in Program.cs
builder.Services.AddRazorPages().AddRazorPagesOptions(opts =>
{
opts.Conventions.Add(new CustomCultureRouteRouteModelConvention()); //add this...
});
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.Configure<RequestLocalizationOptions>(
opt =>
{
var supportCulteres = new List<CultureInfo>
{
new CultureInfo("pt"),
new CultureInfo("fr")
};
opt.DefaultRequestCulture = new RequestCulture("pt");
opt.SupportedCultures = supportCulteres;
opt.SupportedUICultures = supportCulteres;
opt.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider()); //add this...
});
3.The order of the middleware
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//app.UseRequestLocalization();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseRequestLocalization(); //be sure add here......
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Result:
When I sign in with SignInAsync in ASP.NET Core Identity, RedirectToLocal is not authenticated.
If I log in without returning Url or go to allow anonymous action, it works fine, but when I redirect authenticate action return to the login page like the user never signs in. Still, I go to allow anonymous action and see the user sign in everything is okay.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWithSms(string userId, string code, string returnUrl)
{
if (userId == null)
{
throw new ArgumentNullException(nameof(userId));
}
if (code == null)
{
throw new ArgumentNullException(nameof(code));
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
throw new ApplicationException(string.Format(ErrorConstant.UnableToLoadUser, userId));
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
return View("AccountError", this.GetErrorVm(AccountConstant.Persian.ConfirmSms, ErrorConstant.Persian.CodeWrong));
}
if (!user.PhoneNumberConfirmed)
{
user.PhoneNumberConfirmed = true;
_context.Users.Update(user);
_context.SaveChanges();
}
await _signInManager.SignInAsync(user, true);
await _setUserActivityLog.SetLogAsync(user.Id, AccountConstant.Persian.LoginToProfile);
return RedirectToLocal(string.IsNullOrEmpty(returnUrl) ? AccountConstant.Routes.ReturnUrlManageIndex : returnUrl);
}
redirect action:
[HttpGet]
[ActionDetail(menuCode: MenuConstant.ManageService.Code, name: "پاسخ دادن به تیکت")]
public async Task<IActionResult> TicketAnswer(long id)
{
var baseTicket = await _context.Tickets.Include(t => t.TicketType).Include(t => t.TicketRecords)
.ThenInclude(tr => tr.Person)
.SingleOrDefaultAsync(t => t.Id == id);
if (baseTicket == null)
{
return NotFound();
}
var vm = new ManageVm<TicketAnwserVm>
{
Entity = new TicketAnwserVm
{
QuickResponses = _context.QuickResponses.OrderBy(qr => qr.Title).Select(qr => new QuickResponseVm
{
Id = qr.Id,
Title = qr.Title
}),
Ticket = new TicketDisplayVm(baseTicket.StartDate)
{
Id = baseTicket.Id,
PersonId = baseTicket.PersonId,
State = baseTicket.State,
Subject = baseTicket.Subject,
TicketTypeName = baseTicket.TicketType.Name,
TicketRecords = baseTicket.TicketRecords.Join(_context.Users, tr => tr.PersonId,
u => u.PersonId,
(tr, u) => new TicketRecordVm(tr.Date)
{
Id = tr.Id,
PersonName = tr.Person.Name,
UserId = u.Id,
Content = tr.Content,
IsOwner = tr.IsOwner,
TicketId = tr.TicketId,
Status = tr.IsOwner ? TicketStatus.Out : TicketStatus.In
})
}
}
};
return View(vm);
}
I use the following code to create a new DefaultHttpContext and use it to render views to strings.
var httpContext = new DefaultHttpContext();
if (serviceProvider.GetService<IHttpContextAccessor>() is { } accessor)
{
accessor.HttpContext = httpContext;
}
var website = page.WebsitePageModel as WebsitePageModel;
var domain = website?.PrimaryDomain;
if (website != null && domain != null)
{
httpContext.Request.Host = "host"
httpContext.Request.Scheme = "https";
httpContext.Request.Path = "/my-page"
}
httpContext.RequestServices = serviceProvider;
var routeData = httpContext.GetRouteData();
routeData.Routers.Add(new RouteCollection());
This works well until I try to render a view component that uses routes/endpoints, e.g.
string url = Url.Action("MyAction", "MyController");
The url will be null because the controller does not know any routes. How can I access the routes of my web application and copy them to the controller?
If you just want to get the route, I found another way, but I don't know if it helps you.
I get all registered routes using IActionDescriptorCollectionProvider:
#using Microsoft.AspNetCore.Mvc.Infrastructure
#inject IActionDescriptorCollectionProvider provider
#{
var urls = provider.ActionDescriptors.Items
.Select(descriptor => "/" + string.Join('/', descriptor.RouteValues.Values
.Where(v => v != null)
.Select(c => c.ToLower())
.Reverse()))
.Distinct()
.ToList();
}
#foreach (var item in #urls)
{
#item
}
I´am trying to add translations for some error messages, but I'am having a hard time figuring out some .net black magic.
Everything works in firefox, it seems to be because it sends the nb-No field in accept-language header.
Firefox header:
nb-NO,nb;q=0.9,no-NO;q=0.8,no;q=0.6,nn-NO;q=0.5,nn;q=0.4,en-US;q=0.3,en;q=0.1
but it doesn't work in chrome, this seems to be because it sends this accept-lanuage header:
no,sv;q=0.9
I have added support for both nb-NO and no (I thought). I have tried to add a file for each of the languages. This is how it looks:
[![Image of resx file layout in solution][1]][1]
So I have figured out that if the accept-language header is noit goes to Controllers.ErrorController.resx, why doesnt it go to Controllers.ErrorController.no.resx?
I'am using netcoreapp 2.1
I have added relevant code below:
So in startup it looks like this:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddLocalization(options => options.ResourcesPath = "Resources/Languages");
services.Configure<RequestLocalizationOptions>(Language.ConfigureRequestLanguage());
...
}
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
....
app.UseExceptionHandler("/api/errors");
app.UseRequestLocalization(Language.SupportedCultures());
...
}
The helper functions:
public static class Language
{
private const string DefaultLanguage = "en-US";
private static readonly CultureInfo[] SupportedUiCultures = {
new CultureInfo(DefaultLanguage),
new CultureInfo("no"),
new CultureInfo("nb-NO"),
new CultureInfo("sv"),
new CultureInfo("sv-SE"),
};
public static Action<RequestLocalizationOptions> ConfigureRequestLanguage()
{
return options =>
{
options.DefaultRequestCulture = new RequestCulture(culture: DefaultLanguage, uiCulture: DefaultLanguage);
options.SupportedCultures = SupportedUiCultures;
options.SupportedUICultures = SupportedUiCultures;
options.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(context =>
{
var userLanguages = context.Request.GetTypedHeaders().AcceptLanguage
.OrderByDescending(x => x.Quality ?? 1);
var match = userLanguages.FirstOrDefault(r => SupportedUiCultures.Any(s => s.Name == r.Value.ToString()));
var prioritizedLanguage = match == null ? DefaultLanguage : match.Value.ToString();
var language = string.IsNullOrEmpty(prioritizedLanguage) ? DefaultLanguage : prioritizedLanguage;
return Task.FromResult(new ProviderCultureResult(language, language));
}));
};
}
public static RequestLocalizationOptions SupportedCultures()
{
return new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(DefaultLanguage),
SupportedCultures = SupportedUiCultures,
SupportedUICultures = SupportedUiCultures
};
}
}
[1]: https://i.stack.imgur.com/3W58R.png
I am using a filter to check if user is authenticated. If not need to redirect to login page.
public class CookieAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authorized = base.AuthorizeCore(httpContext);
if (authorized)
{
FormsIdentity identity = (FormsIdentity)HttpContext.Current.User.Identity;
System.Web.Security.FormsAuthentication.RenewTicketIfOld(identity.Ticket);
return true;
}
else
{
httpContext.Items["redirectToCompleteProfile"] = true;
return false;
}
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Items.Contains("redirectToCompleteProfile"))
{
var routeValues = new RouteValueDictionary(new
{
controller = "Login",
action = "Login",
});
filterContext.Result = new RedirectToRouteResult(routeValues);
}
}
}
}
Even though code is entering Login controller , redirection is not working.
The request is coming from javascript.
In your Global.asax.cs, you can catch the ajax request using context.Request.IsAjaxRequest and use Application_PostAuthenticateRequest event.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
var context = new HttpContextWrapper(Context);
if(context.User == null || !context.User.Identity.IsAuthenticated)
{
if(context.Request.IsAjaxRequest())
{
var loginPageUrl = UrlHelper.GenerateUrl(null, "Login", "Account", null, RouteTable.Routes, HttpContext.Current.Request.RequestContext, false);
var serializer = new JavaScriptSerializer();
Response.Clear();
Response.TrySkipIisCustomErrors = true;
Response.ContentType = "application/json";
Response.StatusCode = (Int32)HttpStatusCode.Unauthorized;
Response.Write(serializer.Serialize(new
{
Url = Request.UrlReferrer.AbsolutePath,
LoginUrl = loginPageUrl
}));
Response.End();
}
return;
}
}
and in your javascript, you can have a global handler for these errors like
$(document).ajaxError(function (event, jqxhr, settings, thrownError) {
if (jqxhr.status == 401) {
var errorObject = $.parseJSON(jqxhr.responseText);
var pageUrl = errorObject.Url;
var loginUrl = errorObject.LoginUrl;
window.location = loginUrl + "?ReturnUrl=" + pageUrl;
event.stopImmediatePropagation();
return;
}
}
In ajax call case redirection will not work, it will just return the Login View html back as reponse of ajax call.
You have to check if it is a ajax request return status and url to which should be redirect in JSON format:
if (filterContext.HttpContext.Items.Contains("redirectToCompleteProfile") && !context.HttpContext.Request.IsAjaxRequest())
{
var routeValues = new RouteValueDictionary(new
{
controller = "Login",
action = "Login",
});
filterContext.Result = new RedirectToRouteResult(routeValues);
}
else
{
filterContext.HttpContext.Response.StatusCode = 403;
filterContext.Result = new JsonResult
{
Data = "LogOut",
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
and in your master layout, you can write:
$(document).ajaxError(function(e, xhr, opts) {
console.log(xhr.status);
if (xhr.status == 403 && && xhr.responseText.indexOf("LogOut") != -1) {
window.location.href = "#Url.Action("Login", "Login")";
}
});