-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,11 @@ class FormElementErrors extends AbstractHelper | |
*/ | ||
protected $attributes = []; | ||
|
||
/** | ||
* @var bool Whether or not to translate error messages during render. | ||
*/ | ||
protected $translateErrorMessages = true; | ||
|
||
/** | ||
* Invoke helper as functor | ||
* | ||
|
@@ -49,6 +54,10 @@ public function __invoke(ElementInterface $element = null, array $attributes = [ | |
/** | ||
* Render validation errors for the provided $element | ||
* | ||
* If {@link $translateErrorMessages} is true, and a translator is | ||
* composed, messages retrieved from the element will be translated; if | ||
* either is not the case, they will not. | ||
* | ||
* @param ElementInterface $element | ||
* @param array $attributes | ||
* @throws Exception\DomainException | ||
|
@@ -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; | ||
if (! is_array($messages)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doing so allowed simplifying this conditional to only look for array values. |
||
throw new Exception\DomainException(sprintf( | ||
'%s expects that $element->getMessages() will return an array or Traversable; received "%s"', | ||
__METHOD__, | ||
(is_object($messages) ? get_class($messages) : gettype($messages)) | ||
)); | ||
} | ||
|
||
// Flatten message array | ||
$messages = $this->flattenMessages($messages); | ||
if (empty($messages)) { | ||
return ''; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
// Prepare attributes for opening tag | ||
$attributes = array_merge($this->attributes, $attributes); | ||
$attributes = $this->createAttributesString($attributes); | ||
if (! empty($attributes)) { | ||
$attributes = ' ' . $attributes; | ||
} | ||
|
||
// Flatten message array | ||
$escapeHtml = $this->getEscapeHtmlHelper(); | ||
$messagesToPrint = []; | ||
$translator = $this->getTranslator(); | ||
$textDomain = $this->getTranslatorTextDomain(); | ||
$messageCallback = function ($item) use (&$messagesToPrint, $escapeHtml, $translator, $textDomain) { | ||
$item = $translator ? $translator->translate($item, $textDomain) : $item; | ||
$messagesToPrint[] = $escapeHtml($item); | ||
}; | ||
array_walk_recursive($messages, $messageCallback); | ||
|
||
if (empty($messagesToPrint)) { | ||
return ''; | ||
} | ||
|
||
// Generate markup | ||
$markup = sprintf($this->getMessageOpenFormat(), $attributes); | ||
$markup .= implode($this->getMessageSeparatorString(), $messagesToPrint); | ||
$markup .= implode($this->getMessageSeparatorString(), $messages); | ||
$markup .= $this->getMessageCloseString(); | ||
|
||
return $markup; | ||
|
@@ -185,4 +187,56 @@ public function getMessageSeparatorString() | |
{ | ||
return $this->messageSeparatorString; | ||
} | ||
|
||
/** | ||
* Set the flag detailing whether or not to translate error messages. | ||
* | ||
* @param bool $flag | ||
* @return self | ||
*/ | ||
public function setTranslateMessages($flag) | ||
{ | ||
$this->translateErrorMessages = (bool) $flag; | ||
return $this; | ||
} | ||
|
||
/** | ||
* @param array $messages | ||
* @return array | ||
*/ | ||
private function flattenMessages(array $messages) | ||
{ | ||
return $this->translateErrorMessages && $this->getTranslator() | ||
? $this->flattenMessagesWithTranslator($messages) | ||
: $this->flattenMessagesWithoutTranslator($messages); | ||
} | ||
|
||
/** | ||
* @param array $messages | ||
* @return array | ||
*/ | ||
private function flattenMessagesWithoutTranslator(array $messages) | ||
{ | ||
$messagesToPrint = []; | ||
array_walk_recursive($messages, function ($item) use (&$messagesToPrint) { | ||
$messagesToPrint[] = $item; | ||
}); | ||
return $messagesToPrint; | ||
} | ||
|
||
/** | ||
* @param array $messages | ||
* @return array | ||
*/ | ||
private function flattenMessagesWithTranslator(array $messages) | ||
{ | ||
$translator = $this->getTranslator(); | ||
$textDomain = $this->getTranslatorTextDomain(); | ||
$messagesToPrint = []; | ||
$messageCallback = function ($item) use (&$messagesToPrint, $translator, $textDomain) { | ||
$messagesToPrint[] = $translator->translate($item, $textDomain); | ||
}; | ||
array_walk_recursive($messages, $messageCallback); | ||
return $messagesToPrint; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,16 +10,29 @@ | |
namespace ZendTest\Form\View\Helper; | ||
|
||
use Zend\Form\Element; | ||
use Zend\Form\Form; | ||
use Zend\Form\View\Helper\FormElementErrors as FormElementErrorsHelper; | ||
use Zend\Validator\AbstractValidator; | ||
|
||
class FormElementErrorsTest extends CommonTestCase | ||
{ | ||
/** | ||
* @var null|\Zend\Validator\Translator\TranslatorInterface | ||
*/ | ||
protected $defaultTranslator; | ||
|
||
public function setUp() | ||
{ | ||
$this->defaultTranslator = AbstractValidator::getDefaultTranslator(); | ||
$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 commentThe 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. |
||
} | ||
|
||
public function getMessageList() | ||
{ | ||
return [ | ||
|
@@ -76,6 +89,53 @@ public function testRendersErrorMessagesUsingUnorderedListTranslated() | |
// @codingStandardsIgnoreEnd | ||
} | ||
|
||
public function testRendersErrorMessagesWithoutDoubleTranslation() | ||
{ | ||
$form = new Form('test_form'); | ||
$form->add([ | ||
'name' => 'test_element', | ||
'type' => Element\Color::class, | ||
]); | ||
$form->setData(['test_element' => 'This is invalid!']); | ||
|
||
$mockValidatorTranslator = $this->createMock('Zend\Validator\Translator\TranslatorInterface'); | ||
$mockValidatorTranslator | ||
->expects(self::once()) | ||
->method('translate') | ||
->willReturnCallback( | ||
function ($message) { | ||
self::assertEquals( | ||
'The input does not match against pattern \'%pattern%\'', | ||
$message, | ||
'Unexpected translation key.' | ||
); | ||
|
||
return 'TRANSLATED: The input does not match against pattern \'%pattern%\''; | ||
} | ||
); | ||
|
||
AbstractValidator::setDefaultTranslator($mockValidatorTranslator, 'default'); | ||
|
||
self::assertFalse($form->isValid()); | ||
|
||
$mockFormTranslator = $this->createMock('Zend\I18n\Translator\Translator'); | ||
$mockFormTranslator | ||
->expects(self::never()) | ||
->method('translate'); | ||
|
||
$this->helper->setTranslator($mockFormTranslator); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Now, with validators, translation happens at message creation, which happens as soon as 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.
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 commentThe reason will be displayed to describe this comment to others. Learn more.
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).
The effect for our scenario is that we're getting a lot of 'missing translation for key "already translated"' log entries.
This is done automatically in
Pre-#104 error messages were translated at the validator. 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 #104 changed this. Now the 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 commentThe reason will be displayed to describe this comment to others. Learn more. @MatthiasKuehneEllerhold There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 commentThe 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? |
||
$this->assertTrue($this->helper->hasTranslator()); | ||
|
||
$this->helper->setTranslatorTextDomain('default'); | ||
|
||
// Disable translation... | ||
$this->helper->setTranslateMessages(false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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. |
||
} | ||
|
||
public function testCanSpecifyAttributesForOpeningTag() | ||
{ | ||
$messages = $this->getMessageList(); | ||
|
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 aTraversable
was going to lead to problems.