-
Notifications
You must be signed in to change notification settings - Fork 87
Provide a negative unit test for the double translation of PR-104 #188
Provide a negative unit test for the double translation of PR-104 #188
Conversation
That code likely pre-dates the interface. That said, we're removing "Aware" interfaces everywhere, so I'm not sure it makes sense to add that check at this time, either. |
I noticed when running unit tests that we had an additional error, but ONLY when the changes introduced for #188 were in place. I tracked it down to the new test, `testRendersErrorMessagesWithoutDoubleTranslation()` setting a new default translator. This patch also makes a few minor changes for code consistency and readability.
->expects(self::never()) | ||
->method('translate'); | ||
|
||
$this->helper->setTranslator($mockFormTranslator); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This setup doesn't make sense.
If the helper has a translator - and it does - then it WILL be called. Saying you don't expect it to be called indicates that you think the solution is not to call it, which may not be correct.
The difference, though, is what messages it attempts to translate. render()
loops through the result of $element->getMessages()
. This value is set by extracting messages from a zend-inputfilter Input
instance, and those values are extracted from the validator chain attached to the input filter.
Now, with validators, translation happens at message creation, which happens as soon as $validator->error()
is called internally with a message key. At that point, the translator is provided the message matching that message key and the configured text domain, and it then returns the translated message.
What this means is that by the point we get to the FormElementErrors view helper, if the validator translator has translated the message, then the translator associated with the view helper will be asked to attempt to translate the translated message.
In reading back through #104, you got this far.
What I'm asserting is: this test does not prove double translation. It proves that when a translator is attached to the view helper, it will get called. What you should be demonstrating is that it gets called with the translated message from the previous validator.
Now, the next question is: how should we approach a fix, and/or should we?
As you note in #104, the effects can be none, minor, or major. I would argue that the number of scenarios where it would have any effect are incredibly few, and easily avoidable. Check your translations to see if any match the translation keys; if you have any, make changes.
That said, if you are affected, and there's good reason not to fix the translations themselves, there are already solutions.
- Do not pass a translator to the helper before invoking it, or pass
null
tosetTranslator()
immediately prior to invoking it. Unlike zend-validator, we do not have a concept of a default translator for translatable view helpers. You have to pass it to each helper. Using one of these approaches allows you to say, "do not perform translation when invoked" to this helper. - Choose to either translate error messages at the validator or at render. In other words, you can choose not to set a default translator and/or text domain for validators, and only translate at the view layer, or vice versa. If you will be translating other items at the view layer (e.g., labels, descriptions, etc.), set a
null
default translator for validators.
Otherwise, I do not think there are any solutions we can attempt that allow us to keep the feature from #104 and prevent the double translation scenario. And, frankly, I'd argue translation should only ever happen in the view layer, not the validators (the proposed zend-datavalidator component, for example, does not provide ANY translation capabilities; all translation is accomplished either via decorators or in the view).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Saying you don't expect it to be called indicates that you think the solution is not to call it, which may not be correct.
You're correct. But there is no easy way to fix the unit test without deciding which behavior is correct (translation at validator vs translation at render time).
Thats why I opted to use the pre-#104 behavior.
That said, if you are affected, and there's good reason not to fix the translations themselves, there are already solutions.
The effect for our scenario is that we're getting a lot of 'missing translation for key "already translated"' log entries.
Do not pass a translator to the helper before invoking it, or pass null to setTranslator() immediately prior to invoking it.
This is done automatically in Zend\View\HelperPluginManager->injectTranslator()
. I'd have to do some magic to null it after the initializer is done. Sadly delegators wont work because they are invoked before the initializers...
Choose to either translate error messages at the validator or at render.
Pre-#104 error messages were translated at the validator.
Post-#104 error messages are translated at both points in time.
If I translatev the error messages at render time I lose the placeholder capabilities. Thats why the validator has to translate the error messages and then replace the placeholders with their actual values.
The FormElementErrors
helper always assumed that all messages were already translated. There was no ambiguity there and everybody could manage. If I needed a custom message (that was not originating from a validator) I'd set a translated message into the element.
#104 changed this. Now the FormElementErrors
helper always assumes that all message are NOT already translated and tries to translate them again. If we'd follow this train of thought we'd have to remove the translations capabilities from the validators... and lose the capability to use placeholders.
Thats why I think #104 was wrong from a basic level. I doesnt enable anything new and potentionally breaks existing setups.
I've yet to take a look at the new component you've mentioned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MatthiasKuehneEllerhold
Link (RFC) to the new validation component: https://discourse.zendframework.com/t/rfc-new-validation-component/208
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done automatically in Zend\View\HelperPluginManager->injectTranslator(). I'd have to do some magic to null it after the initializer is done. Sadly delegators wont work because they are invoked before the initializers...
I'd forgotten about that part, and that does change my analysis.
I'm re-opening; I have no idea how we'll approach a solution, nor how soon, but that aspect definitely means we need to address this somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've looked at the RFC and a little bit at the new component. Sounds promising! Is there a timeline when it'll be ready and will the old zend-validator component be deprecated then?
In looking through this, I think there's really only one way we can achieve both the goals of #104 and resolve the issues you brought up in here: have two separate helpers, and a way to tell echo $this->formElementErrors($element); // no translation
echo $this->translatedFormElementErrors($element); // translated version
echo $this->formRow($element); // no translation
echo $this->formRow()->setErrorsHelper($this->translatedFormElementErrors()); // w/translation
$this->formRow()->setErrorsHelper($this->formElementErrors()); // reset to original variant Thoughts? |
Wouldn't it be enough to make this behavior togglable in the current helper? $this->formElementErrors()->setTranslateMessages(true);
echo $this->formRow($element); // helper will translate messages
$this->formElementErrors()->setTranslateMessages(false);
echo $this->formRow($element); // helper will not translate messages I'd argue that the default value should be false (= no translation) because this was the behavior of most of the libraries lifetime and it only recently changed. |
I also prefer this solution. No need for:
echo $this->formElementErrors($element); // no translation
echo $this->formElementErrors($element)->setTranslateMessages(true); // translated version And the view helper
|
This patch modifies `FormElementErrors` to add a new method, `setTranslateMessages(bool $flag)`. By default, and per #104, translations are enabled. However, by calling the method with `false`, you can disable translations.
@MatthiasKuehneEllerhold — I have pushed changes to your branch:
As noted, I've kept the default to If both of you could review, I'd appreciate it! |
@@ -60,39 +69,32 @@ public function render(ElementInterface $element, array $attributes = []) | |||
if (empty($messages)) { | |||
return ''; | |||
} | |||
if (! is_array($messages) && ! $messages instanceof Traversable) { | |||
|
|||
$messages = $messages instanceof Traversable ? iterator_to_array($messages) : $messages; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this, as array_walk_recursive()
ONLY allows arrays, and allowing a Traversable
was going to lead to problems.
if (! is_array($messages) && ! $messages instanceof Traversable) { | ||
|
||
$messages = $messages instanceof Traversable ? iterator_to_array($messages) : $messages; | ||
if (! is_array($messages)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing so allowed simplifying this conditional to only look for array values.
$messages = $this->flattenMessages($messages); | ||
if (empty($messages)) { | ||
return ''; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flattening messages first allows us to return early, before doing any attribute aggregation.
$this->helper = new FormElementErrorsHelper(); | ||
parent::setUp(); | ||
} | ||
|
||
public function tearDown() | ||
{ | ||
AbstractValidator::setDefaultTranslator($this->defaultTranslator); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to add this, as otherwise having a default translator caused other tests to fail.
$this->helper->setTranslatorTextDomain('default'); | ||
|
||
// Disable translation... | ||
$this->helper->setTranslateMessages(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this line to your test, as it's now testing what happens when we disable translations.
|
||
$markup = $this->helper->render($form->get('test_element')); | ||
|
||
$this->assertRegexp('#^<ul>\s*<li>TRANSLATED#s', $markup); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modified this regex, as the main thing is we're looking for the string "TRANSLATED"; the previous regex never matched, and it was more difficult to shoe-horn it to work than to do the above.
Your edits look fine to me! Thank you very much for listening and keeping this issue on your radar. I have just a small correction for the example calls from @froschdesign:
Otherwise: if you call the helper with an |
That's right, only the navigation view helpers have a That's why we need the documentation. (#203) |
As requested in #104 (comment) I provide a negative unit test to prove that error message from zend-validator will get doubly translated.
Some notes:
Zend\Form\Form
,Zend\Form\Element\Color
,Zend\Validator\Regex
, etc...Zend\Form\Form
uses duck-typing to fulfillZend\InputFilter\InputFilterAwareInterface
but not actually implementing it. Why is there no "implements InputFilterAwareInterface"?Zend\Validator\Translator\TranslatorInterface
andZend\I18n\Translator\TranslatorInterface
are 2 different interfaces and onlyZend\Mvc\I18n\Translator
implements both. But thats not a dependency of this module.