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 optional culture param to indexer of IStringLocalizer #28047

Open
vaclavholusa-LTD opened this issue Nov 21, 2020 · 19 comments
Open

Add optional culture param to indexer of IStringLocalizer #28047

vaclavholusa-LTD opened this issue Nov 21, 2020 · 19 comments
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates design-proposal This issue represents a design proposal for a different issue, linked in the description enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-localization
Milestone

Comments

@vaclavholusa-LTD
Copy link

Summary

Now when WithCulture() is gone there is no simple method to get localized resource from different culture without changing the application global CurrentUICulture.

Motivation and goals

In my ASP.NET Core App i I'm using translated URLs (stored in resource files). On every page I need to create "Alternate Links" tags that lead to the similar page with a translation. For this I'm iterating through supported cultures and getting translated link for every alternate lang page.

In scope

Since ResourceManagerStringLocalizer internally uses ResourceManger's GetString() method that already supports specifying culture, we should add the option to specify such culture instead of relying on global CurrentUICulture.

Out of scope

none

Risks / unknowns

ResourceManagerStringLocalizer.GetStringSafely() already accepts specific culture so this amendment should be safe to use.

Examples

I implemented this feature by creating my own inherited ResourceManagerStringLocalizer that adds such method.

Extra methods should be added to IStringLocalizer interface

LocalizedString this[string name, CultureInfo culture] { get; }
LocalizedString this[string name, CultureInfo culture, params object[] arguments] { get; }

and example implementation of the first one in ResourceManagerStringLocalizer (similar approach would apply to the second method)

public virtual LocalizedString this[string name, CultureInfo culture]
{
    get
    {
        if (name == null)
        {
            throw new ArgumentNullException(nameof(name));
        }

        var value = GetStringSafely(name, culture);

        return new LocalizedString(name, value ?? name, resourceNotFound: value == null, searchedLocation: _resourceBaseName);
    }
}

I can submit PR if you are happy with the suggestions.

@vaclavholusa-LTD vaclavholusa-LTD added the design-proposal This issue represents a design proposal for a different issue, linked in the description label Nov 21, 2020
@Pilchie Pilchie added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Nov 23, 2020
@javiercn
Copy link
Member

@vaclavholusa-LTD thanks for contacting us.

@DamianEdwards do you have any thoughts?

@javiercn javiercn added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Nov 30, 2020
@javiercn javiercn added this to the Backlog milestone Nov 30, 2020
@ghost
Copy link

ghost commented Nov 30, 2020

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@odinnou
Copy link

odinnou commented Dec 1, 2020

@vaclavholusa-LTD can you transmit a gist of your ResourceManagerStringLocalizer implementation ?

Currently the remove of WithCulture if the only one thing, that is blocking us to migrate to 5.0

thanks in advance

@vaclavholusa-LTD
Copy link
Author

@odinnou basically this
https://gist.github.com/vaclavholusa-LTD/2a27d0bb0af5c07589cffbf1c2fff4f4
just replace "Ltd" with your custom name :-)
Had to inherit some built-in classes/interfaces since it doesn't exist in the original IStringLocalizer.. and naturally you need to pass ILtdStringLocalizer as your dependency

@DamianEdwards
Copy link
Member

We used to have this and it was removed: #3324

The reasons cited were basically that it didn't belong on the interface as it was confusing to imply that all implementations had to support a specific culture or that any instance was culture-specific.

Given that history I'm inclined to suggest we investigate a different approach to solving this, perhaps with a new interface that itself derives from IStringLocalizer that can also be implemented by the default ResourceManagerStringLocalizer, e.g. IStringLocalizerWithCultureFactory or some such.

@dIeGoLi
Copy link
Contributor

dIeGoLi commented Jun 10, 2021

