Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC Core set application culture from controller action

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

like image 983
Kacper Stachowski Avatar asked Aug 10 '18 11:08

Kacper Stachowski


1 Answers

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.

like image 187
Adam Simon Avatar answered Sep 19 '22 04:09

Adam Simon