Skip to content

Conversation

@innocenzi
Copy link
Member

@innocenzi innocenzi commented Oct 6, 2025

Closes #1611

This pull request gives the ability to do that:

final class Book {
    #[Rules\HasLength(min: 5, max: 50)]
    #[TranslationKey('book_management.book_title')]
    public string $title;
}

When the HasLength rule fails, instead of using the validation_error.has_length translation key to get the error message, the validator will use validation_error.has_length.book_management.book_title—basically, appending the specified translation key to what would have been the original translation key.

Technically, it's a breaking change, since the returned failing rules are now wrapped in a FailingRule VO

@innocenzi innocenzi force-pushed the feat/validator-custom-messages branch from f078c90 to f182b0c Compare October 6, 2025 12:39
@innocenzi innocenzi changed the title feat(valitation): add ability to specify translation keys for specific properties feat(valitation)!: add ability to specify translation keys for specific properties Oct 6, 2025
@innocenzi innocenzi changed the title feat(valitation)!: add ability to specify translation keys for specific properties feat(validation)!: add ability to specify translation keys for specific properties Oct 6, 2025
@azjezz
Copy link
Contributor

azjezz commented Oct 6, 2025

The problem i see with this approach is when using a xliff format to store translations ( ref #1610 ), book related things won't be part of the same group. example:

<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
        <body>
            <!-- Group: Book-related translations -->
            <group id="book">
                <!-- Subgroup: Create Book -->
                <group id="book.create">
                    <!-- Form Labels -->
                    <group id="book.create.form">
                        <trans-unit id="book.create.form.title">
                            <source>book.create.form.title</source>
                            <target>Title</target>
                        </trans-unit>

                        <!-- Additional form labels can be added here -->
                    </group>

                    <!-- Error Messages -->
                    <group id="book.create.error">
                        <trans-unit id="book.create.error.title.max_length">
                            <source>book.create.error.title.max_length</source>
                            <target>The book title must not exceed {max} characters.</target>
                        </trans-unit>

                        <trans-unit id="book.create.error.title.min_length">
                            <source>book.create.error.title.min_length</source>
                            <target>The book title must be at least {min} characters long.</target>
                        </trans-unit>
                    </group>
                </group>
            </group>

            <!-- Additional translation units can be added here -->
        </body>
    </file>
</xliff>

Compared to the current:

<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
        <body>
            <!-- Group: Book-related translations -->
            <group id="book">
                <!-- Subgroup: Create Book -->
                <group id="book.create">
                    <!-- Form Labels -->
                    <group id="book.create.form">
                        <trans-unit id="book.create.form.title">
                            <source>book.create.form.title</source>
                            <target>Title</target>
                        </trans-unit>

                        <!-- Additional form labels can be added here -->
                    </group>
                </group>
            </group>

            <!-- Group: Validation Errors -->
            <group id="validation_error">
                <!-- Has Length Errors -->
                <group id="validation_error.has_length">
                    <!-- Book Management Specific Errors -->
                    <group id="validation_error.has_length.book_management">
                        <trans-unit id="validation_error.has_length.book_management.book_title">
                            <source>validation_error.has_length.book_management.book_title</source>
                            <target>The book title must be between 5 and 50 characters long.</target>
                        </trans-unit>
                    </group>
                </group>
            </group>

            <!-- Additional translation units can be added here -->
        </body>
    </file>
</xliff>

Grouping in xliff is useful not just for making things obvious, when using an online translation service where translators will be updating translations, this provides context to them, and makes navigating to the Book -> Create -> Errors section much easier.

Also, having parameters for translation here is a MUST, for big projects, people who work on translation never touch the code, nor do they know what is the minimum or maximum value allowed, therefor, it should not be hardcoded in the message.

@innocenzi
Copy link
Member Author

I see that the translation units' ids are being repeated anyway, so I'm not sure how it's an issue? Because of the tool you use to generate the files? Can you not group book management stuff together, despite the ids starting differently?

Also, having parameters for translation here is a MUST, for big projects, people who work on translation never touch the code, nor do they know what is the minimum or maximum value allowed, therefor, it should not be hardcoded in the message.

There's no code to touch, it's all in the translation message:

.input {$field :string}
.input {$min :number default=null}
.input {$max :number default=null}
.local $has_min = {$min :boolean}
.local $has_max = {$max :boolean}
.match $has_min $has_max
  true true {{{$field} must be between {$min} and {$max}}}
  true false {{{$field} must be at least {$min}}}
  false true {{{$field} must be at most {$max}}}

The content above would be in the <target> tag of your xliff.

See: https://messageformat.unicode.org/
Example: https://messageformat.unicode.org/playground/#ICAgIC5pbnB1dCB7JGZpZWxkIDpzdHJpbmd9CiAgICAuaW5wdXQgeyRtaW4gOm51bWJlciBkZWZhdWx0PW51bGx9CiAgICAuaW5wdXQgeyRtYXggOm51bWJlciBkZWZhdWx0PW51bGx9CiAgICAubG9jYWwgJGhhc19taW4gPSB7JG1pbiA6Ym9vbGVhbn0KICAgIC5sb2NhbCAkaGFzX21heCA9IHskbWF4IDpib29sZWFufQogICAgLm1hdGNoICRoYXNfbWluICRoYXNfbWF4CiAgICAgIHRydWUgZmFsc2Uge3t7JGZpZWxkfSBtdXN0IGJlIGF0IGxlYXN0IHskbWlufX19CiAgICAgIGZhbHNlIHRydWUge3t7JGZpZWxkfSBtdXN0IGJlIGF0IG1vc3QgeyRtYXh9fX0KICAgICAgKiAqIHt7eyRmaWVsZH0gbXVzdCBiZSBiZXR3ZWVuIHskbWlufSBhbmQgeyRtYXh9fX0.ewogICJmaWVsZCI6ICJCb29rIHRpdGxlIGxlbmd0aCIsCiAgIm1pbiI6IDAsCiAgIm1heCI6IDEwMAp9.ZW4tVVM
(don't mind the errors, Tempest has custom helper selectors/functions)

@azjezz
Copy link
Contributor

azjezz commented Oct 6, 2025

The problem now is that validation_error is its own group now. while translation unit for books should all be under the book group.

This creates fraction, as things related to book will now be in completely different groups.

@azjezz
Copy link
Contributor

azjezz commented Oct 6, 2025

There's no code to touch, it's all in the translation message:

Ah cool!

But this still does not the address for the need of a specific message for the min/max.

If we have a field with a min, and no max, and the error message is already translated to 100 locales, when we deploy a new version that sets a max constraint, it might take time for the translation team to catch up and update the message, so users will be getting a confusing message in this duration.

We would rather users see a translation message of the fallback.

@innocenzi
Copy link
Member Author

If we have a field with a min, and no max, and the error message is already translated to 100 locales, when we deploy a new version that sets a max constraint, it might take time for the translation team to catch up and update the message, so users will be getting a confusing message in this duration.

Is that really an issue? If you are migrating to Tempest, updating translations is part of the process, since we use a different format than other frameworks.

MessageFormat 2.0 has clear advantages—rich, readable, context-aware translation messages. It has not been designed to work on an attribute basis, and we won't support that. Especially since this format expects fallbacks! You are not supposed to end up with translation keys being presented to end users

@azjezz
Copy link
Contributor

azjezz commented Oct 7, 2025

Is that really an issue? If you are migrating to Tempest, updating translations is part of the process, since we use a different format than other frameworks.

format is not an issue, i can simply export translations in another format.

But having specific messages for different kind of problems is important. something being too short is not the same as something being too long. The example i specified shows how adding a new limit invalidates all translation messages, that should not be the case, the max constraint is a different thing that should have its own message :/

@innocenzi
Copy link
Member Author

But having specific messages for different kind of problems is important. something being too short is not the same as something being too long.

I agree! I think you did not fully grasp what MF2 is capable of. Let's take my example:

.input {$field :string}
.input {$min :number default=null}
.input {$max :number default=null}
.local $has_min = {$min :boolean}
.local $has_max = {$max :boolean}
.match $has_min $has_max
  true true {{{$field} must be between {$min} and {$max}}}
  true false {{{$field} must be at least {$min}}}
  false true {{{$field} must be at most {$max}}}
  false false {{{{$field}} is invalid}}

If both a max and min are specified in the rule, the message will be:

Field must be between 0 and 100

If just a max is specified, the message will be:

Field must be at most 100

If just a min is specified, the message will be:

Field must be at least 0

If a new variant gets added (though in the example above, it is not possible), then the fallback will be used until a proper variant is specified:

Field is invalid

@azjezz
Copy link
Contributor

azjezz commented Oct 7, 2025

The problem is i want a different message depending on what failed validation, not depending on constraint configuration.

If min: 5, max: 20, and user supplied a message that is 4 in length, i want to show an error message specifically about minimum length constraint. not Field should be between 5 and 20.

My assumption is the current way this works, user will always see Field should be between 5 and 20 even if their message is below 20.

@innocenzi
Copy link
Member Author

Ah, well, that's actually rule-dependent then, and theoretically still achievable through MF2, though we don't provide the value as a translation variable currently.

What is certain though, is that we don't (and won't) have more than one error message translation key by rule, since the whole point of MF2 is to achieve this level of flexibility

I'm curious though, how would you implement in other frameworks what you are asking here? What would the error message be if value was 4 but min was 5 and maxwas100`?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add ability to specify a custom message for validation rules

2 participants