Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit baad332

Browse files
committed
feature #27399 [Translation] Added intl message formatter. (aitboudad, Nyholm)
This PR was merged into the 4.2-dev branch. Discussion ---------- [Translation] Added intl message formatter. | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | replaces #20007 | License | MIT | Doc PR | This PR will replace #20007 and continue the work from the original author. I've have tried to address all comments made in the original PR. Commits ------- 2a90931 cs fb30c77 Be more specific with what exception we catch b1aa004 Only use the default translator if intl extension is loaded f88153f Updates according to feedback 597a15d Use FallbackFormatter instead of support for multiple formatters 2aa7181 Fixes according to feedback a325a44 Allow config for different domain specific formatters b43fe21 Add support for multiple formatters c2b3dc0 [Translation] Added intl message formatter. 19e8e69 use error 940d440 Make it a warning
2 parents 68a910f + 2a90931 commit baad332

File tree

7 files changed

+429
-2
lines changed

7 files changed

+429
-2
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode)
697697
->defaultValue(array('en'))
698698
->end()
699699
->booleanNode('logging')->defaultValue(false)->end()
700-
->scalarNode('formatter')->defaultValue('translator.formatter.default')->end()
700+
->scalarNode('formatter')->defaultValue(class_exists(\MessageFormatter::class) ? 'translator.formatter.default' : 'translator.formatter.symfony')->end()
701701
->scalarNode('default_path')
702702
->info('The default path used to load translations')
703703
->defaultValue('%kernel.project_dir%/translations')

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@
2929
<tag name="monolog.logger" channel="translation" />
3030
</service>
3131

32-
<service id="translator.formatter.default" class="Symfony\Component\Translation\Formatter\MessageFormatter">
32+
<service id="translator.formatter.symfony" class="Symfony\Component\Translation\Formatter\MessageFormatter">
3333
<argument type="service" id="identity_translator" />
3434
</service>
35+
<service id="translator.formatter.intl" class="Symfony\Component\Translation\Formatter\IntlMessageFormatter" public="false" />
36+
<service id="translator.formatter.fallback" class="Symfony\Component\Translation\Formatter\FallbackFormatter" public="false">
37+
<argument type="service" id="translator.formatter.intl" />
38+
<argument type="service" id="translator.formatter.symfony" />
39+
</service>
40+
<service id="translator.formatter.default" alias="translator.formatter.fallback" />
3541

3642
<service id="translation.loader.php" class="Symfony\Component\Translation\Loader\PhpFileLoader">
3743
<tag name="translation.loader" alias="php" />

