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

Enhancements to @bind - globalization and conversions #9386

Closed
12 tasks done
mkArtakMSFT opened this issue Apr 15, 2019 · 5 comments · Fixed by #12377
Closed
12 tasks done

Enhancements to @bind - globalization and conversions #9386

mkArtakMSFT opened this issue Apr 15, 2019 · 5 comments · Fixed by #12377
Assignees
Labels
area-blazor Includes: Blazor, Razor Components Components Big Rock This issue tracks a big effort which can span multiple issues Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one

Comments

@mkArtakMSFT
Copy link
Member

mkArtakMSFT commented Apr 15, 2019

Edit: @rynowak hijacking the top post for great justice

Summary

@bind when used with <input ...> elements is the primary way that users parse and convert user input into .NET types. This makes @bind the primary touch point for globalization - dealing with input and display of information in a locale specific way.

Goals:

  • Support users using any locale in client-side/server-side Blazor
  • Support users using many locales with a single server for server-side Blazor
  • Support flexible custom conversions and formats
  • Provide good support for common built-in .NET types

TLDR My recommendations for 3.0:

  • Make sure user's culture is reflected in .NET
    • Client-side will be done post 3.0
    • Prerendering
    • Server-side (circuits)
  • Update runtime to accept culture
  • Update compiler to support culture and format in mappings
  • Update type="..." mappings to support culture and formats
  • Make our form controls work correctly with different locales
  • Add @bind support for:
    • DateTime?
    • DateTimeOffset
    • DateTimeOffset?
  • Support TypeConverter

CUT

  • Format strings for numbers
  • Unusual primitive types like byte
  • Our own converter primitive (use TypeConverter)

Related user-feedback

Challenges

HTML5 field definitions make dealing with globalization more complicated, because depending on the field type, the culture-sensitivity expectations are different.

<input type="text">Enter a number in German</input>
<input type="number">Enter a number in German</input>

In the type="text" field the .value attribute will contain exactly what the user typed in - this implies that the value needs to parsed, or formatted according the users' locale.

In the type="number" field the .value attribute will contain a number value in an standard non-locale-specific format. The value must be parsed and formatted according to this standard format. It's up to the browser to support the users' preferred style of display for their locale.

It stands to reason that users will want to use text fields for numbers, dates, etc in some cases instead of the HTML5 equivalents. Using text is much more flexible since it's essentially free-form text, as opposed to number which is restricted by the browser, and has a set of UI behaviors the developer might not want.

Additionally, for the server-side Blazor programming model, there's no automatic way to flow the culture from the client to the server. We have to build it in to Blazor.

Plan of record

Since some fields require the use of CultureInfo.InvariantCulture and some require hardcoded formats (type="month") we need to make our parsing and formatting code aware of these details. This knowledge needs to either come from the runtime (JS or .NET), the compiler, or the user.

At first glance it seems like it would be straightforward to put this knowledge in the JS part of the runtime. At this layer we can see each DOM element and it's attributes, so we should be able to tell what type of field we're interacting how. However, this has a few drawbacks. First, we'd need to pull in some JS glob/loc packages, which would increase the size. Secondly, we'd be hoping and assuming that these JS libraries have the same behaviors as .NET. That doesn't seem reasonable.

So we settled on a hybrid approach. We'll allow the user to specify the culture or format, but try to get it right for the known type="..." values. This means that if we see type="number" statically in code, we can apply the right culture. If the user wants to assign the field type dynamically then they will need to specify the format or culture manually.

Ideas and discussion

More control over @bind

Some ideas here... we don't have to do all of these, and probably shouldn't try to do all of these in the first release.


Flow the field type into .NET: We could easily capture data about the field type in the JS side of the binding code. This would allow the .NET code to be aware of what the type="..." attribute specifies - which we could use to perform the conversion using invariant culture or the user's culture. With this done, we could make the default be culture-sensitive, and apply invariant culture where appropriate based on the field type.


