From 1540714951f218ca3d45b2c24e9ad9c1223e0613 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 27 Aug 2024 20:48:52 +0100 Subject: [PATCH 1/2] Fixes #3698. ResourceManager GetResourceSet doesn't fallback to default for no translated keys. --- Terminal.Gui/Drawing/ColorStrings.cs | 24 +-- Terminal.Gui/Resources/GlobalResources.cs | 70 ++++++++ .../Resources/ResourceManagerWrapper.cs | 112 ++++++++++++ UnitTests/Resources/ResourceManagerTests.cs | 165 ++++++++++++++++++ 4 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 Terminal.Gui/Resources/GlobalResources.cs create mode 100644 Terminal.Gui/Resources/ResourceManagerWrapper.cs create mode 100644 UnitTests/Resources/ResourceManagerTests.cs diff --git a/Terminal.Gui/Drawing/ColorStrings.cs b/Terminal.Gui/Drawing/ColorStrings.cs index b7f080042d..93899a6f51 100644 --- a/Terminal.Gui/Drawing/ColorStrings.cs +++ b/Terminal.Gui/Drawing/ColorStrings.cs @@ -11,8 +11,6 @@ namespace Terminal.Gui; /// public static class ColorStrings { - private static readonly ResourceManager _resourceManager = new (typeof (Strings)); - /// /// Gets the W3C standard string for . /// @@ -21,7 +19,7 @@ public static class ColorStrings public static string? GetW3CColorName (Color color) { // Fetch the color name from the resource file - return _resourceManager.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentUICulture); + return GlobalResources.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentUICulture); } /// @@ -30,14 +28,18 @@ public static class ColorStrings /// public static IEnumerable GetW3CColorNames () { - foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentUICulture, true, true)!) - { - string keyName = entry.Key.ToString () ?? string.Empty; + foreach (DictionaryEntry entry in GlobalResources.GetResourceSet ( + CultureInfo.CurrentUICulture, + true, + true, + e => + { + string keyName = e.Key.ToString () ?? string.Empty; - if (entry.Value is string colorName && keyName.StartsWith ('#')) - { - yield return colorName; - } + return e.Value is string && keyName.StartsWith ('#'); + })!) + { + yield return (entry.Value as string)!; } } @@ -50,7 +52,7 @@ public static IEnumerable GetW3CColorNames () public static bool TryParseW3CColorName (string name, out Color color) { // Iterate through all resource entries to find the matching color name - foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentUICulture, true, true)!) + foreach (DictionaryEntry entry in GlobalResources.GetResourceSet (CultureInfo.CurrentUICulture, true, true)!) { if (entry.Value is string colorName && colorName.Equals (name, StringComparison.OrdinalIgnoreCase)) { diff --git a/Terminal.Gui/Resources/GlobalResources.cs b/Terminal.Gui/Resources/GlobalResources.cs new file mode 100644 index 0000000000..b60836d9ac --- /dev/null +++ b/Terminal.Gui/Resources/GlobalResources.cs @@ -0,0 +1,70 @@ +#nullable enable + +using System.Collections; +using System.Globalization; +using System.Resources; + +namespace Terminal.Gui.Resources; + +/// +/// Provide static access to the ResourceManagerWrapper +/// +public static class GlobalResources +{ + private static readonly ResourceManagerWrapper _resourceManagerWrapper; + + static GlobalResources () + { + // Initialize the ResourceManagerWrapper once + var resourceManager = new ResourceManager (typeof (Strings)); + _resourceManagerWrapper = new (resourceManager); + } + + /// + /// Looks up a resource value for a particular name. Looks in the specified CultureInfo, and if not found, all parent + /// CultureInfos. + /// + /// + /// + /// Null if the resource was not found in the current culture or the invariant culture. + public static object GetObject (string name, CultureInfo culture = null!) { return _resourceManagerWrapper.GetObject (name, culture); } + + /// + /// Looks up a set of resources for a particular CultureInfo. This is not useful for most users of the ResourceManager + /// - call GetString() or GetObject() instead. The parameters let you control whether the ResourceSet is created if it + /// hasn't yet been loaded and if parent CultureInfos should be loaded as well for resource inheritance. + /// + /// + /// + /// + /// + public static ResourceSet? GetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents) + { + return _resourceManagerWrapper.GetResourceSet (culture, createIfNotExists, tryParents)!; + } + + /// + /// Looks up a set of resources for a particular CultureInfo. This is not useful for most users of the ResourceManager + /// - call GetString() or GetObject() instead. The parameters let you control whether the ResourceSet is created if it + /// hasn't yet been loaded and if parent CultureInfos should be loaded as well for resource inheritance. Allows + /// filtering of resources. + /// + /// + /// + /// + /// + /// + public static ResourceSet? GetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents, Func? filter) + { + return _resourceManagerWrapper.GetResourceSet (culture, createIfNotExists, tryParents, filter)!; + } + + /// + /// Looks up a resource value for a particular name. Looks in the specified CultureInfo, and if not found, all parent + /// CultureInfos. + /// + /// + /// + /// Null if the resource was not found in the current culture or the invariant culture. + public static string GetString (string name, CultureInfo? culture = null!) { return _resourceManagerWrapper.GetString (name, culture); } +} diff --git a/Terminal.Gui/Resources/ResourceManagerWrapper.cs b/Terminal.Gui/Resources/ResourceManagerWrapper.cs new file mode 100644 index 0000000000..ff4eeeb35d --- /dev/null +++ b/Terminal.Gui/Resources/ResourceManagerWrapper.cs @@ -0,0 +1,112 @@ +#nullable enable + +using System.Collections; +using System.Globalization; +using System.Resources; + +namespace Terminal.Gui.Resources; + +internal class ResourceManagerWrapper (ResourceManager resourceManager) +{ + private readonly ResourceManager _resourceManager = resourceManager ?? throw new ArgumentNullException (nameof (resourceManager)); + + // Optionally, expose other ResourceManager methods as needed + public object GetObject (string name, CultureInfo culture = null!) + { + object value = _resourceManager.GetObject (name, culture)!; + + if (Equals (culture, CultureInfo.InvariantCulture)) + { + return value; + } + + if (value is null) + { + value = _resourceManager.GetObject (name, CultureInfo.InvariantCulture)!; + } + + return value; + } + + public ResourceSet? GetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents) + { + ResourceSet value = _resourceManager.GetResourceSet (culture, createIfNotExists, tryParents)!; + + if (Equals (culture, CultureInfo.InvariantCulture)) + { + return value; + } + + if (value!.Cast ().Any ()) + { + value = _resourceManager.GetResourceSet (CultureInfo.InvariantCulture, createIfNotExists, tryParents)!; + } + + return value; + } + + public ResourceSet? GetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents, Func? filter) + { + ResourceSet value = _resourceManager.GetResourceSet (culture, createIfNotExists, tryParents)!; + + IEnumerable filteredEntries = value.Cast ().Where (filter ?? (_ => true)); + + ResourceSet? filteredValue = ConvertToResourceSet (filteredEntries); + + if (Equals (culture, CultureInfo.InvariantCulture)) + { + return filteredValue; + } + + if (!filteredValue!.Cast ().Any ()) + { + filteredValue = GetResourceSet (CultureInfo.InvariantCulture, createIfNotExists, tryParents, filter)!; + } + + return filteredValue; + } + + public string GetString (string name, CultureInfo? culture = null!) + { + // Attempt to get the string for the specified culture + string value = _resourceManager.GetString (name, culture)!; + + // If it's already using the invariant culture return + if (Equals (culture, CultureInfo.InvariantCulture)) + { + return value; + } + + // If the string is empty or null, fall back to the invariant culture + if (string.IsNullOrEmpty (value)) + { + value = _resourceManager.GetString (name, CultureInfo.InvariantCulture)!; + } + + return value; + } + + private static ResourceSet? ConvertToResourceSet (IEnumerable entries) + { + using var memoryStream = new MemoryStream (); + + using var resourceWriter = new ResourceWriter (memoryStream); + + // Add each DictionaryEntry to the ResourceWriter + foreach (DictionaryEntry entry in entries) + { + resourceWriter.AddResource ((string)entry.Key, entry.Value); + } + + // Finish writing to the stream + resourceWriter.Generate (); + + // Reset the stream position to the beginning + memoryStream.Position = 0; + + // Create a ResourceSet from the MemoryStream + var resourceSet = new ResourceSet (memoryStream); + + return resourceSet; + } +} diff --git a/UnitTests/Resources/ResourceManagerTests.cs b/UnitTests/Resources/ResourceManagerTests.cs new file mode 100644 index 0000000000..cd1488fd6a --- /dev/null +++ b/UnitTests/Resources/ResourceManagerTests.cs @@ -0,0 +1,165 @@ +#nullable enable + +using System.Collections; +using System.Globalization; +using System.Resources; +using Terminal.Gui.Resources; + +namespace Terminal.Gui.ResourcesTests; + +public class ResourceManagerTests +{ + private const string DODGER_BLUE_COLOR_KEY = "#1E90FF"; + private const string DODGER_BLUE_COLOR_NAME = "DodgerBlue"; + private const string EXISTENT_CULTURE = "pt-PT"; + private const string NO_EXISTENT_CULTURE = "de-DE"; + private const string NO_EXISTENT_KEY = "blabla"; + private const string NO_TRANSLATED_KEY = "fdDeleteTitle"; + private const string NO_TRANSLATED_VALUE = "Delete {0}"; + private const string TRANSLATED_KEY = "ctxSelectAll"; + private const string TRANSLATED_VALUE = "_Selecionar Tudo"; + private static readonly string _stringsNoTranslatedKey = Strings.fdDeleteTitle; + private static readonly string _stringsTranslatedKey = Strings.ctxSelectAll; + private static readonly CultureInfo _savedCulture = CultureInfo.CurrentCulture; + private static readonly CultureInfo _savedUICulture = CultureInfo.CurrentUICulture; + + [Fact] + public void GetObject_Does_Not_Overflows_If_Key_Does_Not_Exist () { Assert.Null (GlobalResources.GetObject (NO_EXISTENT_KEY, CultureInfo.CurrentCulture)); } + + [Fact] + public void GetObject_FallBack_To_Default_For_No_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (NO_EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (NO_EXISTENT_CULTURE); + + Assert.Equal (NO_TRANSLATED_VALUE, GlobalResources.GetObject (NO_TRANSLATED_KEY, CultureInfo.CurrentCulture)); + + RestoreCurrentCultures (); + } + + [Fact] + public void GetObject_FallBack_To_Default_For_Not_Translated_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (NO_EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (NO_EXISTENT_CULTURE); + + Assert.Equal (NO_TRANSLATED_VALUE, GlobalResources.GetObject (NO_TRANSLATED_KEY, CultureInfo.CurrentCulture)); + + RestoreCurrentCultures (); + } + + [Fact] + public void GetResourceSet_FallBack_To_Default_For_No_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (NO_EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (NO_EXISTENT_CULTURE); + + // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames + string [] colorNames = new W3CColors ().GetColorNames ().ToArray (); + Assert.Contains (DODGER_BLUE_COLOR_NAME, colorNames); + Assert.DoesNotContain (NO_TRANSLATED_VALUE, colorNames); + + RestoreCurrentCultures (); + } + + [Fact] + public void GetResourceSet_FallBack_To_Default_For_Not_Translated_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (EXISTENT_CULTURE); + + // These aren't already translated + // ColorStrings.GetW3CColorNames method uses GetResourceSet method to retrieve color names + IEnumerable colorNames = ColorStrings.GetW3CColorNames (); + Assert.NotEmpty (colorNames); + + // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames + colorNames = new W3CColors ().GetColorNames ().ToArray (); + Assert.Contains (DODGER_BLUE_COLOR_NAME, colorNames); + Assert.DoesNotContain (NO_TRANSLATED_VALUE, colorNames); + + // ColorStrings.TryParseW3CColorName method uses GetResourceSet method to retrieve a color value + Assert.True (ColorStrings.TryParseW3CColorName (DODGER_BLUE_COLOR_NAME, out Color color)); + Assert.Equal (DODGER_BLUE_COLOR_KEY, color.ToString ()); + + RestoreCurrentCultures (); + } + + [Fact] + public void GetResourceSet_With_Filter_Does_Not_Overflows_If_Key_Does_Not_Exist () + { + ResourceSet value = GlobalResources.GetResourceSet (CultureInfo.CurrentCulture, true, true, d => (string)d.Key == NO_EXISTENT_KEY)!; + Assert.NotNull (value); + Assert.Empty (value.Cast ()); + } + + [Fact] + public void GetResourceSet_Without_Filter_Does_Not_Overflows_If_Key_Does_Not_Exist () + { + ResourceSet value = GlobalResources.GetResourceSet (CultureInfo.CurrentCulture, true, true)!; + Assert.NotNull (value); + Assert.NotEmpty (value.Cast ()); + } + + [Fact] + public void GetString_Does_Not_Overflows_If_Key_Does_Not_Exist () { Assert.Null (GlobalResources.GetString (NO_EXISTENT_KEY, CultureInfo.CurrentCulture)); } + + [Fact] + public void GetString_FallBack_To_Default_For_No_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (NO_EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (NO_EXISTENT_CULTURE); + + Assert.Equal (NO_TRANSLATED_VALUE, GlobalResources.GetString (NO_TRANSLATED_KEY, CultureInfo.CurrentCulture)); + + RestoreCurrentCultures (); + } + + [Fact] + public void GetString_FallBack_To_Default_For_Not_Translated_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (EXISTENT_CULTURE); + + // This is really already translated + Assert.Equal (TRANSLATED_VALUE, GlobalResources.GetString (TRANSLATED_KEY, CultureInfo.CurrentCulture)); + + // These aren't already translated + // Calling Strings.fdDeleteBody return always the invariant culture + Assert.Equal (NO_TRANSLATED_VALUE, GlobalResources.GetString (NO_TRANSLATED_KEY, CultureInfo.CurrentCulture)); + + RestoreCurrentCultures (); + } + + [Fact] + public void Strings_Always_FallBack_To_Default_For_No_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (NO_EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (NO_EXISTENT_CULTURE); + + Assert.Equal (NO_TRANSLATED_VALUE, _stringsNoTranslatedKey); + + RestoreCurrentCultures (); + } + + [Fact] + public void Strings_Always_FallBack_To_Default_For_Not_Translated_Existent_Culture_File () + { + CultureInfo.CurrentCulture = new (EXISTENT_CULTURE); + CultureInfo.CurrentUICulture = new (EXISTENT_CULTURE); + + // This is really already translated + Assert.Equal (TRANSLATED_VALUE, _stringsTranslatedKey); + + // This isn't already translated + Assert.Equal (NO_TRANSLATED_VALUE, _stringsNoTranslatedKey); + + RestoreCurrentCultures (); + } + + private void RestoreCurrentCultures () + { + CultureInfo.CurrentCulture = _savedCulture; + CultureInfo.CurrentUICulture = _savedUICulture; + } +} From c1e3ece8a3bdaa2683fb21455a80d8b36abc4bb5 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 27 Aug 2024 23:29:51 +0100 Subject: [PATCH 2/2] Code cleanup. --- Terminal.Gui/Drawing/ColorStrings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Terminal.Gui/Drawing/ColorStrings.cs b/Terminal.Gui/Drawing/ColorStrings.cs index 93899a6f51..a6b90d8002 100644 --- a/Terminal.Gui/Drawing/ColorStrings.cs +++ b/Terminal.Gui/Drawing/ColorStrings.cs @@ -1,7 +1,6 @@ #nullable enable using System.Collections; using System.Globalization; -using System.Resources; using Terminal.Gui.Resources; namespace Terminal.Gui;