Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BWA global Auto approach #31894

Merged
merged 6 commits into from
Mar 7, 2024
Merged

Add BWA global Auto approach #31894

merged 6 commits into from
Mar 7, 2024

Conversation

guardrex
Copy link
Collaborator

@guardrex guardrex commented Feb 23, 2024

Fixes #30002
Addresses #28161

@hishamco ... I have another πŸ™ˆ RexHacks!β„’ πŸ¦– approach here. It SEEMS to work, but IDK if there are some πŸ‰πŸ‰πŸ‰ with what I did or some lurking 😈😈😈 in here.

Because of the complexity of this, I thought you might want to pull it down and run it locally, so I placed my test app at ...

https://github.com/guardrex/BlazorCulturePerComponentInteractivity

Question 1

There is one thing here that I don't understand. When changing the culture from a CSR component (in the sample app, changing it from the CultureClient component), it does seem to update the localization cookie and carryover to the server-rendered component (in the sample app if you then navigate to the CultureServer component). It's not clear to me where/how the localization cookie is being updated when that happens. The controller action in the server app isn't called when the culture is changed client-side, and I didn't think that Localization Middleware was reading the updated value from local storage. However, it seems to work. If you can see where/how the culture is being carried over to SSR, I'll add an explanation on it to the article text.

Question 2

... and another ❓on the delta between the Chilean Spanish date format SSR vs. CSR.

SSR it's in the format ... 23-02-2024
CSR it's in the format ... 23/2/2024

Any idea on why that format is changing in an odd way? It's another odd behavioral difference that I'll explain in the article if it's known why it happens.


Internal previews

πŸ“„ File πŸ”— Preview link
aspnetcore/blazor/globalization-localization.md ASP.NET Core Blazor globalization and localization

@guardrex guardrex self-assigned this Feb 23, 2024
@hishamco
Copy link
Member

It's a long time from doing localization in Blazor :) but seems everything LGTM

guardrex and others added 2 commits February 23, 2024 08:37
Co-authored-by: Hisham Bin Ateya <hishamco_2007@yahoo.com>
@guardrex
Copy link
Collaborator Author

Thanks @hishamco ... and that last commit was to update the client-only section earlier in the article where it also appears.

What about my ❓ in the opening comment ☝️? ... any ideas on what I can say about those in the article?

@hishamco
Copy link
Member

What about my ❓ in the opening comment ☝️? ... any ideas on what I can say about those in the article?

I need to check, but how I can produce the two formats?

@guardrex
Copy link
Collaborator Author

guardrex commented Feb 23, 2024

Run the sample app, set the culture to Chilean Spanish, and flip back and forth between the CultureClient and CultureServer components. They're in the app's nav sidebar.

https://github.com/guardrex/BlazorCulturePerComponentInteractivity

SSR (CultureServer component) it's in the format ... 23-02-2024
CSR (CultureClient component) it's in the format ... 23/2/2024

... and if you access the Auto component CultureAuto, you'll see it start out as 23-02-2024; and then after SSR and it flips to CSR, it flashes quickly to 23/2/2024.

@hishamco
Copy link
Member

I will check later, if this will not postponed this PR

@guardrex
Copy link
Collaborator Author

guardrex commented Feb 23, 2024

It can definitely wait. I wasn't going to merge it until Monday at the earliest because it needs to be edited again.

... and it could wait until after Monday, too. There's no super rush on this work.

@guardrex
Copy link
Collaborator Author

@hishamco ... Are you still too busy to review? I could just go ahead with this and take reader feedback on this. I'm sure readers will let me know if something goes wrong ... they'll sharpen their BEAKS and CLAWS for me πŸ¦–πŸ˜¨ if this is buggy code πŸ˜†.

I'm more concerned about the "Question 2" situation because devs are going to see that with this code. I'm aware after our chats and talking to Ilona that WASM app glob is a bit different (a subset of data and possibly behavior) than full-blown ASP.NET Core glob.

I think I'll try to see if I can use a different language than Chilean Spanish to get a stable date format between server and client. That would be fine if I can use something else to clear that behavior from the example. I'll report back in a bit .............

@guardrex
Copy link
Collaborator Author

guardrex commented Feb 29, 2024

Yes! πŸ‡²πŸ‡½ πŸŽ‰

If I switch to Mexican Spanish, I get a stable date format. I'll do that ... I'll go with Mexico πŸ‡²πŸ‡½ for the code example. That eliminates "Question 2" from being a major concern.

@guardrex
Copy link
Collaborator Author

guardrex commented Feb 29, 2024

