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

[Discussion] ResourceManagerWithCultureStringLocalizer class and WithCulture interface member marked Obsolete and will be removed #7756

Open
ryanbrandenburg opened this issue Feb 20, 2019 · 14 comments

Comments

@ryanbrandenburg
Copy link
Member

commented Feb 20, 2019

The ResourceManagerWithCultureStringLocalizer class and WithCulture interface member are often sources of confusion for users of Localization, especially if they want to create their own IStringLocalizer implementation. These items give the user the impression that we expect an IStringLocalizer instance to be "per-language, per-resource", when actually they should only be "per-resource", with the language searched for determined by the CultureInfo.CurrentUICulture at execution time. To remove this source of confusion and to avoid APIs which we don't want users to use we will be obsoleting these in 3.0.0-preview3, and they will be removed in a future release (4.0 or above).

For context, see here.


This is the discussion issue for aspnet/Announcements#346.

@hishamco

This comment has been minimized.

Copy link
Contributor

commented Feb 21, 2019

and they will be removed in a future release (4.0 or above).

Why we don't remove them in 3.0 while it's a major release?!!!

@ryanbrandenburg

This comment has been minimized.

Copy link
Member Author

commented Feb 21, 2019

@hishamco our process is generally to Obsolete in a major release then remove in the next major after that.

@hishamco

This comment has been minimized.

Copy link
Contributor

commented Feb 21, 2019

Got it .. thanks

@jcemoller

This comment has been minimized.

Copy link

commented Mar 4, 2019

We're using WithCulture to specify the culture used for the localizer used in an e-mail template, when sending e-mails triggered by certain events. If I understand you correctly @ryanbrandenburg, this usage will be Obsolete.

        protected IStringLocalizer GetLocalizerForCulture<T>(string culture)
        {
            var type = typeof(T);
            var localizer = _localizerFactory.Create(type);

            var cultureInfo = String.IsNullOrEmpty(culture) ? CultureInfo.CurrentCulture : new CultureInfo(culture);
            return localizer.WithCulture(cultureInfo);
        }

        public EmailMessage GenerateDailyDigestMessage(DailyDigest digest)
        {
            var localizer = GetLocalizerForCulture<DailyDigestTemplate>(digest.User.Culture);
            var template = new DailyDigestTemplate(localizer, digest.User.FirstName);

            return new EmailMessage(template, digest.User);
        }

In this example, GenerateDailyDigestMessage will be called multiple times for different users, each having (potentially) a different culture.

Please advise what would be a suggested implementation in 3.0.

Changing the thread's locale multiple times in a loop seems a bit wrong to me.

@ryanbrandenburg

This comment has been minimized.

Copy link
Member Author

commented Mar 4, 2019

It sounds like you already have an idea, but our proximal suggestion for that scenario is something like:

        private CultureInfo _originalCulture;

        protected void SetCurrentCulture(string culture)
        {
            _originalCulture = CultureInfo.CurrentCulture;
            var cultureInfo = String.IsNullOrEmpty(culture) ? CultureInfo.CurrentCulture : new CultureInfo(culture);
            CultureInfo.CurrentCulture = cultureInfo;
            CultureInfo.CurrentUICulture = cultureInfo;
        }

        protected void ResetCurrentCulture()
        {
            CultureInfo.CurrentCulture = _originalCulture;
            CultureInfo.CurrentUICulture = _originalCulture;
        }

        public EmailMessage GenerateDailyDigestMessage(DailyDigest digest)
        {
            try
            {
                SetCurrentCulture(digest.User.Culture);
                var localizer = _localizerFactory.Create(type);
                var template = new DailyDigestTemplate(localizer, digest.User.FirstName);
                return new EmailMessage(template, digest.User);
            }
            finally
            {
                ResetCurrentCulture();
            }
        }

That code hasn't been tested or anything, but it should be relatively close.

