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