Localization/culture is lost upon AJAX request

Bram van der Zee
Member since: 2018
 

Hi,

Because of filter functionality we have created a form that is posted asynchronously to retrieve all items that match the given criteria. This is done using regular MVC controllers, but this raises a problem.

Upon entering the controller, the current culture and the PreferredCulture of ContentLanguage is reset to the master language. Multiple languages are available, so we've tried to force it to a different language. However, when one ContentArea is rendered (with Html.PropertyFor) the PreferredCulture of ContentLanguage was reset to the master language once more. The first block of the page has been rendered in the second language and the rest of the page in the master language.

I'd like to add that this only happens when the language is changed for this first time, or when the page is reloaded without cache (ctrl + f5). 

Is there anyone who could explain why this is happening?

Kind regards,

Bram van der Zee

#202776
Apr 01, 2019 10:23
Stefan Holm Olsen
Member since: 2013
 

Hi Bram

When you make AJAX requests to regular controllers, you don't utilize Episerver routing and the URL does not include the language segment. Episerver looks at this segment when attempting to set the value of PreferredCulture for content requests.

When you make requests to a custom controller, without setting the PreferredCulture, Episerver will use the master language when loading content for your code. However, you can easily detect and set the language code for custom controllers.

I usually do something like this:

  • Add an action filter to the controller. This reads the request header, Accept-Language, decides the best supported language and then sets the PreferredCulture to that language.
  • Write out the current language code to the HTML markup.
  • Using Javascript, set the Accept-Language header of the AJAX request to the current language code (as written to the markup).

I once wrote a piece about above solution, including sample code, here.

#202780
Edited, Apr 01, 2019 10:54
Stefan Holm Olsen
Member since: 2013
 

While reading your question a second time, I remembered another solution.

It sounds like you are requesting HTML using AJAX. If true, you could implement the filtering in your existing Episerver page controller (the page with your form), with the following code at the end of your action method.

return Request.IsAjaxRequest()
    ? PartialView(viewModel)
    : View(viewModel);

Your front end code would then request the page itself as an AJAX request. Then Episerver will figure out the language for you.

#202781
Apr 01, 2019 11:11
Bram van der Zee
Member since: 2018
 

Hi Stefan,

We've managed to partially translate the page using your 1st solution. However, this when we try to add blocks to the page, it will only translate the 1st one. All other blocks added below this one in the view are rendered in the master language. Upon closer inspection we noticed that the PreferredCulture was set to the master language directly after rendering the first block. This only happens with views replaced using javascript. Do you have an idea why the rendering of blocks would cause the PreferredCulture to reset?

#202795
Apr 01, 2019 16:50
Stefan Holm Olsen
Member since: 2013
 

Let me understand this right, Bram.

The custom controller renders an Episerver page that contains several blocks? And you implemented my attribute solution on the custom controller class?

If you did that, then the PreferredCulture should be set and remain for that entire request. Neither pages nor blocks generally change that value. It is normally set before loading any routable content (not including blocks).

Are you sure that no other changes the PreferredCulture property? Do you load those blocks yourself (using IContentLoader), or do you render using the PropertyFor (or similar) method?

#202833
Apr 02, 2019 10:15
Bram van der Zee
Member since: 2018
 

We load the page with IContentLoader. This is done in a controller where the attribute is active. There are multiple blocks on this page that are loaded in the view using the PropertyFor method. I'm testing the block controllers right now, but I doubt they change the PreferredCulture. I'll update the post with my findings.

Update: the blocks do revert to the master language. Adding the attribute to the block controllers does not help. The language settings seem off though. It recognizes the 2nd language as the master language, and the master language as language to show.

#202834
Edited, Apr 02, 2019 10:53
Stefan Holm Olsen
Member since: 2013
 

In your controller, the PreferredCulture is set correctly? And you call IContentLoader.Get<T> with no language parameter (it will pick up the current language, itself)?

I would maybe try loading the blocks from the page, using the content loader with a forced language parameter, to verify that the content can in fact be loaded in the selected language.

#202839
Apr 02, 2019 11:04
Bram van der Zee
Member since: 2018
 

PreferredCulture is set correctly. The content retrieved with IContentLoader.Get<T> is retrieved in the correct language as well. The blocks are shown in the correct language when I do a soft refresh, but shows incorrect data on a hard refresh. From what I've seen, we might've an incorrect implementation of the IUpdateCurrentLanguage interface, where we store the current language in a cookie. However, this cookie is overwritten with the master language through multiple other parts of code that also call this implementation. I haven't been able to find where Episerver uses this implementation though.

#202869
Apr 03, 2019 7:01
Stefan Holm Olsen
Member since: 2013
 

Okay, then you probably don't want to call the IUpdateCurrentLanguage logic for these AJAX requests.

