Open miloush opened 5 years ago
The problem is that WPF expects the XmlLanguage name to be equivalent to the CultureInfo in some places, so the name must be unique and must not cause mismatches between the true CultureInfo and a customized one.
I'm using this as a workaround:
public static void InitializeCurrentLanguageForWPF()
{
// Create a made-up IETF language tag "more specific" than the culture we are based on.
// This allows all standard logic regarding IETF language tag hierarchy to still make sense and we are
// compatible with the fact that we may have overridden language specific defaults with Windows OS settings.
var culture = CultureInfo.CurrentCulture;
var language = XmlLanguage.GetLanguage(culture.IetfLanguageTag + "-current");
var type = typeof(XmlLanguage);
const BindingFlags kField = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
type.GetField("_equivalentCulture", kField).SetValue(language, culture);
type.GetField("_compatibleCulture", kField).SetValue(language, culture);
if (culture.IsNeutralCulture)
culture = CultureInfo.CreateSpecificCulture(culture.Name);
type.GetField("_specificCulture", kField).SetValue(language, culture);
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(language));
}
To make something like this possible without reflection we just need to be able to define a custom XmlLanguage
based on a CultureInfo
. Since the IETF language tag is not optional the only way to make this working is by adding a custom suffix. Even though this is not an "official" IETF language tag it should be as close as you can get compatibility wise. (You don't want to use an official IETF language tag with a custom CultureInfo because that can cause bugs when roundtripping, using a custom tag at least makes it clear what the cause is when anything goes wrong, and "correctly written" code can still generalize the custom tag properly to actually existing languages.)
There's another workaround. On app startup, you can set FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.Name)));
The bigger issue is that the default language for FrameworkElement
is "en-us" when it should be CultureInfo.CurrentCulture
.
The current behavior shows a naive approach to localization as it assumes "en-us" is a suitable global default when it isn't. It also means that anyone who doesn't want their app to use formatting or other localization details for "English (United States)" has to do extra work. The default for an app should be to use the system settings. If an app wants to hard-code that only a single, specific culture should be used everywhere, this should only need to be set in a single place.
There's another workaround. On app startup, you can set [default Language based on CurrentUICulture]
Considering that the culture is used for formatting in bindings I don't think using CurrentUICulture is correct.
There's another workaround. On app startup, you can set [default Language based on CurrentUICulture]
Considering that the culture is used for formatting in bindings I don't think using CurrentUICulture is correct.
yes, my mistake. (will correct above)
Unfortunately when you switch to CurrentCulture your workaround no longer works. The CurrentCulture is not identifyable by its name as it contains customizations the user made in the system controls panel. Once you roundtrip back to CultureInfo from the name you'll lose those user settings, meaning bindings still don't match what non-WPF components produce when they are asked to format with CurrentCulture (e.g. in ToString
calls). See my above workaround to define a custom name and keep user specific format settings.
I think the whole WPF localization infrastructure was a design mistake. It looks like originally it was intended to localize XAML resources so you'd be supposed to use CurrentUICulture (which does rountrip based on its name), but then they made the mistake of using the same culture for formatting, which does not work out at all.
Considering this issue is about the XAML respecting user customizations in the CultureInfo I think my workaround is the only one which reliably works. WPF will pick up the culture from the XmlLanguage object so if you didn't store it there via reflection you have no chance to work around the problem, CurrentCulture is not obtainable by name I believe.
@weltkante I'm trying to use your workaround without success, do you have any idea why? Maybe some changes in .NET5?
Unlike OP, I don't want to follow configured Windows regional options, I just don't like the new DateTime representation under ICU for my local culture and want to force it back to hh:mm:ss.
var culture = CultureInfo.CurrentCulture;
if (culture.Name == "fr-CA")
{
culture.DateTimeFormat.FullDateTimePattern = "dddd d MMMM yyyy HH:mm:ss";
culture.DateTimeFormat.LongTimePattern = "HH:mm:ss";
culture.DateTimeFormat.ShortTimePattern = "HH:mm";
culture.DateTimeFormat.TimeSeparator = ":";
}
// Create a made-up IETF language tag "more specific" than the culture we are based on.
// This allows all standard logic regarding IETF language tag hierarchy to still make sense and we are
// compatible with the fact that we may have overridden language specific defaults with Windows OS settings.
var language = XmlLanguage.GetLanguage(culture.IetfLanguageTag + "-current");
var type = typeof(XmlLanguage);
const BindingFlags kField = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
type.GetField("_equivalentCulture", kField).SetValue(language, culture);
type.GetField("_compatibleCulture", kField).SetValue(language, culture);
if (culture.IsNeutralCulture)
culture = CultureInfo.CreateSpecificCulture(culture.Name);
type.GetField("_specificCulture", kField).SetValue(language, culture);
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(language));
foreach (Window childWindow in Application.Current.Windows)
{
childWindow.Dispatcher.Thread.CurrentCulture = culture;
childWindow.Dispatcher.Thread.CurrentUICulture = culture;
childWindow.Language = language;
}
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
The language
object methods do return the requested CultureInfo
object with the DatetimeFormat properties changed. After this, current and all new threads report the wanted proper format for DateTime.Now.ToString(), but existing and old WPF window still prints the default format. I also tried doing ait t the application start before WPF initialization without success.
edit: Sorry, I had a custom converter at some point re-switching the culture.
but existing and old WPF window still prints the default format
[edit] never mind, I see you are already trying to "update" existing windows by iterating over Application.Current.Windows
. I never did this myself and I can reproduce that changing the language property after window creation doesn't really refresh anything, which kinda is expected since WPF is designed around static localization.
Anyways, the approach I've posted works for me in .NET 5 when called from the Application_Startup
event. You want to call this early. Note that there it is not possible to execute my snippet twice (you only can set a DependencyProperty default value once) so there is really no point delaying this initialization until other windows are already created.
I just ran into the same problem in .NET 8. It really puzzled me, until I eventually stumbled across this GitHub issue. The standard behavior violates the Principle of Least Astonishment. Fortunately, the workaround described by @weltkante worked for me. Thank you for posting that solution!
However, having to resort to reflection to set private backing fields in WPF code feels more than a little dirty. It would be great if WPF could just "do the right thing" out of the box, and somehow preserve backwards compatibility. Even if there was some switch/setting we had to enable to get supported behavior, I'd be happy.
This issue has now been open for 5 years. Any update on if/when this will get prioritized for a fix please?
My update is that I feel less strongly about compatibility here. Without any public API change there are two options:
1) Always use original culture (current behavior) 2) Always use user-overridden culture
If 1. goes wrong, there is nothing a user can do, application developer has to fix it (and there isn't a supported way to do that). If 2. goes wrong, the user has ability to workaround it by changing their settings.
The risks are further minimized by several non-trivial requirements that have to happen together:
And for those applications, it can only break things for users for which things are already broken.
Problem description: Applying
FrameworkElement.Language
and relatedLanguage
properties does not take into account current culture settings. There is no way how to supply a specific instance of CultureInfo to the WPF infrastructure.Minimal repro:
(this assumes English (United Kingdom) to be used for formatting. Use other cultures accordingly.)
Go to Settings > Time and Language > Region > Change data formats and change Short date format to a non-default option, e.g. yyyy-MM-dd. Run the WPF app.
Actual behavior: Current culture settings not respected, the date is shown as dd/MM/yyyy.
Expected behavior: The date to be in the selected format.
Ultimately, the
XmlLanguage
callsnew CultureInfo(name, useUserOverride)
when converting the string into culture. The actual behavior passesfalse
for theuseUserOverride
while the expected behavior would mean passingtrue
instead.As a result, there isn't any way I am aware of to make the WPF infrastructure use the current culture that user prefers other than annotating all data bindings and template bindings with converters with explicit cultures, which is a considerable performance hit if possible at all.
Clearly changing the current behavior would not only be a compatibility break but also prevent people from using non-overridden cultures, but there might be other solutions, such as (in decreasing number of scenarios it would allow):
CultureInfo
rather than just a name string, possible with an extra property of typeCultureInfo
that would be used if set (either onXmlLanguage
or FE, otherwise fallback toLanguage
.UseLanguageOverride
onXmlLanguage
supporting returning user overridden cultures, possibly but not necessarily with an extra property on FE too.Any thoughts?