Allow users to specify the culture for @bind We could allow users to specify the culture at the call-site. This could be a desirable level of control for application developers, but it's not a good default experience - essentially we'd be picking a default behavior and allowing an override. This isn't a good default user experience by itself - because as we've already seen, HTML5 fields like number want a different default than `text.

Directive attributes make it easy for us to build something like this

<input type="text" @bind="coolnessLevel" @bind:culture="CultureInfo.InvariantCulture">
  Rate the coolness of Blazor (in a non-locale-specific way)
</input>

Allow format strings for more types Currently we support format strings for DateTime and only datetime. This is useful because DateTime is used to represent a variety of different kinds of values, times, dates, both, etc. This is also useful because DateTime supports a format string on both formatting and parsing.

It might seem like allowing format strings on more types will free us from the need to do some of these other solutions - however:

  • Format strings in .NET are non-locale-specific.
  • If you could write a format string is locale-specific, then that ties you to a single locale.
  • Types other than DateTime don't use format strings for parsing. (ouch)

So while format strings might be useful, they don't really solve any globalization related problems.

Its definitely possible to support format strings for more types, but we need to make sure users understand that the format string they specify is only used in formatting, and not in parsing. The parsing behaviour is specified by NumberStyles. This implies that we need to be really permissive in what we parse if we want users to specify a format for numbers.


Add support for missing types We could add support to bind for types that don't currently support like DateTimeOffset, DateTime? and a bunch of the rarely-used integral types. Look at the types supported by System.Convert

Since bind currently only supports enums and the types we hardcode we don't have support for types like ushort or byte. It seems obvious to me that there's a priority order of these things:

  • DateTimeOffset & DateTimeOffset? & DateTime?
  • everything else

I think an important question when you consider more exotic types like char or sbyte is - what is the expectation of someone who reaches for that type?. I don't think we know. It's easy to try and understand the motivation of someone using DateTime or int or decimal.

It gives me a little bit of pause when I think about this because I don't know what that user wants - do they want Hex? Octal? Why?

Paired with adding support for type-converters, we could end up in a scenario where not providing hand-coded bind support in this release and adding it in a future release results in a breaking change.


Add support for type converters We could easily add support for arbitrary types to fall back to type converters. Type convert is a well-known extensibility point that .NET provides, and other parts of ASP.NET Core use for conversions.

This seems like a clear win, because we get support for things like Guid and TimeSpan without writing much additional code. Users can write their own TypeConverter and plug it in using a non-Blazor mechanism and it will just work.

This is actually somewhat high priority because users hit this trying to use @bind with a generic type. Since we don't overload @bind for arbitrary types, this will result in a compile error and a bad experience.


Allow users to specify a custom converter` We could allow users to specify a converter at the call-site. This could be a desirable level of control for application developers when they are doing something complex.

Directive attributes make it easy for us to build something like this

<input type="text" @bind="coolnessLevel" @bind:converter="MyConverter.Default">
  Rate the coolness of Blazor (in a totally custom way)
</input>

However, this doesn't really solve any globalization problems. This is just added flexibility.

Flow the culture

We have to make sure that .NET code is running with the correct CultureInfo.CurrentCulture and CultureInfo.CurrentUICulture. This is required for localization (using .resx as well as doing any kind of culture-specific conversion).

Having the properties reflect the user's locale is essential for making Blazor feel like .NET because without this, a bunch of .NET infrastructure will work incorrectly. You normally accomplish this in .NET using the localization middleware and Accept-Language, but I'm not sure that applies to our scenario. Asterix 1

Concerns that need to be handled:

  • How does this work in client-side?? We want this to be free and provided by Mono
  • How does this work in prerendering?? (localization middleware)
  • How does this work with Circuits?? (I assume we have to build this)

This of course is considering that we can assume navigator.language reflects what the user wants to see. I think we need to consider providing an override or some other mechanism to customize this to be out of scope for 3.0 and react to feedback.

Appendix

Asterix 1

Users really like to get fancy with the localization middleware and do all kinds of things that aren't based on Accept-Language. Typically means using the query string or URL path. These mechanisms aren't inherently visible to JS in the same way that Accept-Language would be - since the browser generates Accept-Language.

I expect we're going to a substantial amount of feedback from users that want something fancy to work, but I don't see a way we can make that possible for either Blazor scenario. The trap here is that the localization middleware will be required for pre-rendering, but won't be used for non-prerendering - resulting in subtle bugs in users' applications. The solution would be for users to write their own logic to resolve the culture (in JS).

