Skip to content

Commit f07c61d

Browse files
committed
merged branch bschussek/issue5844 (PR symfony#6137)
This PR was merged into the master branch. Commits ------- 586a16e [Validator] Changed DefaultTranslator::getLocale() to always return 'en' 58bfd60 [Validator] Improved the inline documentation of DefaultTranslator cd662cc [Validator] Added ExceptionInterface, BadMethodCallException and InvalidArgumentException e00e5ec [Validator] Fixed failing test cc0df0a [Validator] Changed validator to support pluralized messages by default 56d61eb [Form][Validator] Added BC breaks in unstable code to the CHANGELOG 1e34e91 [Form] Added upgrade instructions to the UPGRADE file b94a256 [DoctrineBridge] Adapted DoctrineBridge to translator integration in the validator c96a051 [FrameworkBundle] Adapted FrameworkBundle to translator integration in the validator 92a3b27 [TwigBridge] Adapted TwigBridge to translator integration in the validator e7eb5b0 [Form] Adapted Form component to translator integration in the validator 46f751c [Validator] Extracted message interpolation logic of ConstraintViolation and used the Translation component for that Discussion ---------- [Validator] Integrated the Translator in the Validator component Bug fix: no Feature addition: no Backwards compatibility break: yes Symfony2 tests pass: yes Fixes the following tickets: symfony#5844, symfony#6117 Todo: - License of the code: MIT Documentation PR: - This PR allows to replace the default message substitution strategy in the validator (`strtr()`) by passing an implementation of `Symfony\Component\Translation\TranslatorInterface`. The motivation for this are both symfony#5844 and the need to replace the translation strategy in Drupal's integration of the Validator. In the stand-alone usage of the validator, both the translator and the default translation domain can now be passed to `ValidatorBuilderInterface`: ```php $validator = Validation::createValidatorBuilder() ->setTranslator(new MyTranslator()) ->setTranslationDomain('validators') ->getValidator(); ``` References: * symfony#5844 * symfony#6117 * symfony#6129 * [Add a validation framework to Drupal 8](http://drupal.org/node/1845546) * [Add the symfony validator component to core despite Symfony potentially releasing BC-breaking updates after 2.3.](http://drupal.org/node/1849564) --------------------------------------------------------------------------- by Tobion at 2012-11-28T08:53:25Z no BC break? Looking at ValidatorBuilderInterface there is definitely one. --------------------------------------------------------------------------- by bschussek at 2012-11-28T08:55:01Z ValidatorBuilderInterface is not part of the stable API. You are not supposed to implement this interface. --------------------------------------------------------------------------- by Tobion at 2012-11-28T09:01:07Z We're not only documenting bc breaks for stable API, otherwise we could remove 90% of the upgrade file since few methods are tagged with API. An interface that nobody should implement? --------------------------------------------------------------------------- by bschussek at 2012-11-28T09:30:02Z The question is what to consider a BC break. Something will always break for someone. Should we consequently mark everything as BC break? I don't think so. For example, since 2.1, you are supposed to use `Validation::createValidator*()` for creating a validator. Because of that, I won't consider changing the constructor signature of `Validator` a BC break anymore from 2.1 on. The same for the unstable interfaces. These are currently meant to be used only, that is, type hint against them and call their methods. But we don't guarantee that we won't add methods to them. --------------------------------------------------------------------------- by Tobion at 2012-11-28T09:38:19Z I agree that almost any change could be considered a BC break. So we probably need to better define what a BC break is and what not. Otherwise Symfony will stop evolving after 2.3 because from then on Fabien wanted to prevent BC breaks which is almost impossible in a strict definition of bc break. --------------------------------------------------------------------------- by fabpot at 2012-11-28T11:37:22Z BC breaks should always be documented, and we guarantee BC only for things tagged with @api. I'm going to update the docs to make things clearer. --------------------------------------------------------------------------- by bschussek at 2012-11-28T13:09:57Z @fabpot I documented these changes now in the CHANGELOG: af99ebb1206ac92889b7193ba1ecc12bf2617e85 Are we sure we want to document *all* BC breaks from now on, even in non-@api code? This could rather scare people looking at our changelogs (lots of BC BREAKS there). --------------------------------------------------------------------------- by fago at 2012-11-28T17:29:58Z Unfortunately, it turns out the symfony translator interface does not mach the Drupal translation system as well as we initally thought, see http://drupal.org/node/1852106. Given that, this would integrating the validator component into Drupal even harder, because it introduces the dependency on the (unwanted) translation component. :( --------------------------------------------------------------------------- by stof at 2012-11-28T18:19:36Z If this does not help Drupal anyway, maybe symfony#5844 is a better way to manage translations for people using the validator outside forms ? and the Drupal guys would simply follow a similar approach, but based on their own translator instead of the symfony one. --------------------------------------------------------------------------- by fago at 2012-11-28T18:50:12Z Yeah. The only problem I see with the approach of symfony#5844 is that *after* validation only the translated messages are available. We'd need to have access to the untranslated messages also. --------------------------------------------------------------------------- by fago at 2012-11-29T09:49:47Z As our translation system handles translating pluralized messages differently, the current ExecutionContextInterface::addViolation() method poses a problem also. We need to pass on - both the single and plural - message, as the message gets chosen during translation, see http://api.drupal.org/api/drupal/core!includes!common.inc/function/format_plural/8 So maybe, we could allow adding an already created ConstraintViolation object also? Then, we could implement a "PluralConstraintViolation" class that takes both message templates. --------------------------------------------------------------------------- by bschussek at 2012-12-03T15:52:36Z I updated this PR to support pluralized messages by default in the validator. This should solve the problem of the Drupal guys, because their implementation of `TranslatorInterface::transChoice($id, $number, ...)` can now simply split the $id by pipes (`|`) and pass the parts to their own `format_plural($count, $singular, $plural, ...)` function. For us, it breaks BC because translation catalog sources had to be adapted. --------------------------------------------------------------------------- by fabpot at 2012-12-03T16:25:52Z Most of the XLF files are broken (the end is missing now). IIUC, we now have a hard dependency on the Translation component, which is something we wanted to avoid. --------------------------------------------------------------------------- by fabpot at 2012-12-03T16:27:56Z Oops, clicked on the "comment" button too fast. So, the dependency is hard (you need to install the dep) but light as we only rely on the translation interface from the component (when using the default translator). It looks acceptable to me, especially because we now use Composer to manage dependencies. --------------------------------------------------------------------------- by bschussek at 2012-12-03T16:54:10Z @fabpot Thanks for the hint. Going to fix this. --------------------------------------------------------------------------- by bschussek at 2012-12-04T11:34:43Z @fabpot Fixed. --------------------------------------------------------------------------- by bschussek at 2012-12-07T12:40:24Z Is there anything missing for this PR to be merged?
2 parents 84e4f4b + 586a16e commit f07c61d

File tree

87 files changed

+974
-454
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+974
-454
lines changed

UPGRADE-2.2.md

+41
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@
6464
Symfony\Component\Form\Exception namespace or to create custom exception
6565
classes for your purpose.
6666

67+
* Translating validation errors is now optional. You can still do so
68+
manually if you like, or you can simplify your templates to simply output
69+
the already translated message.
70+
71+
Before:
72+
73+
```
74+
{{
75+
error.messagePluralization is null
76+
? error.messageTemplate|trans(error.messageParameters, 'validators')
77+
: error.messageTemplate|transchoice(error.messagePluralization, error.messageParameters, 'validators')
78+
}}
79+
```
80+
81+
After:
82+
83+
```
84+
{{ error.message }}
85+
```
86+
6787
#### Deprecations
6888

6989
* The methods `getParent()`, `setParent()` and `hasParent()` in
@@ -186,6 +206,27 @@
186206
}
187207
```
188208

209+
* The sources of the pluralized messages in translation files have changed
210+
from the singular to the pluralized version. If you created custom
211+
translation files for validator errors, you should adapt them.
212+
213+
Before:
214+
215+
<trans-unit id="6">
216+
<source>You must select at least {{ limit }} choices.</source>
217+
<target>Sie müssen mindestens {{ limit }} Möglichkeit wählen.|Sie müssen mindestens {{ limit }} Möglichkeiten wählen.</target>
218+
</trans-unit>
219+
220+
After:
221+
222+
<trans-unit id="6">
223+
<source>You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.</source>
224+
<target>Sie müssen mindestens {{ limit }} Möglichkeit wählen.|Sie müssen mindestens {{ limit }} Möglichkeiten wählen.</target>
225+
</trans-unit>
226+
227+
Check the file src/Symfony/Component/Validator/Resources/translations/validators.en.xlf
228+
for the new message sources.
229+
189230
#### Deprecations
190231

191232
* The interface `ClassMetadataFactoryInterface` was deprecated and will be

src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueValidatorTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bridge\Doctrine\Tests\Validator\Constraints;
1313

1414
use Symfony\Bridge\Doctrine\Tests\DoctrineOrmTestCase;
15+
use Symfony\Component\Validator\DefaultTranslator;
1516
use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory;
1617
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIdentEntity;
1718
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleIdentEntity;
@@ -132,7 +133,7 @@ public function createValidator($entityManagerName, $em, $validateClass = null,
132133
$metadataFactory->addMetadata($metadata);
133134
$validatorFactory = $this->createValidatorFactory($uniqueValidator);
134135

135-
return new Validator($metadataFactory, $validatorFactory);
136+
return new Validator($metadataFactory, $validatorFactory, new DefaultTranslator());
136137
}
137138

138139
private function createSchema($em)

src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig

+1-5
Original file line numberDiff line numberDiff line change
@@ -275,11 +275,7 @@
275275
{% if errors|length > 0 %}
276276
<ul>
277277
{% for error in errors %}
278-
<li>{{
279-
error.messagePluralization is null
280-
? error.messageTemplate|trans(error.messageParameters, 'validators')
281-
: error.messageTemplate|transchoice(error.messagePluralization, error.messageParameters, 'validators')
282-
}}</li>
278+
<li>{{ error.message }}</li>
283279
{% endfor %}
284280
</ul>
285281
{% endif %}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ public function process(ContainerBuilder $container)
2828
$initializers[] = new Reference($id);
2929
}
3030

31-
$container->getDefinition('validator')->replaceArgument(2, $initializers);
31+
$container->getDefinition('validator')->replaceArgument(4, $initializers);
3232
}
3333
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

+1
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode)
370370
->children()
371371
->scalarNode('cache')->end()
372372
->booleanNode('enable_annotations')->defaultFalse()->end()
373+
->scalarNode('translation_domain')->defaultValue('validators')->end()
373374
->end()
374375
->end()
375376
->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+1
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
574574
{
575575
$loader->load('validator.xml');
576576

577+
$container->setParameter('validator.translation_domain', $config['translation_domain']);
577578
$container->setParameter('validator.mapping.loader.xml_files_loader.mapping_files', $this->getValidatorXmlMappingFiles($container));
578579
$container->setParameter('validator.mapping.loader.yaml_files_loader.mapping_files', $this->getValidatorYamlMappingFiles($container));
579580

src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<service id="validator" class="%validator.class%">
2424
<argument type="service" id="validator.mapping.class_metadata_factory" />
2525
<argument type="service" id="validator.validator_factory" />
26+
<argument type="service" id="translator.default" />
27+
<argument>%validator.translation_domain%</argument>
2628
<argument type="collection" />
2729
</service>
2830

Original file line numberDiff line numberDiff line change
@@ -1,21 +1,7 @@
11
<?php if ($errors): ?>
22
<ul>
33
<?php foreach ($errors as $error): ?>
4-
<li><?php
5-
if (null === $error->getMessagePluralization()) {
6-
echo $view['translator']->trans(
7-
$error->getMessageTemplate(),
8-
$error->getMessageParameters(),
9-
'validators'
10-
);
11-
} else {
12-
echo $view['translator']->transChoice(
13-
$error->getMessageTemplate(),
14-
$error->getMessagePluralization(),
15-
$error->getMessageParameters(),
16-
'validators'
17-
);
18-
}?></li>
4+
<li><?php echo $error->getMessage() ?></li>
195
<?php endforeach; ?>
206
</ul>
217
<?php endif ?>

src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_errors.html.php

-21
This file was deleted.

src/Symfony/Component/Form/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CHANGELOG
1212
* deprecated FormException and introduced ExceptionInterface instead
1313
* [BC BREAK] FormException is now an interface
1414
* protected FormBuilder methods from being called when it is turned into a FormConfigInterface with getFormConfig()
15+
* [BC BREAK] inserted argument `$message` in the constructor of `FormError`
1516

1617
2.1.0
1718
-----

src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form
123123
// Only add the error if the form is synchronized
124124
if ($this->acceptsErrors($scope)) {
125125
$scope->addError(new FormError(
126+
$violation->getMessage(),
126127
$violation->getMessageTemplate(),
127128
$violation->getMessageParameters(),
128129
$violation->getMessagePluralization()

src/Symfony/Component/Form/FormError.php

+13-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
*/
1919
class FormError
2020
{
21+
/**
22+
* @var string
23+
*/
24+
private $message;
25+
2126
/**
2227
* The template for the error message
2328
* @var string
@@ -41,16 +46,19 @@ class FormError
4146
*
4247
* Any array key in $messageParameters will be used as a placeholder in
4348
* $messageTemplate.
44-
* @see \Symfony\Component\Translation\Translator
4549
*
46-
* @param string $messageTemplate The template for the error message
50+
* @param string $message The translated error message
51+
* @param string|null $messageTemplate The template for the error message
4752
* @param array $messageParameters The parameters that should be
4853
* substituted in the message template.
4954
* @param integer|null $messagePluralization The value for error message pluralization
55+
*
56+
* @see \Symfony\Component\Translation\Translator
5057
*/
51-
public function __construct($messageTemplate, array $messageParameters = array(), $messagePluralization = null)
58+
public function __construct($message, $messageTemplate = null, array $messageParameters = array(), $messagePluralization = null)
5259
{
53-
$this->messageTemplate = $messageTemplate;
60+
$this->message = $message;
61+
$this->messageTemplate = $messageTemplate ?: $message;
5462
$this->messageParameters = $messageParameters;
5563
$this->messagePluralization = $messagePluralization;
5664
}
@@ -62,7 +70,7 @@ public function __construct($messageTemplate, array $messageParameters = array()
6270
*/
6371
public function getMessage()
6472
{
65-
return strtr($this->messageTemplate, $this->messageParameters);
73+
return $this->message;
6674
}
6775

6876
/**

src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
1919
public function testRow()
2020
{
2121
$form = $this->factory->createNamed('name', 'text');
22-
$form->addError(new FormError('Error!'));
22+
$form->addError(new FormError('[trans]Error![/trans]'));
2323
$view = $form->createView();
2424
$html = $this->renderRow($view);
2525

@@ -58,7 +58,7 @@ public function testRowOverrideVariables()
5858
public function testRepeatedRow()
5959
{
6060
$form = $this->factory->createNamed('name', 'repeated');
61-
$form->addError(new FormError('Error!'));
61+
$form->addError(new FormError('[trans]Error![/trans]'));
6262
$view = $form->createView();
6363
$html = $this->renderRow($view);
6464

@@ -398,7 +398,7 @@ public function testNestedFormError()
398398
)
399399
->getForm();
400400

401-
$form->get('child')->addError(new FormError('Error!'));
401+
$form->get('child')->addError(new FormError('[trans]Error![/trans]'));
402402

403403
$this->assertWidgetMatchesXpath($form->createView(), array(),
404404
'/div

src/Symfony/Component/Form/Tests/AbstractLayoutTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly
283283
public function testErrors()
284284
{
285285
$form = $this->factory->createNamed('name', 'text');
286-
$form->addError(new FormError('Error 1'));
287-
$form->addError(new FormError('Error 2'));
286+
$form->addError(new FormError('[trans]Error 1[/trans]'));
287+
$form->addError(new FormError('[trans]Error 2[/trans]'));
288288
$view = $form->createView();
289289
$html = $this->renderErrors($view);
290290

@@ -1151,7 +1151,7 @@ public function testDateErrorBubbling()
11511151
{
11521152
$child = $this->factory->createNamed('date', 'date');
11531153
$form = $this->factory->createNamed('form', 'form')->add($child);
1154-
$child->addError(new FormError('Error!'));
1154+
$child->addError(new FormError('[trans]Error![/trans]'));
11551155
$view = $form->createView();
11561156

11571157
$this->assertEmpty($this->renderErrors($view));
@@ -1676,7 +1676,7 @@ public function testTimeErrorBubbling()
16761676
{
16771677
$child = $this->factory->createNamed('time', 'time');
16781678
$form = $this->factory->createNamed('form', 'form')->add($child);
1679-
$child->addError(new FormError('Error!'));
1679+
$child->addError(new FormError('[trans]Error![/trans]'));
16801680
$view = $form->createView();
16811681

16821682
$this->assertEmpty($this->renderErrors($view));

src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
1818
public function testRow()
1919
{
2020
$form = $this->factory->createNamed('name', 'text');
21-
$form->addError(new FormError('Error!'));
21+
$form->addError(new FormError('[trans]Error![/trans]'));
2222
$view = $form->createView();
2323
$html = $this->renderRow($view);
2424

@@ -91,7 +91,7 @@ public function testRepeatedRow()
9191
public function testRepeatedRowWithErrors()
9292
{
9393
$form = $this->factory->createNamed('name', 'repeated');
94-
$form->addError(new FormError('Error!'));
94+
$form->addError(new FormError('[trans]Error![/trans]'));
9595
$view = $form->createView();
9696
$html = $this->renderRow($view);
9797

@@ -250,7 +250,7 @@ public function testNestedFormError()
250250
)
251251
->getForm();
252252

253-
$form->get('child')->addError(new FormError('Error!'));
253+
$form->get('child')->addError(new FormError('[trans]Error![/trans]'));
254254

255255
$this->assertWidgetMatchesXpath($form->createView(), array(),
256256
'/table

src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php

+4-6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class ValidationListenerTest extends \PHPUnit_Framework_TestCase
4949

5050
private $message;
5151

52+
private $messageTemplate;
53+
5254
private $params;
5355

5456
protected function setUp()
@@ -63,17 +65,13 @@ protected function setUp()
6365
$this->violationMapper = $this->getMock('Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface');
6466
$this->listener = new ValidationListener($this->validator, $this->violationMapper);
6567
$this->message = 'Message';
68+
$this->messageTemplate = 'Message template';
6669
$this->params = array('foo' => 'bar');
6770
}
6871

6972
private function getConstraintViolation($code = null)
7073
{
71-
return new ConstraintViolation($this->message, $this->params, null, 'prop.path', null, null, $code);
72-
}
73-
74-
private function getFormError()
75-
{
76-
return new FormError($this->message, $this->params);
74+
return new ConstraintViolation($this->message, $this->messageTemplate, $this->params, null, 'prop.path', null, null, $code);
7775
}
7876

7977
private function getBuilder($name = 'name', $propertyPath = null, $dataClass = null)

src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
4848
*/
4949
private $message;
5050

51+
/**
52+
* @var string
53+
*/
54+
private $messageTemplate;
55+
5156
/**
5257
* @var array
5358
*/
@@ -62,6 +67,7 @@ protected function setUp()
6267
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
6368
$this->mapper = new ViolationMapper();
6469
$this->message = 'Message';
70+
$this->messageTemplate = 'Message template';
6571
$this->params = array('foo' => 'bar');
6672
}
6773

@@ -101,15 +107,15 @@ private function getDataMapper()
101107
*/
102108
protected function getConstraintViolation($propertyPath)
103109
{
104-
return new ConstraintViolation($this->message, $this->params, null, $propertyPath, null);
110+
return new ConstraintViolation($this->message, $this->messageTemplate, $this->params, null, $propertyPath, null);
105111
}
106112

107113
/**
108114
* @return FormError
109115
*/
110116
protected function getFormError()
111117
{
112-
return new FormError($this->message, $this->params);
118+
return new FormError($this->message, $this->messageTemplate, $this->params);
113119
}
114120

115121
public function testMapToVirtualFormIfDataDoesNotMatch()

src/Symfony/Component/Validator/CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ CHANGELOG
2828
As of Symfony 2.3, this method will be typed against `MetadataFactoryInterface` instead.
2929
* [BC BREAK] the switches `traverse` and `deep` in the `Valid` constraint and in `GraphWalker::walkReference`
3030
are ignored for arrays now. Arrays are always traversed recursively.
31+
* added dependency to Translation component
32+
* violation messages are now translated with a TranslatorInterface implementation
33+
* [BC BREAK] inserted argument `$message` in the constructor of `ConstraintViolation`
34+
* [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `ExecutionContext`
35+
* [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `GraphWalker`
36+
* [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `ValidationVisitor`
37+
* [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `Validator`
38+
* [BC BREAK] added `setTranslator()` and `setTranslationDomain()` to `ValidatorBuilderInterface`
39+
* improved the Validator to support pluralized messages by default
40+
* [BC BREAK] changed the source of all pluralized messages in the translation files to the pluralized version
41+
* added ExceptionInterface, BadMethodCallException and InvalidArgumentException
3142

3243
2.1.0
3344
-----

0 commit comments

Comments
 (0)