Well ... I'm actually not too keen on Mexican Spanish because the number format (dot separator) is the same that we use in English ...

1999.69

Chilean Spanish uses a comma separator, and I'd like to go with a lang that uses a different number format ...

1999,69

@guardrex
Copy link
Collaborator Author

guardrex commented Feb 29, 2024

OK ... I have one ......

πŸ‡¨πŸ‡· Costa Rica πŸŽ‰ (es-CR)

The number and date formats change from en-US, and it's stable across WASM and server rendering.

I'll go with that.

I've updated my test app with es-CR ... https://github.com/guardrex/BlazorCulturePerComponentInteractivity

@hishamco
Copy link
Member

Frankly, I was busy with Orchard Core and something else in my home :)

I hope to find time this weekend, if you didn't merge this yet

@hishamco
Copy link
Member

hishamco commented Mar 1, 2024

@guardrex I did a quick look for what you did, I'm asking why you didn't rely on the cookie that send by ASP.NET Core, this SHOULD work correctly in both Server & Client

FYI we already did something similar long time back in Oqtane Framework

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 1, 2024

Only because traditionally we didn't take that approach for WASM (standalone or hosted). We always pushed local storage for WASM; so I just assumed that after the Blazor bundle comes down and Auto components go with CSR, they would switch over to local storage. I get the impression from you now that Auto components reaching CSR should just navigate to the server controller and then be redirected back, which is how we've always managed the cookie for Blazor Server <8.0.

I'll try again only adopting our Blazor Server approach (cookie) in a BWA, but I'm buried in call web API work right now and probably won't be able to get back to this until the middle of next week. I'll ping you back here then when I complete a new round of testing with a new test app.

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 5, 2024

@hishamco ... I was able to fix up the test app a bit. However, it still seems like using local storage is the best way to go for CSR because without it one is left reading the cookie directly via JS in order to set the culture.

The latest version of the test app is here πŸ‘‰ https://github.com/guardrex/BlazorCulturePerComponentInteractivity

If you're too busy to look, no worries ... I'll ping Mackinnon to take a look.

@hishamco
Copy link
Member

hishamco commented Mar 5, 2024

I think both read from JS , right?

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 5, 2024

I think only on the client (CSR) is when the culture is read from local storage via JS interop.

I think for SSR that the Loc Middleware is reading the culture from the cookie.

When the user changes the culture from an SSR component, the local storage is updated and they're thrown to the controller to update the cookie.

The part that I'm not 100% clear on is what's happening when the culture selector is activated for CSR.

If the component (and thus culture selector) are in SSR mode, it seems like it sets the culture in local storage and redirects to the controller and back ... which is updating the cookie.

However ... For CSR with the culture selector, I have it set the new culture in local storage ... ok, that's fine ... and I still have the code there for the controller redirect. I guess that works, but I'm not sure on the mechanics of is the component being rerendered completely (on WASM) after it rendered the first time on WASM. I'm not sure if that's the correct way to go for CSR. It seems like the code should update the cookie (for example if the user were to then navigate to an SSR component), but perhaps it should just be calling a backend endpoint to update it without the redirect.

SEE BELOW ... I think I understand it now.

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 5, 2024

πŸ€” ... I guess that's ok for CSR ... it sets the culture into local storage and redirects (for the cookie to be set correctly) ... and then on reload after coming back it sucks the changed culture in from local storage and rerenders the component CSR.

AFAICT, this whole approach is just a bit hacky because there isn't a nice built-in way of managing things.

I guess an alternative to local storage is to deal directly with the loc cookie in JS on the client. Idk if that's any better than this current approach. It would require as much or more code than this, and it would be perhaps equally inelegant as this approach.

... and I guess there's an alternate version of the current approach for CSR without the redirect where in the culture selector component ...

  1. Sets the culture into local storage.
  2. Sets the culture directly on DefaultThreadCurrentCulture/DefaultThreadCurrentUICulture, possibly with a call to StateHasChanged for force a render.
  3. Calls a Minimal web API on the backend to update the cookie (i.e., don't navigate to the controller and redirect back to the app).

I'll try that alternate modification (without the redirect) now and see how it goes. UPDATE: It didn't go well! πŸ˜„ There might be a way to get that approach to work, but I certainly didn't have any luck with it.

I guess the good news is that at least the current approach works and seems stable.

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 6, 2024

I'll place the bits here in one spot to make it simpler to see the overall setup ...

Server project of the BWA

Set up loc in the Program file ...

var supportedCultures = new[] { "en-US", "es-CR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

Controller that can set the culture to the loc cookie and redirect back ...

[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);
    }
}