My more structural suggestion would be that the CurrentCulture and CurrentUICulture be set off of User.Culture when the users context is loaded (and reset when it's disposed). That way you don't have to repeat this pattern everywhere you localize in your system. If you're working inside of an MVC context that's basically what happens behind the scenes anyway, but given the implied context here I assume this is more of a "CRON job" kind of task.

@jcemoller

This comment has been minimized.

Copy link

commented Mar 5, 2019

Thanks @ryanbrandenburg.

As you've guessed, this is part of a background task (IHostedService) where there are 20+ templates for e-mail and text messaging sent to thousands of users all on different locales.

I was mostly concerned about performance, concurrency or other issues that could arise from rapidly changing the current culture on the thread. Since that does not seem to be an issue, I will go with your suggestion.

Took the liberty to redesign it for the using pattern, which would pair nicely with the new C# 8.0 using directive. https://github.com/jcemoller/culture-switcher/blob/master/src/CultureSwitcher.cs

Thanks for the early notification, gives us time to adapt our systems in time for 3.0.

@ryanbrandenburg

This comment has been minimized.

Copy link
Member Author

commented Mar 5, 2019

Looks great! Yes, this is a great candidate for a using/IDisposable structure. Glad we could help!

@martincostello

This comment has been minimized.

Copy link
Contributor

commented Mar 7, 2019

Similar to the above, we were using WithCulture() to create a localizer for specific cultures for usage in tests. It was neat and clean.

It could also be used to pull a single string for a specific language out for inclusion in a page rendered in another language, for example in a language picker where a string in the language of what you want to switch the UI to is needed.

Now we need to wrap all such tests in try-catch blocks to change the culture of the current thread and then swap it back again, which is much less tidy.

@martincostello

This comment has been minimized.

Copy link
Contributor

commented Mar 7, 2019

We also have a unit test that we use to validate that translations have been applied to our resource files. This is now much less trivial to achieve.

foreach (string language in SupportedOtherLanguages)
{
    var culture = new CultureInfo(language);
    var specificLocalizer = localizer.WithCulture(language);

    foreach (string resource in resourceNames)
    {
        string otherText = specificLocalizer[resource].Value;
        string englishText = baseLocalizer[resource].Value;

        otherText.ShouldNotBe(
            englishText,
            $"The value of resource '{resource}' is the same in English and {culture.EnglishName}. Does it exist in the appropriate .resx file?");
    }
}
@ryanbrandenburg

This comment has been minimized.

Copy link
Member Author

commented Mar 7, 2019

RE try-catch: Something similar to the IDisposable pattern @jcemoller used cleans that up a lot. With regards to loading multiple languages depending on how many you needed you could either cache the result before changing languages or write a ResourceManagerStringLocalizer with behavior similar to https://github.com/aspnet/Extensions/blob/master/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs.

@dradovic

This comment has been minimized.

Copy link

commented Mar 22, 2019

From an API design point of view, I would expect that an IStringLocalizerFactory is able to create an IStringLocalizer which is bound to the correct CultureInfo.

Why not add a parameter there? Something like:

    IStringLocalizer Create(Type resourceSource, CultureInfo culture);

The default for culture then of course should be CurrentUICulture. (Needless to say that such a defaulting can easily be achieved by having a IStringLocalizerFactory extension method that only has a resourceSource parameter).

@hishamco

This comment has been minimized.

Copy link
Contributor

commented Jun 9, 2019

@ryanbrandenburg I think we need to close this while it's already done

@ryanbrandenburg

This comment has been minimized.

Copy link
Member Author

commented Jun 10, 2019

There is a bot that closes Discussion issues after a certain period, there's no need to close it outside of that framework.

@anna-git

This comment has been minimized.

Copy link

commented Oct 12, 2019

To generate documents, emails and for so many other usecases, we need to be able to specify the culture in the localizers. The idea of reading the culture statically within the thread is non testable, not a solution for these so common use cases, and feels somewhat dirty. Why not keep the nice mechanism that the method GetStringSafely() provided within ResourceManagerWithCultureStringLocalizer, eventually calling ResourceManager.GetString(name, culture) which seems totally made for the purpose. I understand the false "per-language, per-resource" impression with WithCulture function returning a new instance, then why not make it a void method, which would just change the _culture field value?
If I understood correctly, the solution for those common usecases is to copy the obsolete class and to keep it locally in the project. It seems like a regression, something that is gonna be missing again from the framework.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.