@mkArtakMSFT mkArtakMSFT added Components Big Rock This issue tracks a big effort which can span multiple issues cost: 0 area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates area-blazor Includes: Blazor, Razor Components labels Apr 15, 2019
@mkArtakMSFT mkArtakMSFT added this to the 3.0.0-preview6 milestone Apr 15, 2019
@rynowak rynowak mentioned this issue Apr 15, 2019
56 tasks
@danroth27 danroth27 added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Apr 25, 2019
@mkArtakMSFT mkArtakMSFT removed area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels May 9, 2019
@rynowak rynowak changed the title Globalization support Enhancements to @bind - globalization and conversions May 29, 2019
@Andrzej-W
Copy link

Andrzej-W commented Jun 27, 2019

If possible I would like to add my 2 cents to the discussion about culture. In my opinion the best practice is (example for 2 cultures):

  1. design your url scheme with culture at the beginning (this is required for step 2): example.com/en-us/customers, example.com/pl-pl/kontrahenci, etc.
  2. create 3 main page urls (one more than number of supported cultures) example.com, example.com/en-us, example.com/pl-pl

How to decide which culture to use?

  1. Culture in url has priority and we don't look any further. Each time user displays any page with culture in the url we set a cookie with preferred culture. This is necessary for step 2.

  2. There is only one address without culture: example.com. When user visits this address (usually because he/she entered it manually in the browser) server will check our "preferred culture" cookie first and if it is set it will return HTTP 302 with new address based on the "preferred culture" cookie. If there is no cookie server will check Accept-Language and use this information for redirect. If our web page doesn't support any culture from Accept-Language header then it redirects to main page with default culture.

Why is this the best practice?

User experience

Let's test it on the most complicated example. We have a web page with support for en-us and pl-pl culture. Default culture is pl-pl because I live in Poland and our customers are mainly from Poland. User from Germany visits our main page. We don't support de-de culture - he is redirected to example.com/pl-pl. Unfortunately this user doesn't speak Polish but he can easily switch to English. Next day he visits our page again. He has entered example.com and he is automatically redirected to example.com/en-us - his preferred culture on our site.

SEO

Every page have to have distinct address. You cannot display different content (language) for the same url based on some "magic" like cookie. As far as I know search engines index only one content per url. Some guidelines from Google: https://support.google.com/webmasters/answer/189077?hl=en

Works everywhere (server, client, prerendering, api)

We have only one page without culture in the url - the main page, which essentially does not render anything because it is only used to redirect user to the page with culture. Every other page has culture in the url so it is easy to find this information on the server (including prerendering) and on the client (Blazor working in web assembly). Blazor can also call web api with culture in the url (example.com/en-us/api/products) With this information in the URL server can return product names and descriptions in proper language.

Of course culture can be encoded in query string instead of url, but in my humble opinion url is better.

Proof that it works

You can check this site: tajskiespa.pl to see how it works in practice (it was written in ASP.NET Core 1.1 and it supports pl and en language, not full culture.). You can also check how it is displayed in search results. In Google search for "tajskie spa wilanow branickiego". If you have English preference in the browser you should see link to https://tajskiespa.pl/en/our-salons/wilanow-branickiego (scroll down if you don't use add blocker and you see some Google adds at the top). Change your browser to prefer Polish language and you should see link to https://tajskiespa.pl/pl/nasze-salony/wilanow-branickiego.

I understand that good framework is probably not opinionated, but at the same time it should promote best practices and as you can see culture in the url (eventually query string) is the best solution which at the same time probably resolves all problems.

I expect we're going to a substantial amount of feedback from users that want something fancy to work, but I don't see a way we can make that possible for either Blazor scenario.

Implement solution which is in line with best practices and don't worry about people who want something fancy.

What is not acceptable

You should not use Accept-Language header for anything other than redirection from main page. Example: English speaking tourist visits Paris. In the hotel or internet cafe he uses French browser to look for some attractions. He has no idea what Accept-Language header is and how to change it in the browser. The only chance for him is to change the culture using the user interface available on the website.

@rynowak
Copy link
Member

rynowak commented Jul 1, 2019

thanks for the comments @Andrzej-W - I'm going to look at putting this together as a sample and see what the blockers are.

@rynowak
Copy link
Member

rynowak commented Jul 1, 2019

