dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.57k stars 25.3k forks source link

Update Blazor Web App glob/loc approach #32020

Open guardrex opened 6 months ago

guardrex commented 6 months ago

Description

cc: @hishamco ... I'll work off this issue. Please post your sample cross-link here and let me know if you want a byline (and what cross-link to use for it if so).

Page URL

https://learn.microsoft.com/en-us/aspnet/core/blazor/globalization-localization?view=aspnetcore-8.0

Content source URL

https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/globalization-localization.md

Document ID

d6f07538-228e-9f96-680f-6c324caf11d6

Article author

@guardrex

hishamco commented 6 months ago

Please let me know all your requirements, hopefully I will do quick one when the time permits

guardrex commented 6 months ago

Blazor Web App that allows the user to set the culture via a drop-down list from a set of selected cultures ... the app must persist the culture across SSR and CSR transitions between SSR/CSR/prerendering components.

If it's quicker for you, you can start with the one that I created and just modify it. It has the server/WASM/Auto components already in it and preconfigures for en-US and es-CR (Costa Rica), which work well for demo because the number and date formats change the same way between SSR and CSR glob/loc.

https://github.com/guardrex/BlazorCulturePerComponentInteractivity

Is the plan to add the sample and link it to our docs or refer to an actual blog post?

I'll place a sample app in the Blazor samples repo, and the article will explain basically how it works. You only need to provide the sample app. My problem is just that I'm too dumb of a 🦖 to figure it out 😆. I can write it up for the article and place the sample if I can just see how to set it up.

I can add you as a byline author "By ..." and link it to whatever you want if that's helpful to you.

guardrex commented 6 months ago

@hishamco ... I take it that you're still busy there? You could let me know what your idea is for CSR and the loc cookie to make it work? I can try to implement your idea.

Is your idea to interact directly with the loc cookie in the .Client project for CSR via JS interop? That's the only thing that I could think of to get CSR components to pick up the correct culture from the cookie. I tried it and failed to get that approach to work 🙈 ... but I could try again with fresh eyes and see if I can make it work.

If you have a different idea that doesn't rely on JS interop, what's your idea to make it work?

hishamco commented 6 months ago

I tried it and failed to get that approach to work 🙈 ... but I could try again with fresh eyes and see if I can make it work.

Do you have your demo in a public repo, so I can start from where you stop instead of doing it from scratch

BTW I was sick for week or so, that's why I'm active at that time

guardrex commented 6 months ago

https://github.com/guardrex/BlazorCulturePerComponentInteractivity

hishamco commented 6 months ago

I think the sample works well for both SSR & CSR, right? What I remember from our last conversation is trying the cookie approach, right?

hishamco commented 6 months ago

After a quick look seems you are using both cookies & local storage

guardrex commented 6 months ago

Yes, it works by keeping both the loc cookie and local storage up-to-date. That way, when it's rendering a component server-side, it goes off the loc cookie; and when it's rendering client-side, it goes off of local storage. Due to the differences in the availability of naming server- versus client-side, I use a dictionary for the dropdown list text so that it appears the same way to the user.

It doesn't rely on the loc cookie for CSR. When I investigated trying to get the loc cookie to be used via CSR, the only approach that seemed like it might work would be to interact with the cookie directly via JS interop. However, I tried to make that work and couldn't get it working properly.

hishamco commented 6 months ago

Let me check the CSR and make the cookie works as well

hishamco commented 6 months ago

Shall I submit a PR to your repo?

guardrex commented 6 months ago

Sure ... that's cool.

hishamco commented 6 months ago

Check the PR https://github.com/guardrex/BlazorCulturePerComponentInteractivity/pull/1 and let me know if you have comments

guardrex commented 6 months ago

Yes ... that was about what I was trying to do and didn't quite get it working.

I'll take a closer look on Monday. If it's all good, I'll update the article section from the example.

If you want an author byline on the topic, let me know what you want it linked to.

UPDATE > "I'll take a closer look on Monday"

It will be either Tuesday or Wednesday. I'm bogged down at the moment in security bits.

hishamco commented 6 months ago

If there's anything to help please let me know

guardrex commented 6 months ago

@hishamco ... I just looked it over.

So, the change you made was to basically swap interacting directly with the cookie for local storage.

However, this seems less elegant to me. The cookie approach ...

Why would the cookie approach be better than local storage? If there isn't a compelling reason to change, I'd kind'a like to stick with local storage for CSR, as the topic already covers ...

https://learn.microsoft.com/aspnet/core/blazor/globalization-localization#dynamically-set-the-culture-in-a-blazor-web-app-by-user-preference