You can try changing my code sample, so it directly sets the content language and the UI culture (for things like date formatting and translations).

// Remove this:
_updateCurrentLanguage.Service.UpdateLanguage(culture.Name);

// Add these:
ContentLanguage.Instance.SetCulture(culture.Name);
UserInterfaceLanguage.Instance.SetCulture(culture.Name);

Generally the IUpdateCurrentLanguage interface seems to be called from deep inside Episerver's routing. But not when using custom controllers, custom routing or WebAPI.

#202870
Apr 03, 2019 7:59
Bram van der Zee
Member since: 2018
 

That didn't seem to make any difference. In our implementation of the IUpdateCurrentLanguage interface, somehow the master language is passed as a parameter. I've tried to force this to the 2nd language, which resulted in all pages being translated correctly. What we don't know is why the master language is passed as the parameter and how we can isolate the correct language in this method.

Update: seems like incorrect language is supplied by Episerver. Called from EPiServer.Cms.AspNet.dll!EPiServer.Web.Routing.Segments.Internal.RequestSegmentContext.Language.set(string value)

#202880
Edited, Apr 03, 2019 10:29
Bram van der Zee
Member since: 2018
 

I've been going through the IUpdateCurrentLanguage implementation today and found out that images and bundles (css/js) also pass through this interface and try to update the language. As they do not have a 2nd language or even a language at all, they are defaulted to the master language or even null. By filtering on the accept-types of the incoming request, we were able to isolate the language update when actual pages were loaded, instead of images and bundles. It works as it should now. Thank you very much for your help! 

#202913
Apr 03, 2019 13:16
Jeroen van Lieshout
Member since: 2016
 

I noticed when using this approach on Ajax.BeginForm and using the OnBegin method sending accros the Accept-Language using JavaScript, that the ModelState doesn't have the right Culture. Is there a way to make sure that also the MVC ModelState is getting the right culture?

#203282
Edited, Apr 15, 2019 15:45
Stefan Holm Olsen
Member since: 2013
 

You mean that your ModelState error messages are in wrong language, Jeroen?

In your controller action, can you verify that both Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture are set to the requested language?

#203298
Edited, Apr 16, 2019 8:30
Jeroen van Lieshout
Member since: 2016
 

@Stefan, That is set correctly already using that custom attribute approach you suggested, but it seems that the ModelState is already filled earlier in the request pipeline with the culture. Because when in the controller I clear ModelState and revalidate it again it respects the right language.

#203306
Apr 16, 2019 11:22
Stefan Holm Olsen
Member since: 2013
 

That sounds right, Jeroen. OnActionExecuting seems to run just after the model binding and validation.

This is not a scenario I considered when implementing the solution. But you can probably amend it to fit in earlier in the pipeline.

#203316
Apr 16, 2019 14:33
valdis iljuconoks
Member since: 2011
 

this is nice overview of mvc pipeline. we had the same problem back in days..

#203335
Apr 16, 2019 22:04
Jeroen van Lieshout
Member since: 2016
 

I implemented an IHttpModule and this logic seems to do the trick. The Ajax requests are still working and there is no need for a custom filter attribute on controller level. Also the ModelState is now using the correct language. The code I've used below:

    public class LocalizationHttpModule : IHttpModule
    {
        private readonly Injected<EPiServer.Configuration.Settings> _settings;
        private readonly Injected<IUpdateCurrentLanguage> _updateCurrentLanguage;


        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            context.BeginRequest += new EventHandler(context_BeginRequest);
        }

        private void context_BeginRequest(object sender, EventArgs e)
        {
            HttpRequest request = ((HttpApplication)sender)?.Request;

            if (_settings.Service.PageUseBrowserLanguagePreferences)
            {
                // If the PageUseBrowserLanguagePreferences setting is true, this is already handled everywhere.
                return;
            }

            if (request == null)
            {
                return;
            }

            HttpRequestBase httpRequest = new HttpRequestWrapper(request);
            if (!httpRequest.IsAjaxRequest())
            {
                return;
            }

            string[] acceptedLanguages = httpRequest.UserLanguages;
            if (acceptedLanguages == null || acceptedLanguages.Length == 0)
            {
                return;
            }

            var languagePreferenceList = new LanguagePreferenceList();
            languagePreferenceList.ConditionalAddRange(acceptedLanguages);
            var culture = ContentLanguage.Instance.DetermineCulture(languagePreferenceList);

            _updateCurrentLanguage.Service.UpdateLanguage(culture.Name);
            ContentLanguage.Instance.SetCulture(culture.Name);
            UserInterfaceLanguage.Instance.SetCulture(culture.Name);
        }
    }
#203346
Apr 17, 2019 9:45