Browse Source

Fixes #3698. ResourceManager GetResourceSet doesn't fallback to default for no translated keys.

BDisp 11 months ago
parent
commit
1540714951

+ 13 - 11
Terminal.Gui/Drawing/ColorStrings.cs

@@ -11,8 +11,6 @@ namespace Terminal.Gui;
 /// </summary>
 public static class ColorStrings
 {
-    private static readonly ResourceManager _resourceManager = new (typeof (Strings));
-
     /// <summary>
     ///     Gets the W3C standard string for <paramref name="color"/>.
     /// </summary>
@@ -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);
     }
 
     /// <summary>
@@ -30,14 +28,18 @@ public static class ColorStrings
     /// <returns></returns>
     public static IEnumerable<string> 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 class ColorStrings
     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))
             {

+ 70 - 0
Terminal.Gui/Resources/GlobalResources.cs

@@ -0,0 +1,70 @@
+#nullable enable
+
+using System.Collections;
+using System.Globalization;
+using System.Resources;
+
+namespace Terminal.Gui.Resources;
+
+/// <summary>
+///     Provide static access to the ResourceManagerWrapper
+/// </summary>
+public static class GlobalResources
+{
+    private static readonly ResourceManagerWrapper _resourceManagerWrapper;
+
+    static GlobalResources ()
+    {
+        // Initialize the ResourceManagerWrapper once
+        var resourceManager = new ResourceManager (typeof (Strings));
+        _resourceManagerWrapper = new (resourceManager);
+    }
+
+    /// <summary>
+    ///     Looks up a resource value for a particular name.  Looks in the specified CultureInfo, and if not found, all parent
+    ///     CultureInfos.
+    /// </summary>
+    /// <param name="name"></param>
+    /// <param name="culture"></param>
+    /// <returns>Null if the resource was not found in the current culture or the invariant culture.</returns>
+    public static object GetObject (string name, CultureInfo culture = null!) { return _resourceManagerWrapper.GetObject (name, culture); }
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    /// <param name="culture"></param>
+    /// <param name="createIfNotExists"></param>
+    /// <param name="tryParents"></param>
+    /// <returns></returns>
+    public static ResourceSet? GetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents)
+    {
+        return _resourceManagerWrapper.GetResourceSet (culture, createIfNotExists, tryParents)!;
+    }
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    /// <param name="culture"></param>
+    /// <param name="createIfNotExists"></param>
+    /// <param name="tryParents"></param>
+    /// <param name="filter"></param>
+    /// <returns></returns>
+    public static ResourceSet? GetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents, Func<DictionaryEntry, bool>? filter)
+    {
+        return _resourceManagerWrapper.GetResourceSet (culture, createIfNotExists, tryParents, filter)!;
+    }
+
+    /// <summary>
+    ///     Looks up a resource value for a particular name. Looks in the specified CultureInfo, and if not found, all parent
+    ///     CultureInfos.
+    /// </summary>
+    /// <param name="name"></param>
+    /// <param name="culture"></param>
+    /// <returns>Null if the resource was not found in the current culture or the invariant culture.</returns>
+    public static string GetString (string name, CultureInfo? culture = null!) { return _resourceManagerWrapper.GetString (name, culture); }
+}

+ 112 - 0
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<DictionaryEntry> ().Any ())
+        {
+            value = _resourceManager.GetResourceSet (CultureInfo.InvariantCulture, createIfNotExists, tryParents)!;
+        }
+
+        return value;
+    }
+
+    public ResourceSet? GetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents, Func<DictionaryEntry, bool>? filter)
+    {
+        ResourceSet value = _resourceManager.GetResourceSet (culture, createIfNotExists, tryParents)!;
+
+        IEnumerable<DictionaryEntry> filteredEntries = value.Cast<DictionaryEntry> ().Where (filter ?? (_ => true));
+
+        ResourceSet? filteredValue = ConvertToResourceSet (filteredEntries);
+
+        if (Equals (culture, CultureInfo.InvariantCulture))
+        {
+            return filteredValue;
+        }
+
+        if (!filteredValue!.Cast<DictionaryEntry> ().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<DictionaryEntry> 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;
+    }
+}

+ 165 - 0
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<string> 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<DictionaryEntry> ());
+    }
+
+    [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<DictionaryEntry> ());
+    }
+
+    [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;
+    }
+}