Super rudimentary sample of the culture propagation part of this here: https://github.com/rynowak/ServerSideBlazorLocalization

This uses the localization middleware (as well as some other custom middleware) to set a cookie on the client. I also created a custom thing to treat the locale as part of the path+pathbase. I don't generally feel positively about using routing to track the culture - it's massively complex, and it feels wrong when you have the assumption that you're going to use a consistent localisation scheme for your app.

Due to the fact that I'm using a cookie for locale, it works for free in server-side Blazor, because I can just rely on the same localisation middleware. This also makes things easy in client-side Blazor because the cookie is available to the app trivially.

What I don't like about this is that we need to find a home for all of the custom middleware that this requires. We'd never put something like this in a sample directly, it's too nuanced.

@Andrzej-W
Copy link

I don't generally feel positively about using routing to track the culture

@rynowak Unfortunately we cannot ignore the fact that any web site have to be properly indexed by search engines. Otherwise this site simply "does not exists". Recently I have read the study and it looks that people who want to open google.com search for google! It is interesting that they prefer to hit Enter and then use mouse to click on the link in the search results than simply write additional .com.

As I have already written in my previous post each multilingual site should have different urls for the same page in different languages (cultures). It is necessary for proper indexing. Now when you have different urls for each culture search engine will send you directly to appropriate address. If you visit this site the first time (no cookie in the browser) there are two ways to select the culture: url or Accept-Language header. You have seen in my previous post that Accept-Language header is far from perfect for people who need to use not their own computers. There are a few possible url schemes (I will use customers and Polish translation kontrahenci).

  1. example.com/customers
    example.com/kontrahenci
  2. example.com/en-us/customers
    example.com/pl-pl/kontrahenci
  3. example.com/en-us/customers
    example.com/pl-pl/customers
  4. en-us.example.com/customers
    pl-pl.example.com/kontrahenci
  5. en-us.example.com/customers
    pl-pl.example.com/customers

I have never seen option 1, at least I don't remember. The most popular are options 2 and 3. We can say that urls in option 2 are fully localized and it is probably the best from the SEO point of view. Option 3 is probably the easiest to implement for programmers. We have to remember that each page have to have links to itself and all other languages/cultures (SEO requirements).

<link rel="alternate" hreflang="pl-pl" href="http://example.com/pl-pl/kontrahenci" />
<link rel="alternate" hreflang="en-us"  href="http://example.com/en-us/customers" />

We also want to have "language switcher" UI element on every page. When we switch the language we should stay on the same page. Option 3 is the easiest because we (programmers) only have to change culture in the url to create alternate addresses. Personally I prefer option 2. Options 4 and 5 are probably seldom used. Usually different subdomain means that web server is physically located in different data center (on different continent) to optimize access time for users around the globe. But in that case application is not really multilingual - each application instance serves pages for only one culture.

It can be complex but I think we have no other choice - urls should be tightly coupled with culture. If someone is not interested in true multilingual/multiculture application then it should be possible to set some default culture and use it on the server and on the client. Eventually single language web site can support multiple cultures and this for simplicity can be based on Accept-Language header (first request) and then you can use cookies. Here you don't need different urls because you always display the same content and culture is probably only used for date and numbers formatting.

@rynowak rynowak added Done This issue has been fixed and removed Working labels Jul 20, 2019
@rynowak
Copy link
Member

rynowak commented Jul 20, 2019

@Andrzej-W - thanks for your input on this.

When I said that I didn't think routing was a good solution for this, I didn't mean that URLs aren't a good solution. Ultimately I think you're right and I'm wrong after reading your reply. I'm torn on this because we don't currently offer a solution for translated URLs.

I did a little bit of sample building based on what you described. The part that's not going to easily be possible in this release is translating the URL path into various languages. The routing system in Blazor is pretty primitive right now.

As far as what we're shipping in the box, we're going to recommend that users use a cookie as the source of truth for server-side blazor. This works with the SignalR services, with self-hosted SignalR, and is already supported by ASP.NET Core.

To set this cookie, or decide what locale to give users by default, that's going to be up to you to figure out. We're not going to build in a specific localization scheme to server-side Blazor.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 3, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components Components Big Rock This issue tracks a big effort which can span multiple issues Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants