From 18b3cb08c02c1bc605e589542e05cb2f816afcb8 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:56:18 +0100 Subject: [PATCH] Add app-level override for localization resources Enable application resource to override library translations in Translator. Add SetPrimaryTranslatorResource method and update translation logic. Include unit tests and resource files to verify override and fallback behavior. --- .../Localizations/Translator.cs | 56 +++++++++++++++---- .../Resources/OverrideApplication.cs | 10 ++++ .../Resources/OverrideApplication.resx | 18 ++++++ .../Resources/OverrideLibrary.cs | 10 ++++ .../Resources/OverrideLibrary.resx | 21 +++++++ .../Localizations/TranslatorTests.cs | 55 +++++++++++++++++- 6 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.cs create mode 100644 src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.resx create mode 100644 src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.cs create mode 100644 src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.resx diff --git a/src/AXSharp.connectors/src/AXSharp.Connector/Localizations/Translator.cs b/src/AXSharp.connectors/src/AXSharp.Connector/Localizations/Translator.cs index 40100fb3e..0e59580f3 100644 --- a/src/AXSharp.connectors/src/AXSharp.Connector/Localizations/Translator.cs +++ b/src/AXSharp.connectors/src/AXSharp.Connector/Localizations/Translator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -17,9 +17,20 @@ namespace AXSharp.Connector.Localizations /// public class Translator { - //private ResxLocalizations LocalizationResources { get; set; } - - private ResourceManager _resourceManager; + private IEnumerable ResourceManagers + { + get + { + // IMPORTANT: order matters. Application should be searched first to allow overrides. + // Do not cache: resources can be configured at runtime (e.g., SetPrimaryTranslatorResource). + if (_applicationResourceManager != null) yield return _applicationResourceManager; + if (_libraryResourceManager != null) yield return _libraryResourceManager; + } + } + + private static ResourceManager _applicationResourceManager; + + private ResourceManager _libraryResourceManager; private CultureInfo Culture = CultureInfo.InvariantCulture; @@ -45,7 +56,7 @@ public void SetLocalizationResource(Type resourceType) { if (resourceType != null) { - _resourceManager = new ResourceManager(resourceType) + _libraryResourceManager = new ResourceManager(resourceType) { IgnoreCase = true }; @@ -115,20 +126,28 @@ private string LocalizeInParents(string token, ITwinElement rootObj, CultureInfo public string Localize(string str, ITwinElement twinElement, CultureInfo culture) { - + foreach (var localizable in GetTranslatable(str)) { var validIdentifier = LocalizationHelper.CreateId(localizable.CleanUpLocalizationTokens()); - // Search in first level resource - var translation = _resourceManager?.GetString(validIdentifier, culture); - - // Search in parent resources + // Search through all resource managers for the key + string translation = null; + foreach (var resourceManager in ResourceManagers) + { + translation = resourceManager?.GetString(validIdentifier, culture); + if (translation != null) + { + break; // Found translation, stop searching + } + } + + // Search in parent resources if not found if (translation == null) { try { - return LocalizeInParents(str, twinElement, culture); + translation = LocalizeInParents(localizable, twinElement, culture); } catch { @@ -144,5 +163,20 @@ public string Localize(string str, ITwinElement twinElement, CultureInfo culture return str; } + + /// + /// Sets the primary application resource for all translators. + /// This would be tipycally set to the resource containing the application's translations. + /// Any matching localization key will be first searched in this resource and then in the library resource. + /// You can leverage this to override library translations with application specific ones. + /// + /// + public static void SetPrimaryTranslatorResource(Type resourceType) + { + _applicationResourceManager = new ResourceManager(resourceType) + { + IgnoreCase = true + }; + } } } diff --git a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.cs b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.cs new file mode 100644 index 000000000..e543ae0a1 --- /dev/null +++ b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.cs @@ -0,0 +1,10 @@ +namespace AXSharp.ConnectorTests.Localizations.Resources +{ + /// + /// Marker type used to locate the embedded resource `OverrideApplication.resx`. + /// + public class OverrideApplication + { + } +} + diff --git a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.resx b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.resx new file mode 100644 index 000000000..5c42b4e48 --- /dev/null +++ b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideApplication.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + APP + + diff --git a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.cs b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.cs new file mode 100644 index 000000000..07a78e28e --- /dev/null +++ b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.cs @@ -0,0 +1,10 @@ +namespace AXSharp.ConnectorTests.Localizations.Resources +{ + /// + /// Marker type used to locate the embedded resource `OverrideLibrary.resx`. + /// + public class OverrideLibrary + { + } +} + diff --git a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.resx b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.resx new file mode 100644 index 000000000..0b195ddc9 --- /dev/null +++ b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/Resources/OverrideLibrary.resx @@ -0,0 +1,21 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + LIB + + + LIB_ONLY + + diff --git a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/TranslatorTests.cs b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/TranslatorTests.cs index f7294ec9a..f1e34c750 100644 --- a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/TranslatorTests.cs +++ b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Localizations/TranslatorTests.cs @@ -2,9 +2,11 @@ namespace AXSharp.ConnectorTests.Localizations { using AXSharp.Connector.Localizations; using System; + using System.Globalization; using Xunit; using NSubstitute; using AXSharp.Connector; + using AXSharp.ConnectorTests.Localizations.Resources; public class TranslatorTests { @@ -53,6 +55,57 @@ public void CanCallSetLocalizationResource() // Act _testClass.SetLocalizationResource(resourceType); - } + } + + [Fact] + public void Translate_prefers_primary_application_resource_over_library_resource() + { + // Arrange + Translator.SetPrimaryTranslatorResource(typeof(OverrideApplication)); + var translator = new Translator(); + translator.SetLocalizationResource(typeof(OverrideLibrary)); + var twin = Substitute.For(); + var originalString = "<#Override token#>"; + + // Act + var result = translator.Translate(originalString, twin, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal("APP", result); + } + + [Fact] + public void Translate_falls_back_to_library_resource_when_primary_does_not_have_key() + { + // Arrange + Translator.SetPrimaryTranslatorResource(typeof(OverrideApplication)); + var translator = new Translator(); + translator.SetLocalizationResource(typeof(OverrideLibrary)); + var twin = Substitute.For(); + var originalString = "<#Library only#>"; + + // Act + var result = translator.Translate(originalString, twin, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal("LIB_ONLY", result); + } + + [Fact] + public void Translate_when_key_missing_returns_text_without_localization_tokens() + { + // Arrange + Translator.SetPrimaryTranslatorResource(typeof(OverrideApplication)); + var translator = new Translator(); + translator.SetLocalizationResource(typeof(OverrideLibrary)); + var twin = Substitute.For(); + var originalString = "<#Does not exist#>"; + + // Act + var result = translator.Translate(originalString, twin, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal("Does not exist", result); + } } } \ No newline at end of file