Skip to content

Converter: Enable translation for error messages (#3313)#10411

Merged
ScarletKuro merged 14 commits intoMudBlazor:devfrom
pwasilewski:feat/localize-converter-error-messages
Jan 8, 2025
Merged

Converter: Enable translation for error messages (#3313)#10411
ScarletKuro merged 14 commits intoMudBlazor:devfrom
pwasilewski:feat/localize-converter-error-messages

Conversation

@pwasilewski
Copy link
Contributor

@pwasilewski pwasilewski commented Dec 9, 2024

Description

This contribution refactors error message handling in Converter and DefaultConverter. The converters now return error message keys and optional arguments. Localization is performed by MudFormComponent, which resolves these keys and arguments into localized error messages using the InternalMudLocalizer service.
Unfortunately, some method signatures and property types have been modified as part of this change, resulting in breaking changes.
Fixes #3313

How Has This Been Tested?

  • Visually: by changing the .resx file and verifying that the appropriate localized error messages appear.

  • Unit Tests: by adding relevant unit tests.

Type of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation (fix or improvement to the website or code docs)

Checklist

  • The PR is submitted to the correct branch (dev).
  • My code follows the code style of this project.
  • I've added relevant tests.

@github-actions github-actions bot added enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library PR: needs review labels Dec 9, 2024
@codecov
Copy link

codecov bot commented Dec 9, 2024

Codecov Report

Attention: Patch coverage is 85.41667% with 7 lines in your changes missing coverage. Please review.

Project coverage is 91.73%. Comparing base (75070da) to head (f6232a7).
Report is 3 commits behind head on dev.

Files with missing lines Patch % Lines
...lazor/Utilities/BindingConverters/DateConverter.cs 0.00% 4 Missing ⚠️
...lazor/Utilities/BindingConverters/BoolConverter.cs 50.00% 2 Missing ⚠️
...lazor/Components/TimePicker/MudTimePicker.razor.cs 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #10411      +/-   ##
==========================================
+ Coverage   91.60%   91.73%   +0.13%     
==========================================
  Files         427      427              
  Lines       13368    13375       +7     
  Branches     2580     2581       +1     
==========================================
+ Hits        12246    12270      +24     
+ Misses        544      531      -13     
+ Partials      578      574       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pwasilewski pwasilewski marked this pull request as draft December 9, 2024 20:52
@pwasilewski pwasilewski marked this pull request as ready for review December 16, 2024 11:59
@versile2
Copy link
Contributor

Add these to BindingConverterTests.cs should get you closer to 100% coverage. Mostly just missing Error testing. Which Looks like Converter.cs instead of setting SetErrorMessage and GetErrorMessage directly the error messages should be calling UpdateSetErrorMessage() and UpdateGetErrorMessage() respectively.

        [Test]
        public void DefaultConverter_TimeSpan_EdgeCases()
        {
            var converter = new DefaultConverter<TimeSpan>();

            // Test invalid format
            converter.Get("25:00").Should().Be(TimeSpan.Zero); // Invalid hours
            converter.GetError.Should().BeTrue();
            converter.GetErrorMessage.Should().Be("Not a valid time span");

            // Test overflow
            converter.Get("999999:00:00").Should().Be(TimeSpan.Zero);
            converter.GetError.Should().BeTrue();
            converter.GetErrorMessage.Should().Be("Not a valid time span");

            // Test invalid characters
            converter.Get("12:ab:00").Should().Be(TimeSpan.Zero);
            converter.GetError.Should().BeTrue();
            converter.GetErrorMessage.Should().Be("Not a valid time span");

            // Test empty/null
            converter.Get("").Should().Be(TimeSpan.Zero);
            converter.Get(null).Should().Be(TimeSpan.Zero);
        }

        [Test]
        public void DefaultConverter_TimeSpanFormat_CustomFormats()
        {
            var converter = new DefaultConverter<TimeSpan>();

            // Test default format
            var time = new TimeSpan(1, 2, 3);
            converter.Set(time).Should().Be("01:02:03");
            converter.Get("01:02:03").Should().Be(time);

            // Test custom DefaultTimeSpanFormat
            converter.DefaultTimeSpanFormat = @"hh\:mm";
            converter.Set(time).Should().Be("01:02");
            converter.Get("01:02").Should().Be(new TimeSpan(1, 2, 0));

            // Test Format property override
            converter.Format = @"hh\:mm\:ss\.fff";
            var preciseTime = new TimeSpan(0, 1, 2, 3, 456);
            converter.Set(preciseTime).Should().Be("01:02:03.456");
            converter.Get("01:02:03.456").Should().Be(preciseTime);
        }

        [Test]
        public void BaseConverter_Culture_AffectsConversion()
        {
            var cultureInfo = new CultureInfo("fr-FR");
            var converter = new Converter<double, string>
            {
                Culture = cultureInfo,
                SetFunc = (d) => d.ToString(cultureInfo),
                GetFunc = (s) => double.Parse(s, cultureInfo)
            };

            converter.Set(1.5).Should().Be("1,5"); // French uses comma
            converter.Get("1,5").Should().Be(1.5);

            cultureInfo = new CultureInfo("en-US");
            converter = new Converter<double, string>
            {
                Culture = cultureInfo,
                SetFunc = (d) => d.ToString(cultureInfo),
                GetFunc = (s) => double.Parse(s, cultureInfo)
            };

            converter.Set(1.5).Should().Be("1.5"); // US uses period
            converter.Get("1.5").Should().Be(1.5);
        }

        [Test]
        public void BaseConverter_NullFuncs_ReturnsDefaults()
        {
            var converter = new Converter<int, string>();
            converter.Set(42).Should().BeNull(); // SetFunc is null
            converter.Get("42").Should().Be(0);  // GetFunc is null
            converter.SetError.Should().BeFalse();
            converter.GetError.Should().BeFalse();
        }

        [Test]
        public void BaseConverter_OnError_CallbackInvoked()
        {
            // Variable to capture the last error message from OnError callback
            var lastError = string.Empty;

            // Create a converter with intentionally failing SetFunc and GetFunc
            var converter = new Converter<int, string>
            {
                OnError = (msg) => lastError = msg, // Assign OnError callback
                SetFunc = (i) => throw new Exception("Set failed"),
                GetFunc = (s) => throw new Exception("Get failed")
            };

            // Test the Set method
            converter.Set(42);
            lastError.Should().Contain("Set failed");
            converter.SetErrorMessage.Should().Contain("Set failed");
            converter.SetError.Should().BeTrue();

            // Test the Get method
            converter.Get("42");
            lastError.Should().Contain("Get failed");
            converter.GetError.Should().BeTrue();
            converter.GetErrorMessage.Should().Contain("Get failed");
        }

@ScarletKuro
Copy link
Member

ScarletKuro commented Dec 17, 2024

Thanks for the PR.

I'm not sure I'm a fan of this approach. The design seems off, with attaching the localizer to the Converter from the side.

First, converters are something I plan to rewrite sooner or later. I want to make them more like in WPF, where the culture is part of the From/To methods, rather than being just a side property, as we have with the localizer here. Also, I'm not a fan of the universal converter that attempts to handle all conversions. As well I don't like that it has this OnError things, it should just throw and someone must just catch. I think the idea was primarily to support inline conversions in Razor syntax with getter and setters, but at this point, I'm not sure it's worth it, after all you could just write a decorator on top of normal conversion for this rather than making it a core logic.

Second, I would approach the translation differently. I wouldn't translate it at the Converter level, so it wouldn't depend on the localizer. Instead, I would spit out the translation keys

-UpdateGetError(Localizer?[LanguageResource.DefaultConverter_InvalidBoolean] ?? "Not a valid boolean");
+UpdateGetError(LanguageResource.DefaultConverter_InvalidBoolean);

And have the components that listen for the OnError event of the converters

_converter.OnError = OnConversionError;

public string? ConversionErrorMessage => _converter.GetErrorMessage;

private void OnConversionError(string error)

handle that. These components would then depend on the localizer, catch the key, and only then perform the translation.

+Localizer[error] / Localizer[_converter.GetErrorMessage]

This would feel more natural.

@pwasilewski pwasilewski marked this pull request as draft December 21, 2024 12:54
@pwasilewski
Copy link
Contributor Author

@ScarletKuro Thank you for the review!

Your solution indeed looks cleaner. However, I have a question regarding handling strings with interpolation. For example:

UpdateGetError($"Conversion to type {typeof(T)} not implemented");

UpdateGetError("Conversion error: " + e.Message);

I initially considered changing

protected void UpdateGetError(string msg)

to UpdateGetError(string msg, params object[] arguments) to support interpolation, but this would also require modifying

public string? GetErrorMessage { get; set; }

to something like a Tuple. This approach doesn’t feel entirely appropriate either. Do you have any recommendations for handling this scenario?

Alternatively, I could change the existing behavior in

public string? ConversionErrorMessage => _converter.GetErrorMessage;

and implement a custom solution, but that also seems strange.

@ScarletKuro
Copy link
Member

I guess we could do a breaking change and make it

public Action<string, object[]>? OnError { get; set; }
public (string?, object[]) GetErrorMessage { get; set; }
protected void UpdateGetError(string msg, object[] arguments)

?

@pwasilewski
Copy link
Contributor Author

pwasilewski commented Jan 1, 2025

Feedback applied. I hope it's better now.

I also made two additional changes since the last update:

  • I changed the prefix for the keys from DefaultConverter to Converter to make it more general.
  • I took inspiration from versile2's suggestion and reorganized the unit tests in BindingConverterTests. Instead of having one method for the happy cases and one for negative cases, I split the tests by type. (I hope this change will help a bit for the future reworks of the converters.)

@pwasilewski pwasilewski marked this pull request as ready for review January 1, 2025 21:59
Copy link
Member

@ScarletKuro ScarletKuro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ScarletKuro ScarletKuro requested a review from henon January 7, 2025 15:32
@ScarletKuro
Copy link
Member

@henon, I’m not sure who else needs to review this.
I don't think many people are using the converter's OnError functionality, so I guess we could add it to v8?

@henon
Copy link
Contributor

henon commented Jan 7, 2025

@ScarletKuro As you can see from the issue number, this issue has existed for a long time. I know this is needed, so I'd say let's go for it.

I know that @HClausing is using converters. @HClausing what do you think about merging this in v8?

@pwasilewski can you write a sentence for our migration guide that advises users how they should adapt their code to this breaking change?

@pwasilewski
Copy link
Contributor Author

@henon
Sure! Does something like this work?

In Converter.cs, several property signatures have been updated to enhance localization support. These properties no longer return English messages directly. Instead, they now return keys intended for localization, accompanied by additional parameters:

  • OnError: Updated from Action<string>? to Action<string, object[]>?. This allows passing a key and optional parameters, enabling more flexible and accurate localization.
  • SetErrorMessage and GetErrorMessage: Both changed from string? to (string, object[])?. These properties now provide a key for localization along with relevant parameters, shifting message formatting to the localization system.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 8, 2025

@ScarletKuro
Copy link
Member

Added to v8.0.0 Migration Guide #9953

@ScarletKuro ScarletKuro merged commit d4a36a6 into MudBlazor:dev Jan 8, 2025
6 checks passed
@HClausing
Copy link
Member

@ScarletKuro As you can see from the issue number, this issue has existed for a long time. I know this is needed, so I'd say let's go for it.

I know that @HClausing is using converters. @HClausing what do you think about merging this in v8?

@pwasilewski can you write a sentence for our migration guide that advises users how they should adapt their code to this breaking change?

I really like this approach.

Yes, I've written some custom Converters to dynamically apply masks for brazilian social ID's (companies and natural peopla have different masks, but should be placed on the same field on model/binding).

Would be nice to have it on v8.

@henon
Copy link
Contributor

henon commented Jan 8, 2025

Thanks @pwasilewski !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Conversion errors should be translateable/editable

5 participants