The App component has a script to get/set the culture for local storage ...

window.blazorCulture = {
  get: () => window.localStorage['BlazorCulture'],
  set: (value) => window.localStorage['BlazorCulture'] = value
};

Client project of the BWA

In the Program file, set a default or read an existing culture and set the culture for the app running CSR ...

CultureInfo culture;
var js = host.Services.GetRequiredService<IJSRuntime>();
var result = await js.InvokeAsync<string>("blazorCulture.get");

if (result != null)
{
    culture = new CultureInfo(result);
}
else
{
    culture = new CultureInfo("en-US");
    await js.InvokeVoidAsync("blazorCulture.set", "en-US");
}

CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;

... and here's the meat πŸ– ... the CultureSelector component that appears throughout the app via the MainLayout component and given an Interactive Auto render mode (<CultureSelector @rendermode="InteractiveAuto" />).

@using System.Globalization
@using System.Runtime.InteropServices
@inject IJSRuntime JS
@inject NavigationManager Navigation

<p>
    <label>
        Select your locale:
        <select @bind="Culture">
            @foreach (var culture in supportedCultures)
            {
                <option value="@culture">@cultureDict[culture.Name]</option>
            }
        </select>
    </label>
</p>

@code
{
    private Dictionary<string, string> cultureDict = 
        new()
        {
            { "en-US", "English (United States)" },
            { "es-CR", "Spanish (Costa Rica)" }
        };

    private CultureInfo[] supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("es-CR"),
    };

    private CultureInfo Culture
    {
        get => CultureInfo.CurrentCulture;
        set
        {
            if (CultureInfo.CurrentCulture != value)
            {
                JS.InvokeVoidAsync("blazorCulture.set", value.Name);

                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);
            }
        }
    }
}

The basic idea is that regardless of rendering location, the culture is kept up-to-date in local storage, and the loc cookie is kept up-to-date via the controller. When going into CSR, local storage is read. When in SSR, the loc cookie is used. In order to avoid deltas on naming of the cultures between SSR and CSR for the dropdown list, the component uses a custom dictionary for the text displayed to the user. The example uses Costa Rica πŸ‡¨πŸ‡· Spanish because it changes both the date and number format the same way from English notwithstanding SSR/CSR. It makes a good demo for this ... and a mini-shoutout to our southern friends, whose good people have suffered quite a bit in recent years.

AFAICT, this is how this approach should work. The only other alternatives that I could think of would be trying to deal with the loc cookie directly for CSR without a redirect, but it seems like it would be no better than this in terms of the amount of code and complexity of code (but it would potentially save the need for a redirect) ... IF it could be make to work cleanly. I tested a bit, and that approach had some πŸ‰ that didn't even let me get it running correctly. Another idea would be to maintain loc outside of the ASP.NET Core loc bits ... try to use some kind of service-based loc management. I didn't investigate it tho.

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 7, 2024

@hishamco ... I'm going to go ahead with this. It's fairly close to what we had here before, merging the server and client approaches in a BWA for components rendering either way, server or client. It seems stable πŸ€žπŸ€. I propose to take reader feedback on it. I'm sure if devs run into trouble that I'll receive the normal threats of πŸ¦– dismemberment πŸ˜¨πŸ˜†.

@guardrex guardrex merged commit 5d42a36 into main Mar 7, 2024
3 checks passed
@guardrex guardrex deleted the guardrex/blazor-bwa-glob branch March 7, 2024 17:05
@hishamco
Copy link
Member

hishamco commented Mar 9, 2024

If you look closely

 HttpContext.Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(
                    new RequestCulture(culture, culture)));

you set the cookie in the server, but you are using the local storage on the client, that's why I prefer to use the cookies everywhere

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 9, 2024

Yes, but I don't understand how to get the cookie read when the app switches to CSR. I'll let Dan know that we need a PU engineer to build out the approach in a sample app for me. I'll ping you on the new issue that I open later with a cross-link to whatever they provide.

@hishamco
Copy link
Member

I can create a demo if you want

@guardrex
Copy link
Collaborator Author

guardrex commented Mar 10, 2024

Yes, please do.

BTW ... I don't know why you aren't on the article with a byline ("By Hisham Bin Ateya" ... cross-linked to your blog, X account, or company), but would you like a byline on the article with a cross-link? If so, what do you want it linked to?

I opened a new issue to work further on the article, and I pinged you on it. Let's take up the discussion and work on that issue.

@hishamco
Copy link
Member

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

@guardrex
Copy link
Collaborator Author

Let's pick up with discussion on the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dynamic culture approach updates 8.0
2 participants