I would assume the expectation of any programmer is, that you can specify a specific culture when retrieving a resource. Fallback to thread culture is fine. But having to change thread culture to achieve this effect is quite sad.
Completely not understandable, why that feature was removed without a proper replacement. (down & upvotes seem to have been ignored) . I really hope a solution will arise. I would suggest not to make it unnecessary complicated with extra interfaces etc. There is a StringLocalizer with a method to retrieve strings...
Confusion can be dealt with in different ways. As well you need to put in relation how many people will be using IStringLocalizer and how many people have to implement their own. A little more complexity for the later is justified instead of making it complicated for the majority.

@odinnou
Copy link

odinnou commented Jun 10, 2021

Hi, we made a radical choice.
To allow us to switch to .NET 5, we stopped using IStringLocalizer and .resx files.
We now work with .json files in i18next format.

@dIeGoLi
Copy link
Contributor

dIeGoLi commented Jun 10, 2021

Hi, we made a radical choice.
To allow us to switch to .NET 5, we stopped using IStringLocalizer and .resx files.
We now work with .json files in i18next format.

That is radical :). For me the framework works in most cases. (But as well i am not too happy with the organisation of resx files). There are just a handful of cases where i need to specify an explicit culture.

I like the suggestion of @vaclavholusa-LTD from a user perspective.
But as "quick fix" i go with overriding thread culture, since it's easier to integrate.

    public static LocalizedString GetString(this IStringLocalizer stringLocalizer, User user, String name) 
      => GetString(stringLocalizer, user, name, Array.Empty<Object>());

    public static LocalizedString GetString(this IStringLocalizer stringLocalizer, User user, String name, params Object[] arguments) {
      String culture = user?.DefaultCultureName;
      CultureInfo cultureInfo = String.IsNullOrEmpty(culture) ? CultureInfo.CurrentUICulture : CultureInfo.GetCultureInfo(culture);
      CultureInfo cultureInfoOriginal = CultureInfo.CurrentUICulture;
      try {
        CultureInfo.CurrentUICulture = cultureInfo;
        CultureInfo.CurrentCulture = cultureInfo;
        return stringLocalizer.GetString(name, arguments);
      }
      finally {
        CultureInfo.CurrentUICulture = cultureInfoOriginal;
        CultureInfo.CurrentCulture = cultureInfoOriginal;
      }
    }

The user object specifies the desired culture, others may prefer to pass the culture info.

@NekkoDroid
Copy link

NekkoDroid commented Aug 25, 2021

I personally would say something like this would be really useful to be able to explicitly state a culture for certain applications, but the suggested overloads with the CultureInfo as second parameter after the format-string could cause problems in cases where you used a culture in the message as first argument, I would suggest switching around the 2 parameters if it were ever implemented

LocalizedString this[CultureInfo culture, string name] { get; }
LocalizedString this[CultureInfo culture, string name, params object[] arguments] { get; }

@TanayParikh TanayParikh modified the milestones: Backlog, .NET 7 Planning Oct 18, 2021
@ghost
Copy link

ghost commented Oct 21, 2021

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@maskalek
Copy link

Just following up on this.
We are still waiting for the solution without chaning CurrentUICulture

@rjgotten
Copy link

The reasons cited were basically that it didn't belong on the interface as it was confusing to imply that all implementations had to support a specific culture or that any instance was culture-specific.

I would honestly like to see concrete metrics regarding that.
Because I have a very hard time believing that.

I have an easier time believing in confusion over why a piece of code has to temporarily switch thread culture to fetch resources from a different culture. E.g. to fit translated links for other languages, as has been cited as a real-world usecase in this issue.

There was nothing wrong with WithCulture and it should never have been removed the way it was.
What should have happened -- mind: if there actually was provably notable confusion over it -- is to update and strengthen its documentation to make clear what it does.

Actual (bad)

Creates a new IStringLocalizer for a specific CultureInfo.

Expected (good)

Creates a new StringLocalizer that is locked to the specified CultureInfo instead of observing CultureInfo.CurrentUICulture.

And possibly rename the method to WithLockedCulture or WithFixedCulture.

@schmitch
Copy link

schmitch commented Nov 22, 2023

wouldn't it make more sense with the api to actually use keyed services with IStringLocalizer to get a specific locale? like:

scope.ServiceProvider.GetRequiredKeyedService<IStringLocalizer>(new CultureInfo("de-De")) ?

@rjgotten
Copy link

@schmitch
That would mean you need to statically register individual keyed IStringLocalizer services in the service container.
Which is a very, very - pardon my French - crappy thing to force on users.

Moreover, you might not even know all the languages to be supported statically upfront.

@hishamco
Copy link
Member

hishamco commented May 7, 2024

I might disagree with bringing this API back, fortunately, you remind me of CultureScope that I wrote a long time back in Orchard Core https://github.com/OrchardCMS/OrchardCore/blob/main/src/OrchardCore/OrchardCore.Abstractions/Localization/CultureScope.cs

Please let me know if you have any further questions or if we need to close this

@rjgotten
Copy link

rjgotten commented May 8, 2024

@hishamco
I might disagree with bringing this API back, fortunately, you remind me of CultureScope that I wrote a long time back in Orchard Core https://github.com/OrchardCMS/OrchardCore/blob/main/src/OrchardCore/OrchardCore.Abstractions/Localization/CultureScope.cs

Please let me know if you have any further questions or if we need to close this

The only thing that does is paper over things.
While- yes; it is a neat little abstraction with an IDisposable that avoids having to manually do the culture swap and swap back or write try-finally blocks to ensure a thrown exception doesn't screw the pooch,

... it still doesn't resolve the cognitive disconnect of having to switch cultures in the first place.
And the actual API surface is still an ugly wart.

How would you resolve something that needs to combine multiple resources from multiple languages in a single expression? Compute out everything beforehand? Any idea how ugly that becomes?

string en, de, fr, es;
using (CultureScope.Create("en")) 
{
  en = localizer["some.resource.name"];
}
using (CultureScope.Create("de")) 
{
  de = localizer["some.resource.name"];
}
using (CultureScope.Create("fr")) 
{
  fr = localizer["some.resource.name"];
}
using (CultureScope.Create("es")) 
{
  es = localizer["some.resource.name"];
}

return FormatCombinedSomehow(en, de, fr, es);

Compare that to:

return FormatCombinedSomehow(
  localizer.WithCulture("en")["some.resource.name"],
  localizer.WithCulture("de")["some.resource.name"],
  localizer.WithCulture("fr")["some.resource.name"],
  localizer.WithCulture("es")["some.resource.name"]
 );

Humorously, the proposed alternative that uses a CultureInfo as the first parameter, while it may work just as well for some other cases as the old and removed API would, is actually slightly worse for this particular degenerate case as well; though at least still more acceptable than the IDisposable-based swaperoo.

return FormatCombinedSomehow(
  localizer[CultureInfo.GetCultureInfo("en"), "some.resource.name"],
  localizer[CultureInfo.GetCultureInfo("de"), "some.resource.name"],
  localizer[CultureInfo.GetCultureInfo("fr"), "some.resource.name"],
  localizer[CultureInfo.GetCultureInfo("es"), "some.resource.name"],
);

@hishamco
Copy link
Member

hishamco commented May 8, 2024

@rjgotten could you please elaborate on what are you trying to do with the above example?

@rjgotten
Copy link

rjgotten commented May 8, 2024

@hishamco
That example is meant to illustrate that the CultureScope based approach is not a solution, because it still has far more overhead for callers than simply passing a culture to the IStringLocalizer indexer or GetString method, or than constructing a derived localizer with a fixed culture as the old WithCulture API did.

It papers over the details of the overhead and lifts it to a higher level of abstraction, but doesn't resolve it anywhere near as elegantly as the removed WithCulture API did, or the proposed alternative based on passing the culture directly.

@hishamco
Copy link
Member

hishamco commented May 8, 2024

I didn't mention this above as a solution in your case, that is why I told you in my last comment what are you trying to do, so I can help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates design-proposal This issue represents a design proposal for a different issue, linked in the description enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-localization
Projects
None yet
Development

No branches or pull requests