src/Symfony/Component/Translation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Started using ICU parent locales as fallback locales.
88
* deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface`
99
* deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead
10+
* Added `IntlMessageFormatter` and `FallbackMessageFormatter`
1011

1112
4.1.0
1213
-----
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Formatter;
13+
14+
use Symfony\Component\Translation\Exception\InvalidArgumentException;
15+
use Symfony\Component\Translation\Exception\LogicException;
16+
17+
class FallbackFormatter implements MessageFormatterInterface, ChoiceMessageFormatterInterface
18+
{
19+
/**
20+
* @var MessageFormatterInterface|ChoiceMessageFormatterInterface
21+
*/
22+
private $firstFormatter;
23+
24+
/**
25+
* @var MessageFormatterInterface|ChoiceMessageFormatterInterface
26+
*/
27+
private $secondFormatter;
28+
29+
public function __construct(MessageFormatterInterface $firstFormatter, MessageFormatterInterface $secondFormatter)
30+
{
31+
$this->firstFormatter = $firstFormatter;
32+
$this->secondFormatter = $secondFormatter;
33+
}
34+
35+
public function format($message, $locale, array $parameters = array())
36+
{
37+
try {
38+
$result = $this->firstFormatter->format($message, $locale, $parameters);
39+
} catch (InvalidArgumentException $e) {
40+
return $this->secondFormatter->format($message, $locale, $parameters);
41+
}
42+
43+
if ($result === $message) {
44+
$result = $this->secondFormatter->format($message, $locale, $parameters);
45+
}
46+
47+
return $result;
48+
}
49+
50+
public function choiceFormat($message, $number, $locale, array $parameters = array())
51+
{
52+
// If both support ChoiceMessageFormatterInterface
53+
if ($this->firstFormatter instanceof ChoiceMessageFormatterInterface && $this->secondFormatter instanceof ChoiceMessageFormatterInterface) {
54+
try {
55+
$result = $this->firstFormatter->choiceFormat($message, $number, $locale, $parameters);
56+
} catch (InvalidArgumentException $e) {
57+
return $this->secondFormatter->choiceFormat($message, $number, $locale, $parameters);
58+
}
59+
60+
if ($result === $message) {
61+
$result = $this->secondFormatter->choiceFormat($message, $number, $locale, $parameters);
62+
}
63+
64+
return $result;
65+
}
66+
67+
if ($this->firstFormatter instanceof ChoiceMessageFormatterInterface) {
68+
return $this->firstFormatter->choiceFormat($message, $number, $locale, $parameters);
69+
}
70+
71+
if ($this->secondFormatter instanceof ChoiceMessageFormatterInterface) {
72+
return $this->secondFormatter->choiceFormat($message, $number, $locale, $parameters);
73+
}
74+
75+
throw new LogicException(sprintf('No formatters support plural translations.'));
76+
}
77+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Formatter;
13+
14+
use Symfony\Component\Translation\Exception\InvalidArgumentException;
15+
16+
/**
17+
* @author Guilherme Blanco <[email protected]>
18+
* @author Abdellatif Ait boudad <[email protected]>
19+
*/
20+
class IntlMessageFormatter implements MessageFormatterInterface
21+
{
22+
/**
23+
* {@inheritdoc}
24+
*/
25+
public function format($message, $locale, array $parameters = array())
26+
{
27+
try {
28+
$formatter = new \MessageFormatter($locale, $message);
29+
} catch (\Throwable $e) {
30+
throw new InvalidArgumentException(sprintf('Invalid message format (%s, error #%d).', intl_get_error_message(), intl_get_error_code()), 0, $e);
31+
}
32+
33+
$message = $formatter->format($parameters);
34+
if (U_ZERO_ERROR !== $formatter->getErrorCode()) {
35+
throw new InvalidArgumentException(sprintf('Unable to format message ( %s, error #%s).', $formatter->getErrorMessage(), $formatter->getErrorCode()));
36+
}
37+
38+
return $message;
39+
}
40+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Tests\Formatter;
13+
14+
use Symfony\Component\Translation\Exception\InvalidArgumentException;
15+
use Symfony\Component\Translation\Exception\LogicException;
16+
use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface;
17+
use Symfony\Component\Translation\Formatter\FallbackFormatter;
18+
use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
19+
20+
class FallbackFormatterTest extends \PHPUnit\Framework\TestCase
21+
{
22+
public function testFormatSame()
23+
{
24+
$first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
25+
$first
26+
->expects($this->once())
27+
->method('format')
28+
->with('foo', 'en', array(2))
29+
->willReturn('foo');
30+
31+
$second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
32+
$second
33+
->expects($this->once())
34+
->method('format')
35+
->with('foo', 'en', array(2))
36+
->willReturn('bar');
37+
38+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
39+
}
40+
41+
public function testFormatDifferent()
42+
{
43+
$first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
44+
$first
45+
->expects($this->once())
46+
->method('format')
47+
->with('foo', 'en', array(2))
48+
->willReturn('new value');
49+
50+
$second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
51+
$second
52+
->expects($this->exactly(0))
53+
->method('format')
54+
->withAnyParameters();
55+
56+
$this->assertEquals('new value', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
57+
}
58+
59+
public function testFormatException()
60+
{
61+
$first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
62+
$first
63+
->expects($this->once())
64+
->method('format')
65+
->willThrowException(new InvalidArgumentException());
66+
67+
$second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
68+
$second
69+
->expects($this->once())
70+
->method('format')
71+
->with('foo', 'en', array(2))
72+
->willReturn('bar');
73+
74+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
75+
}
76+
77+
public function testFormatExceptionUnknown()
78+
{
79+
$first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
80+
$first
81+
->expects($this->once())
82+
->method('format')
83+
->willThrowException(new \RuntimeException());
84+
85+
$second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
86+
$second
87+
->expects($this->exactly(0))
88+
->method('format');
89+
90+
$this->expectException(\RuntimeException::class);
91+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
92+
}
93+
94+
public function testChoiceFormatSame()
95+
{
96+
$first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
97+
$first
98+
->expects($this->once())
99+
->method('choiceFormat')
100+
->with('foo', 1, 'en', array(2))
101+
->willReturn('foo');
102+
103+
$second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
104+
$second
105+
->expects($this->once())
106+
->method('choiceFormat')
107+
->with('foo', 1, 'en', array(2))
108+
->willReturn('bar');
109+
110+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
111+
}
112+
113+
public function testChoiceFormatDifferent()
114+
{
115+
$first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
116+
$first
117+
->expects($this->once())
118+
->method('choiceFormat')
119+
->with('foo', 1, 'en', array(2))
120+
->willReturn('new value');
121+
122+
$second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
123+
$second
124+
->expects($this->exactly(0))
125+
->method('choiceFormat')
126+
->withAnyParameters()
127+
->willReturn('bar');
128+
129+
$this->assertEquals('new value', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
130+
}
131+
132+
public function testChoiceFormatException()
133+
{
134+
$first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
135+
$first
136+
->expects($this->once())
137+
->method('choiceFormat')
138+
->willThrowException(new InvalidArgumentException());
139+
140+
$second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
141+
$second
142+
->expects($this->once())
143+
->method('choiceFormat')
144+
->with('foo', 1, 'en', array(2))
145+
->willReturn('bar');
146+
147+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
148+
}
149+
150+
public function testChoiceFormatOnlyFirst()
151+
{
152+
// Implements both interfaces
153+
$first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
154+
$first
155+
->expects($this->once())
156+
->method('choiceFormat')
157+
->with('foo', 1, 'en', array(2))
158+
->willReturn('bar');
159+
160+
// Implements only one interface
161+
$second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
162+
$second
163+
->expects($this->exactly(0))
164+
->method('format')
165+
->withAnyParameters()
166+
->willReturn('error');
167+
168+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
169+
}
170+
171+
public function testChoiceFormatOnlySecond()
172+
{
173+
// Implements only one interface
174+
$first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
175+
$first
176+
->expects($this->exactly(0))
177+
->method('format')
178+
->withAnyParameters()
179+
->willReturn('error');
180+
181+
// Implements both interfaces
182+
$second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
183+
$second
184+
->expects($this->once())
185+
->method('choiceFormat')
186+
->with('foo', 1, 'en', array(2))
187+
->willReturn('bar');
188+
189+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
190+
}
191+
192+
public function testChoiceFormatNoChoiceFormat()
193+
{
194+
// Implements only one interface
195+
$first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
196+
$first
197+
->expects($this->exactly(0))
198+
->method('format');
199+
200+
// Implements both interfaces
201+
$second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
202+
$second
203+
->expects($this->exactly(0))
204+
->method('format');
205+
206+
$this->expectException(LogicException::class);
207+
$this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
208+
}
209+
}
210+
211+
interface SuperFormatterInterface extends MessageFormatterInterface, ChoiceMessageFormatterInterface
212+
{
213+
}

0 commit comments

Comments
 (0)