Local Storage approach Cookie approach
window.blazorCulture = {
get: () => window.localStorage['BlazorCulture'],
set: (value) => window.localStorage['BlazorCulture'] = value
};
window.blazorCulture = {
get: function (name) {
name = name + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
},
set: function (name, value) {
const days = 7;
var d = new Date();
d.setTime(d.getTime() + (days 24 60 60 1000));
var expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}
const string defaultCulture = "en-US";
var js = host.Services.GetRequiredService();
var result = await js.InvokeAsync("blazorCulture.get");
var culture = CultureInfo.GetCulture(result ?? defaultCulture);
if (result == null)
{
await js.InvokeVoidAsync("blazorCulture.set", defaultCulture);
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
CultureInfo culture;
var cultureCookieName = CookieRequestCultureProvider.DefaultCookieName;
var js = host.Services.GetRequiredService();
var localizationCookie = await js.InvokeAsync("blazorCulture.get", cultureCookieName);
var result = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICultures?[0].Value;
if (result != null)
{
culture = new CultureInfo(result);
}
else
{
culture = new CultureInfo("en-US");
await js.InvokeVoidAsync("blazorCulture.set", cultureCookieName, "en-US");
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
hishamco commented 6 months ago

Requires more code (and messy code because it's more work to deal with the cookie), as shown below.

Not much :)

It requires that the client app reference an additional package (Microsoft.AspNetCore.Localization).

You can avoid it by providing the localization cookie name or get the value directly from the server. FYI the cookie APIs is able to read any cookie value

It seems to use values for the loc cookie for CSR that are different from the server-based loc cookie. For English, wouldn't the normal server-side cookie value be c=en-US|uic=en-US, not just en-US?

I just used the default values provided by CookieRequestCultureProvider

Will make the CSR parts of a BWA less like what we're pitching for pure standalone WASM, which uses local storage.

Doesn't help shed use of the controller under CSR to set the cookie server-side so that server-side loc can pick up its value.

I didn't know that your requirement to use the controller

We're still stuck having to run code locally via JS interop and toss back and forth the server, so there's no improvement for that aspect.

What's the issue here? Could you please elaborate?

guardrex commented 6 months ago

Not much :)

It's still messy 🤮😆 compared to local storage, and it still departs from our standalone WASM approach.

You can avoid it by providing the localization cookie name

That's fair, and I think that would be wise.

I just used the default values provided by CookieRequestCultureProvider

I'm not sure what you mean. You used just the name value of the culture (e.g., en-US) for the cookie value, but I thought that such loc cookies use the c=...|uic=... format for the value. I don't really know much about the cookie. I just noticed a delta on that when I was looking at what the ASP.NET Core loc bits were doing compared to what you had for the cookie values. So, it looks like there's this delta in cookie values between the two, and that seems less elegant. Because local storage is a completely independent system, it can be different ... use different values ... and it doesn't seem inconsistent.

I didn't know that your requirement to use the controller
What's the issue here? Could you please elaborate?

In the CultureSelector component to make this work, the controller still has to be hit. Otherwise, the system breaks down and culture can't be updated. Using the cookie approach didn't get rid of that. It didn't simplify anything really in that regard. I was hoping that there was a magic way ✨ to perhaps drop the controller request for CSR and make this whole system still work. However, it seems like the controller has to be hit both for SSR and CSR to make this work. The direct cookie use approach didn't do anything to help that situation out and make it simpler with less code and with dropping a server request for components on CSR.

All that this cookie approach did was result in nasty JS cookie code 😆 over sweet JS local storage code 😎. I just felt that the loc storage code is a lot nicer to see and work with ... and going back to what we have earlier in the article, it's exactly the approach taken for standalone WASM (i.e., the true, real CSR Blazor app type). It feels more like a natural extension of what we're telling users to do in a standalone WASM app.

Anyway ... these are just impressions. If you really think that the cookie approach is better, let me know why you think so. I think Dan and Artak will decide which way the article should go. If you don't have a good reason for this approach tho, I still like the local storage approach the best, and that's what I'd use, unless there are some 🐉 or 😈 that I'm not aware of that could break it 💥 in some way.

Here's the controller code that I'm referring to .....................

In the server project ...

[Route("[controller]/[action]")]
public class CultureController : Controller
{
    public IActionResult Set(string culture, string redirectUri)
    {
        if (culture != null)
        {
            HttpContext.Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(
                    new RequestCulture(culture, culture)));
        }

        return LocalRedirect(redirectUri);
    }
}

In the CultureSelector component ...

var uri = new Uri(Navigation.Uri)
    .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
var cultureEscaped = Uri.EscapeDataString(value.Name);
var uriEscaped = Uri.EscapeDataString(uri);

Navigation.NavigateTo(
    $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}",
    forceLoad: true);
guardrex commented 6 months ago

BTW ... one more thing ... the CultureSelector isn't invoking blazorCulture.set the same way. It invokes it like this ...

JS.InvokeVoidAsync("blazorCulture.set", value.Name);

... without the cookie name.

UPDATE: Actually, it looks like that line can be removed 🔪 for the cookie approach, just leaving the controller request there.

mkArtakMSFT commented 5 months ago

@MackinnonBuck looks like @guardrex has some questions with this. But I know you've a ton on your plate already. So please keep this on the backlog of your todo list. I think it'll be ok to tackle closer to preview 6/7 timeframe.